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/.gitignore b/.gitignore
index 971c945246..b5a46e16f4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -198,3 +198,11 @@ info.plist
build-intermediate
obj-Direct2D1/
obj-Skia/
+
+##################
+# Vim
+##################
+.vim
+coc-settings.json
+.ccls-cache
+.ccls
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 3f4fbb0d50..7e3532ee23 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -102,7 +102,7 @@ jobs:
- job: Windows
pool:
- vmImage: 'vs2017-win2016'
+ vmImage: 'windows-2019'
steps:
- task: CmdLine@2
displayName: 'Install Nuke'
diff --git a/build-native.sh b/build-native.sh
old mode 100644
new mode 100755
diff --git a/build/Base.props b/build/Base.props
index e565ab1664..a60373ebb3 100644
--- a/build/Base.props
+++ b/build/Base.props
@@ -2,4 +2,4 @@
-
\ No newline at end of file
+
diff --git a/build/SharedVersion.props b/build/SharedVersion.props
index 4f0b1f0a5b..76abcf6912 100644
--- a/build/SharedVersion.props
+++ b/build/SharedVersion.props
@@ -2,7 +2,7 @@
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
Avalonia
- 0.8.1
+ 0.8.999
Copyright 2019 © The AvaloniaUI Project
https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md
https://github.com/AvaloniaUI/Avalonia/
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/KeyTransform.mm b/native/Avalonia.Native/src/OSX/KeyTransform.mm
index 7486aaad69..971bcf24f8 100644
--- a/native/Avalonia.Native/src/OSX/KeyTransform.mm
+++ b/native/Avalonia.Native/src/OSX/KeyTransform.mm
@@ -26,7 +26,7 @@ const int kVK_ANSI_3 = 0x14;
const int kVK_ANSI_4 = 0x15;
const int kVK_ANSI_6 = 0x16;
const int kVK_ANSI_5 = 0x17;
-//const int kVK_ANSI_Equal = 0x18;
+const int kVK_ANSI_Equal = 0x18;
const int kVK_ANSI_9 = 0x19;
const int kVK_ANSI_7 = 0x1A;
const int kVK_ANSI_Minus = 0x1B;
@@ -45,11 +45,11 @@ const int kVK_ANSI_K = 0x28;
const int kVK_ANSI_Semicolon = 0x29;
const int kVK_ANSI_Backslash = 0x2A;
const int kVK_ANSI_Comma = 0x2B;
-//const int kVK_ANSI_Slash = 0x2C;
+const int kVK_ANSI_Slash = 0x2C;
const int kVK_ANSI_N = 0x2D;
const int kVK_ANSI_M = 0x2E;
const int kVK_ANSI_Period = 0x2F;
-//const int kVK_ANSI_Grave = 0x32;
+const int kVK_ANSI_Grave = 0x32;
const int kVK_ANSI_KeypadDecimal = 0x41;
const int kVK_ANSI_KeypadMultiply = 0x43;
const int kVK_ANSI_KeypadPlus = 0x45;
@@ -57,7 +57,7 @@ const int kVK_ANSI_KeypadClear = 0x47;
const int kVK_ANSI_KeypadDivide = 0x4B;
const int kVK_ANSI_KeypadEnter = 0x4C;
const int kVK_ANSI_KeypadMinus = 0x4E;
-//const int kVK_ANSI_KeypadEquals = 0x51;
+const int kVK_ANSI_KeypadEquals = 0x51;
const int kVK_ANSI_Keypad0 = 0x52;
const int kVK_ANSI_Keypad1 = 0x53;
const int kVK_ANSI_Keypad2 = 0x54;
@@ -121,7 +121,7 @@ const int kVK_UpArrow = 0x7E;
//const int kVK_JIS_Underscore = 0x5E;
//const int kVK_JIS_KeypadComma = 0x5F;
//const int kVK_JIS_Eisu = 0x66;
-//const int kVK_JIS_Kana = 0x68;
+const int kVK_JIS_Kana = 0x68;
std::map s_KeyMap =
{
@@ -148,7 +148,7 @@ const int kVK_UpArrow = 0x7E;
{kVK_ANSI_4, D4},
{kVK_ANSI_6, D6},
{kVK_ANSI_5, D5},
- //{kVK_ANSI_Equal, ?},
+ {kVK_ANSI_Equal, OemPlus},
{kVK_ANSI_9, D9},
{kVK_ANSI_7, D7},
{kVK_ANSI_Minus, OemMinus},
@@ -167,11 +167,11 @@ const int kVK_UpArrow = 0x7E;
{kVK_ANSI_Semicolon, OemSemicolon},
{kVK_ANSI_Backslash, OemBackslash},
{kVK_ANSI_Comma, OemComma},
- //{kVK_ANSI_Slash, ?},
+ {kVK_ANSI_Slash, Oem2},
{kVK_ANSI_N, N},
{kVK_ANSI_M, M},
{kVK_ANSI_Period, OemPeriod},
- //{kVK_ANSI_Grave, ?},
+ {kVK_ANSI_Grave, OemTilde},
{kVK_ANSI_KeypadDecimal, Decimal},
{kVK_ANSI_KeypadMultiply, Multiply},
{kVK_ANSI_KeypadPlus, OemPlus},
@@ -179,7 +179,7 @@ const int kVK_UpArrow = 0x7E;
{kVK_ANSI_KeypadDivide, Divide},
{kVK_ANSI_KeypadEnter, AvnKeyEnter},
{kVK_ANSI_KeypadMinus, OemMinus},
- //{kVK_ANSI_KeypadEquals, ?},
+ {kVK_ANSI_KeypadEquals, OemPlus},
{kVK_ANSI_Keypad0, NumPad0},
{kVK_ANSI_Keypad1, NumPad1},
{kVK_ANSI_Keypad2, NumPad2},
@@ -237,5 +237,6 @@ const int kVK_UpArrow = 0x7E;
{kVK_LeftArrow, Left},
{kVK_RightArrow, Right},
{kVK_DownArrow, Down},
- {kVK_UpArrow, Up}
+ {kVK_UpArrow, Up},
+ {kVK_JIS_Kana, AvnKeyKanaMode},
};
diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs
index a99ac4d026..dd2f27116d 100644
--- a/nukebuild/Build.cs
+++ b/nukebuild/Build.cs
@@ -89,6 +89,29 @@ partial class Build : NukeBuild
}
+ IReadOnlyCollection
diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs
index 40321496c0..de9ca02ed1 100644
--- a/samples/ControlCatalog.NetCore/Program.cs
+++ b/samples/ControlCatalog.NetCore/Program.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Threading;
using Avalonia;
using Avalonia.Controls;
+using Avalonia.LinuxFramebuffer.Output;
using Avalonia.Skia;
using Avalonia.ReactiveUI;
@@ -29,8 +30,13 @@ namespace ControlCatalog.NetCore
var builder = BuildAvaloniaApp();
if (args.Contains("--fbdev"))
{
- System.Threading.ThreadPool.QueueUserWorkItem(_ => ConsoleSilencer());
- return builder.StartLinuxFramebuffer(args);
+ SilenceConsole();
+ return builder.StartLinuxFbDev(args);
+ }
+ else if (args.Contains("--drm"))
+ {
+ SilenceConsole();
+ return builder.StartLinuxDrm(args);
}
else
return builder.StartWithClassicDesktopLifetime(args);
@@ -51,11 +57,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/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 1cddb9d295..5f24c8062e 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -26,6 +26,7 @@
+
@@ -36,6 +37,7 @@
+
diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
index 0ca3567970..f90a0c4658 100644
--- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
@@ -37,10 +37,6 @@
-
-
+
+
+ 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..214de89253
--- /dev/null
+++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
@@ -0,0 +1,71 @@
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Layout;
+using Avalonia.Markup.Xaml;
+
+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");
+ DataContext = Enumerable.Range(1, 100000).Select(i => $"Item {i}" ).ToArray();
+ }
+
+ 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;
+ }
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/TabStripPage.xaml b/samples/ControlCatalog/Pages/TabStripPage.xaml
new file mode 100644
index 0000000000..a824336f75
--- /dev/null
+++ b/samples/ControlCatalog/Pages/TabStripPage.xaml
@@ -0,0 +1,33 @@
+
+
+ TabStrip
+ A control which displays a selectable strip of tabs
+
+
+
+ Defined in XAML
+
+ Item 1
+ Item 2
+ Disabled
+
+
+
+
+ Dynamically generated
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/TabStripPage.xaml.cs b/samples/ControlCatalog/Pages/TabStripPage.xaml.cs
new file mode 100644
index 0000000000..f0630cf534
--- /dev/null
+++ b/samples/ControlCatalog/Pages/TabStripPage.xaml.cs
@@ -0,0 +1,45 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+
+namespace ControlCatalog.Pages
+{
+ public class TabStripPage : UserControl
+ {
+ public TabStripPage()
+ {
+ InitializeComponent();
+
+ DataContext = new[]
+ {
+ new TabStripItemViewModel
+ {
+ Header = "Item 1",
+ },
+ new TabStripItemViewModel
+ {
+ Header = "Item 2",
+ },
+ new TabStripItemViewModel
+ {
+ Header = "Disabled",
+ IsEnabled = false,
+ },
+ };
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private class TabStripItemViewModel
+ {
+ public string Header { get; set; }
+ public bool IsEnabled { get; set; } = true;
+ }
+ }
+}
diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml
index 3047b1e519..3513e94107 100644
--- a/samples/ControlCatalog/SideBar.xaml
+++ b/samples/ControlCatalog/SideBar.xaml
@@ -29,8 +29,7 @@
Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
- ItemTemplate="{TemplateBinding ItemTemplate}"
- MemberSelector="{TemplateBinding MemberSelector}">
+ ItemTemplate="{TemplateBinding ItemTemplate}">
+ ItemTemplate="{TemplateBinding ItemTemplate}">
-
\ No newline at end of file
+
diff --git a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
index 80e0fb2586..93fe09a156 100644
--- a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
+++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
@@ -9,6 +9,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using ReactiveUI.Legacy;
using ReactiveUI;
+using Avalonia.Layout;
namespace VirtualizationDemo.ViewModels
{
diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj
index 9c3d4fb3a1..0089ea3b8d 100644
--- a/src/Android/Avalonia.Android/Avalonia.Android.csproj
+++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj
@@ -1,6 +1,6 @@
- monoandroid44
+ monoandroid80
true
diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
index c2c0cd4301..1b2b205d45 100644
--- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
+++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
@@ -16,7 +16,7 @@
Resources\Resource.Designer.cs
Off
False
- v4.4
+ v8.0
Properties\AndroidManifest.xml
@@ -150,6 +150,7 @@
+
diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs
index 7601b64ce9..0e2f0feada 100644
--- a/src/Avalonia.Base/AvaloniaObject.cs
+++ b/src/Avalonia.Base/AvaloniaObject.cs
@@ -163,6 +163,37 @@ namespace Avalonia
SetValue(property, AvaloniaProperty.UnsetValue);
}
+ ///
+ /// Compares two objects using reference equality.
+ ///
+ /// The object to compare.
+ ///
+ /// Overriding Equals and GetHashCode on an AvaloniaObject is disallowed for two reasons:
+ ///
+ /// - AvaloniaObjects are by their nature mutable
+ /// - The presence of attached properties means that the semantics of equality are
+ /// difficult to define
+ ///
+ /// See https://github.com/AvaloniaUI/Avalonia/pull/2747 for the discussion that prompted
+ /// this.
+ ///
+ public sealed override bool Equals(object obj) => base.Equals(obj);
+
+ ///
+ /// Gets the hash code for the object.
+ ///
+ ///
+ /// Overriding Equals and GetHashCode on an AvaloniaObject is disallowed for two reasons:
+ ///
+ /// - AvaloniaObjects are by their nature mutable
+ /// - The presence of attached properties means that the semantics of equality are
+ /// difficult to define
+ ///
+ /// See https://github.com/AvaloniaUI/Avalonia/pull/2747 for the discussion that prompted
+ /// this.
+ ///
+ public sealed override int GetHashCode() => base.GetHashCode();
+
///
/// Gets a value.
///
@@ -466,7 +497,7 @@ namespace Avalonia
/// The old property value.
/// The new property value.
/// The priority of the binding that produced the value.
- protected void RaisePropertyChanged(
+ protected internal void RaisePropertyChanged(
AvaloniaProperty property,
object oldValue,
object newValue,
@@ -508,45 +539,6 @@ namespace Avalonia
}
}
- ///
- /// A callback type for encapsulating complex logic for setting direct properties.
- ///
- /// The type of the property.
- /// The value to which to set the property.
- /// The backing field for the property.
- /// A wrapper for the property-changed notification.
- protected delegate void SetAndRaiseCallback(T value, ref T field, Action notifyWrapper);
-
- ///
- /// Sets the backing field for a direct avalonia property, raising the
- /// event if the value has changed.
- ///
- /// The type of the property.
- /// The property.
- /// The backing field.
- /// A callback called to actually set the value to the backing field.
- /// The value.
- ///
- /// True if the value changed, otherwise false.
- ///
- protected bool SetAndRaise(
- AvaloniaProperty property,
- ref T field,
- SetAndRaiseCallback setterCallback,
- T value)
- {
- Contract.Requires(setterCallback != null);
- return Values.Setter.SetAndNotify(
- property,
- ref field,
- (object update, ref T backing, Action notify) =>
- {
- setterCallback((T)update, ref backing, notify);
- return true;
- },
- value);
- }
-
///
/// Sets the backing field for a direct avalonia property, raising the
/// event if the value has changed.
@@ -561,32 +553,15 @@ namespace Avalonia
protected bool SetAndRaise(AvaloniaProperty property, ref T field, T value)
{
VerifyAccess();
- return SetAndRaise(
- property,
- ref field,
- (T val, ref T backing, Action notifyWrapper)
- => SetAndRaiseCore(property, ref backing, val, notifyWrapper),
- value);
- }
- ///
- /// Default assignment logic for SetAndRaise.
- ///
- /// The type of the property.
- /// The property.
- /// The backing field.
- /// The value.
- /// A wrapper for the property-changed notification.
- ///
- /// True if the value changed, otherwise false.
- ///
- private bool SetAndRaiseCore(AvaloniaProperty property, ref T field, T value, Action notifyWrapper)
- {
- var old = field;
- field = value;
+ if (EqualityComparer.Default.Equals(field, value))
+ {
+ return false;
+ }
+
+ DeferredSetter setter = Values.GetDirectDeferredSetter(property);
- notifyWrapper(() => RaisePropertyChanged(property, old, value, BindingPriority.LocalValue));
- return true;
+ return setter.SetAndNotify(this, property, ref field, value);
}
///
diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs
index 15de6c4d0d..44b47329ac 100644
--- a/src/Avalonia.Base/Data/BindingOperations.cs
+++ b/src/Avalonia.Base/Data/BindingOperations.cs
@@ -10,6 +10,8 @@ namespace Avalonia.Data
{
public static class BindingOperations
{
+ public static readonly object DoNothing = new object();
+
///
/// Applies an a property on an .
///
diff --git a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs
index 990a4b04f2..0ffd6a9539 100644
--- a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs
+++ b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs
@@ -31,7 +31,7 @@ namespace Avalonia.Data.Converters
{
if (value == null)
{
- return AvaloniaProperty.UnsetValue;
+ return targetType.IsValueType ? AvaloniaProperty.UnsetValue : null;
}
if (typeof(ICommand).IsAssignableFrom(targetType) && value is Delegate d && d.Method.GetParameters().Length <= 1)
diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs
index f1717bde3b..7f8396cdfa 100644
--- a/src/Avalonia.Base/Data/Core/BindingExpression.cs
+++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs
@@ -114,6 +114,11 @@ namespace Avalonia.Data.Core
///
public void OnNext(object value)
{
+ if (value == BindingOperations.DoNothing)
+ {
+ return;
+ }
+
using (_inner.Subscribe(_ => { }))
{
var type = _inner.ResultType;
@@ -126,6 +131,11 @@ namespace Avalonia.Data.Core
ConverterParameter,
CultureInfo.CurrentCulture);
+ if (converted == BindingOperations.DoNothing)
+ {
+ return;
+ }
+
if (converted == AvaloniaProperty.UnsetValue)
{
converted = TypeUtilities.Default(type);
@@ -186,6 +196,11 @@ namespace Avalonia.Data.Core
///
private object ConvertValue(object value)
{
+ if (value == BindingOperations.DoNothing)
+ {
+ return value;
+ }
+
var notification = value as BindingNotification;
if (notification == null)
@@ -196,6 +211,11 @@ namespace Avalonia.Data.Core
ConverterParameter,
CultureInfo.CurrentCulture);
+ if (converted == BindingOperations.DoNothing)
+ {
+ return converted;
+ }
+
notification = converted as BindingNotification;
if (notification?.ErrorType == BindingErrorType.None)
@@ -327,7 +347,18 @@ namespace Avalonia.Data.Core
public void OnNext(object value)
{
+ if (value == BindingOperations.DoNothing)
+ {
+ return;
+ }
+
var converted = _owner.ConvertValue(value);
+
+ if (converted == BindingOperations.DoNothing)
+ {
+ return;
+ }
+
_owner._value = new WeakReference
void VerifyAccess();
-
- DeferredSetter Setter { get; }
}
}
diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs
index 89a893577f..4996420fe7 100644
--- a/src/Avalonia.Base/PriorityValue.cs
+++ b/src/Avalonia.Base/PriorityValue.cs
@@ -30,7 +30,9 @@ namespace Avalonia
private readonly SingleOrDictionary _levels = new SingleOrDictionary();
private readonly Func _validate;
+ private readonly SetAndNotifyCallback<(object, int)> _setAndNotifyCallback;
private (object value, int priority) _value;
+ private DeferredSetter _setter;
///
/// Initializes a new instance of the class.
@@ -50,6 +52,7 @@ namespace Avalonia
_valueType = valueType;
_value = (AvaloniaProperty.UnsetValue, int.MaxValue);
_validate = validate;
+ _setAndNotifyCallback = SetAndNotify;
}
///
@@ -242,22 +245,22 @@ namespace Avalonia
/// The priority level that the value came from.
private void UpdateValue(object value, int priority)
{
- Owner.Setter.SetAndNotify(Property,
- ref _value,
- UpdateCore,
- (value, priority));
- }
+ var newValue = (value, priority);
+
+ if (newValue == _value)
+ {
+ return;
+ }
- private bool UpdateCore(
- object update,
- ref (object value, int priority) backing,
- Action notify)
- => UpdateCore(((object, int))update, ref backing, notify);
+ if (_setter == null)
+ {
+ _setter = Owner.GetNonDirectDeferredSetter(Property);
+ }
+
+ _setter.SetAndNotifyCallback(Property, _setAndNotifyCallback, ref _value, newValue);
+ }
- private bool UpdateCore(
- (object value, int priority) update,
- ref (object value, int priority) backing,
- Action notify)
+ private void SetAndNotify(AvaloniaProperty property, ref (object value, int priority) backing, (object value, int priority) update)
{
var val = update.value;
var notification = val as BindingNotification;
@@ -286,7 +289,7 @@ namespace Avalonia
if (notification == null || notification.HasValue)
{
- notify(() => Owner?.Changed(Property, ValuePriority, old, Value));
+ Owner?.Changed(Property, ValuePriority, old, Value);
}
if (notification != null)
@@ -305,7 +308,6 @@ namespace Avalonia
val,
val?.GetType());
}
- return true;
}
}
}
diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
new file mode 100644
index 0000000000..ac128d83de
--- /dev/null
+++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
@@ -0,0 +1,150 @@
+// 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.Utilities
+{
+ ///
+ /// Stores values with as key.
+ ///
+ /// Stored value type.
+ internal sealed class AvaloniaPropertyValueStore
+ {
+ private Entry[] _entries;
+
+ public AvaloniaPropertyValueStore()
+ {
+ // The last item in the list is always int.MaxValue
+ _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } };
+ }
+
+ private (int, bool) TryFindEntry(int propertyId)
+ {
+ if (_entries.Length <= 12)
+ {
+ // For small lists, we use an optimized linear search. Since the last item in the list
+ // is always int.MaxValue, we can skip a conditional branch in each iteration.
+ // By unrolling the loop, we can skip another unconditional branch in each iteration.
+
+ if (_entries[0].PropertyId >= propertyId)
+ return (0, _entries[0].PropertyId == propertyId);
+ if (_entries[1].PropertyId >= propertyId)
+ return (1, _entries[1].PropertyId == propertyId);
+ if (_entries[2].PropertyId >= propertyId)
+ return (2, _entries[2].PropertyId == propertyId);
+ if (_entries[3].PropertyId >= propertyId)
+ return (3, _entries[3].PropertyId == propertyId);
+ if (_entries[4].PropertyId >= propertyId)
+ return (4, _entries[4].PropertyId == propertyId);
+ if (_entries[5].PropertyId >= propertyId)
+ return (5, _entries[5].PropertyId == propertyId);
+ if (_entries[6].PropertyId >= propertyId)
+ return (6, _entries[6].PropertyId == propertyId);
+ if (_entries[7].PropertyId >= propertyId)
+ return (7, _entries[7].PropertyId == propertyId);
+ if (_entries[8].PropertyId >= propertyId)
+ return (8, _entries[8].PropertyId == propertyId);
+ if (_entries[9].PropertyId >= propertyId)
+ return (9, _entries[9].PropertyId == propertyId);
+ if (_entries[10].PropertyId >= propertyId)
+ return (10, _entries[10].PropertyId == propertyId);
+ }
+ else
+ {
+ int low = 0;
+ int high = _entries.Length;
+ int id;
+
+ while (high - low > 3)
+ {
+ int pivot = (high + low) / 2;
+ id = _entries[pivot].PropertyId;
+
+ if (propertyId == id)
+ return (pivot, true);
+
+ if (propertyId <= id)
+ high = pivot;
+ else
+ low = pivot + 1;
+ }
+
+ do
+ {
+ id = _entries[low].PropertyId;
+
+ if (id == propertyId)
+ return (low, true);
+
+ if (id > propertyId)
+ break;
+
+ ++low;
+ }
+ while (low < high);
+ }
+
+ return (0, false);
+ }
+
+ public bool TryGetValue(AvaloniaProperty property, out TValue value)
+ {
+ (int index, bool found) = TryFindEntry(property.Id);
+ if (!found)
+ {
+ value = default;
+ return false;
+ }
+
+ value = _entries[index].Value;
+ return true;
+ }
+
+ public void AddValue(AvaloniaProperty property, TValue value)
+ {
+ Entry[] entries = new Entry[_entries.Length + 1];
+
+ for (int i = 0; i < _entries.Length; ++i)
+ {
+ if (_entries[i].PropertyId > property.Id)
+ {
+ if (i > 0)
+ {
+ Array.Copy(_entries, 0, entries, 0, i);
+ }
+
+ entries[i] = new Entry { PropertyId = property.Id, Value = value };
+ Array.Copy(_entries, i, entries, i + 1, _entries.Length - i);
+ break;
+ }
+ }
+
+ _entries = entries;
+ }
+
+ public void SetValue(AvaloniaProperty property, TValue value)
+ {
+ _entries[TryFindEntry(property.Id).Item1].Value = value;
+ }
+
+ public Dictionary ToDictionary()
+ {
+ var dict = new Dictionary(_entries.Length - 1);
+
+ for (int i = 0; i < _entries.Length - 1; ++i)
+ {
+ dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value);
+ }
+
+ return dict;
+ }
+
+ private struct Entry
+ {
+ internal int PropertyId;
+ internal TValue Value;
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Utilities/DeferredSetter.cs b/src/Avalonia.Base/Utilities/DeferredSetter.cs
index 1b1324b1c5..fd7a66fb52 100644
--- a/src/Avalonia.Base/Utilities/DeferredSetter.cs
+++ b/src/Avalonia.Base/Utilities/DeferredSetter.cs
@@ -1,168 +1,122 @@
-using System;
-using System.Collections.Generic;
+// 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;
namespace Avalonia.Utilities
{
+ ///
+ /// Callback invoked when deferred setter wants to set a value.
+ ///
+ /// Value type.
+ /// Property being set.
+ /// Backing field reference.
+ /// New value.
+ internal delegate void SetAndNotifyCallback(AvaloniaProperty property, ref TValue backing, TValue value);
+
///
/// A utility class to enable deferring assignment until after property-changed notifications are sent.
/// Used to fix #855.
///
/// The type of value with which to track the delayed assignment.
- class DeferredSetter
+ internal sealed class DeferredSetter
{
- private struct NotifyDisposable : IDisposable
+ private readonly SingleOrQueue _pendingValues;
+ private bool _isNotifying;
+
+ public DeferredSetter()
{
- private readonly SettingStatus status;
+ _pendingValues = new SingleOrQueue();
+ }
- internal NotifyDisposable(SettingStatus status)
- {
- this.status = status;
- status.Notifying = true;
- }
+ private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty property, ref TSetRecord backing, TSetRecord value)
+ {
+ var old = backing;
- public void Dispose()
- {
- status.Notifying = false;
- }
+ backing = value;
+
+ source.RaisePropertyChanged(property, old, value);
}
- ///
- /// Information on current setting/notification status of a property.
- ///
- private class SettingStatus
+ public bool SetAndNotify(
+ AvaloniaObject source,
+ AvaloniaProperty property,
+ ref TSetRecord backing,
+ TSetRecord value)
{
- public bool Notifying { get; set; }
-
- private SingleOrQueue pendingValues;
-
- public SingleOrQueue PendingValues
+ if (!_isNotifying)
{
- get
+ using (new NotifyDisposable(this))
{
- return pendingValues ?? (pendingValues = new SingleOrQueue());
+ SetAndRaisePropertyChanged(source, property, ref backing, value);
}
- }
- }
- private Dictionary _setRecords;
- private Dictionary SetRecords
- => _setRecords ?? (_setRecords = new Dictionary());
+ if (!_pendingValues.Empty)
+ {
+ using (new NotifyDisposable(this))
+ {
+ while (!_pendingValues.Empty)
+ {
+ SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue());
+ }
+ }
+ }
- private SettingStatus GetOrCreateStatus(AvaloniaProperty property)
- {
- if (!SetRecords.TryGetValue(property, out var status))
- {
- status = new SettingStatus();
- SetRecords.Add(property, status);
+ return true;
}
- return status;
+ _pendingValues.Enqueue(value);
+
+ return false;
}
- ///
- /// Mark the property as currently notifying.
- ///
- /// The property to mark as notifying.
- /// Returns a disposable that when disposed, marks the property as done notifying.
- private NotifyDisposable MarkNotifying(AvaloniaProperty property)
+ public bool SetAndNotifyCallback(AvaloniaProperty property, SetAndNotifyCallback setAndNotifyCallback, ref TValue backing, TValue value)
+ where TValue : TSetRecord
{
- Contract.Requires(!IsNotifying(property));
-
- SettingStatus status = GetOrCreateStatus(property);
-
- return new NotifyDisposable(status);
- }
+ if (!_isNotifying)
+ {
+ using (new NotifyDisposable(this))
+ {
+ setAndNotifyCallback(property, ref backing, value);
+ }
- ///
- /// Check if the property is currently notifying listeners.
- ///
- /// The property.
- /// If the property is currently notifying listeners.
- private bool IsNotifying(AvaloniaProperty property)
- => SetRecords.TryGetValue(property, out var value) && value.Notifying;
+ if (!_pendingValues.Empty)
+ {
+ using (new NotifyDisposable(this))
+ {
+ while (!_pendingValues.Empty)
+ {
+ setAndNotifyCallback(property, ref backing, (TValue) _pendingValues.Dequeue());
+ }
+ }
+ }
- ///
- /// Add a pending assignment for the property.
- ///
- /// The property.
- /// The value to assign.
- private void AddPendingSet(AvaloniaProperty property, TSetRecord value)
- {
- Contract.Requires(IsNotifying(property));
+ return true;
+ }
- GetOrCreateStatus(property).PendingValues.Enqueue(value);
- }
+ _pendingValues.Enqueue(value);
- ///
- /// Checks if there are any pending assignments for the property.
- ///
- /// The property to check.
- /// If the property has any pending assignments.
- private bool HasPendingSet(AvaloniaProperty property)
- {
- return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty;
+ return false;
}
///
- /// Gets the first pending assignment for the property.
+ /// Disposable that marks the property as currently notifying.
+ /// When disposed, marks the property as done notifying.
///
- /// The property to check.
- /// The first pending assignment for the property.
- private TSetRecord GetFirstPendingSet(AvaloniaProperty property)
+ private readonly struct NotifyDisposable : IDisposable
{
- return GetOrCreateStatus(property).PendingValues.Dequeue();
- }
-
- public delegate bool SetterDelegate(TSetRecord record, ref TValue backing, Action notifyCallback);
+ private readonly DeferredSetter _setter;
- ///
- /// Set the property and notify listeners while ensuring we don't get into a stack overflow as happens with #855 and #824
- ///
- /// The property to set.
- /// The backing field for the property
- ///
- /// A callback that actually sets the property.
- /// The first parameter is the value to set, and the second is a wrapper that takes a callback that sends the property-changed notification.
- ///
- /// The value to try to set.
- public bool SetAndNotify(
- AvaloniaProperty property,
- ref TValue backing,
- SetterDelegate setterCallback,
- TSetRecord value)
- {
- Contract.Requires(setterCallback != null);
- if (!IsNotifying(property))
+ internal NotifyDisposable(DeferredSetter setter)
{
- bool updated = false;
- if (!object.Equals(value, backing))
- {
- updated = setterCallback(value, ref backing, notification =>
- {
- using (MarkNotifying(property))
- {
- notification();
- }
- });
- }
- while (HasPendingSet(property))
- {
- updated |= setterCallback(GetFirstPendingSet(property), ref backing, notification =>
- {
- using (MarkNotifying(property))
- {
- notification();
- }
- });
- }
-
- return updated;
+ _setter = setter;
+ _setter._isNotifying = true;
}
- else if(!object.Equals(value, backing))
+
+ public void Dispose()
{
- AddPendingSet(property, value);
+ _setter._isNotifying = false;
}
- return false;
}
}
}
diff --git a/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs b/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs
new file mode 100644
index 0000000000..f321b2b2f1
--- /dev/null
+++ b/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace Avalonia.Utilities
+{
+ ///
+ /// A task-like operation that is guaranteed to finish continuations synchronously,
+ /// can be used for parametrized one-shot events
+ ///
+ public struct SynchronousCompletionAsyncResult : INotifyCompletion
+ {
+ private readonly SynchronousCompletionAsyncResultSource _source;
+ private readonly T _result;
+ private readonly bool _isValid;
+ internal SynchronousCompletionAsyncResult(SynchronousCompletionAsyncResultSource source)
+ {
+ _source = source;
+ _result = default;
+ _isValid = true;
+ }
+
+ public SynchronousCompletionAsyncResult(T result)
+ {
+ _result = result;
+ _source = null;
+ _isValid = true;
+ }
+
+ static void ThrowNotInitialized() =>
+ throw new InvalidOperationException("This SynchronousCompletionAsyncResult was not initialized");
+
+ public bool IsCompleted
+ {
+ get
+ {
+ if (!_isValid)
+ ThrowNotInitialized();
+ return _source == null || _source.IsCompleted;
+ }
+ }
+
+ public T GetResult()
+ {
+ if (!_isValid)
+ ThrowNotInitialized();
+ return _source == null ? _result : _source.Result;
+ }
+
+
+ public void OnCompleted(Action continuation)
+ {
+ if (!_isValid)
+ ThrowNotInitialized();
+ if (_source == null)
+ continuation();
+ else
+ _source.OnCompleted(continuation);
+ }
+
+ public SynchronousCompletionAsyncResult GetAwaiter() => this;
+ }
+
+ ///
+ /// Source for incomplete SynchronousCompletionAsyncResult
+ ///
+ ///
+ public class SynchronousCompletionAsyncResultSource
+ {
+ private T _result;
+ internal bool IsCompleted { get; private set; }
+ public SynchronousCompletionAsyncResult AsyncResult => new SynchronousCompletionAsyncResult(this);
+
+ internal T Result => IsCompleted ?
+ _result :
+ throw new InvalidOperationException("Asynchronous operation is not yet completed");
+
+ private List _continuations;
+
+ internal void OnCompleted(Action continuation)
+ {
+ if(_continuations==null)
+ _continuations = new List();
+ _continuations.Add(continuation);
+ }
+
+ public void SetResult(T result)
+ {
+ if (IsCompleted)
+ throw new InvalidOperationException("Asynchronous operation is already completed");
+ _result = result;
+ IsCompleted = true;
+ if(_continuations!=null)
+ foreach (var c in _continuations)
+ c();
+ _continuations = null;
+ }
+
+ public void TrySetResult(T result)
+ {
+ if(IsCompleted)
+ return;
+ SetResult(result);
+ }
+ }
+}
diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs
index 24f85ea6b1..1bdbd4ca7c 100644
--- a/src/Avalonia.Base/ValueStore.cs
+++ b/src/Avalonia.Base/ValueStore.cs
@@ -7,21 +7,15 @@ namespace Avalonia
{
internal class ValueStore : IPriorityValueOwner
{
- private struct Entry
- {
- internal int PropertyId;
- internal object Value;
- }
-
+ private readonly AvaloniaPropertyValueStore _propertyValues;
+ private readonly AvaloniaPropertyValueStore _deferredSetters;
private readonly AvaloniaObject _owner;
- private Entry[] _entries;
public ValueStore(AvaloniaObject owner)
{
_owner = owner;
-
- // The last item in the list is always int.MaxValue
- _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = null } };
+ _propertyValues = new AvaloniaPropertyValueStore();
+ _deferredSetters = new AvaloniaPropertyValueStore();
}
public IDisposable AddBinding(
@@ -31,7 +25,7 @@ namespace Avalonia
{
PriorityValue priorityValue;
- if (TryGetValue(property, out var v))
+ if (_propertyValues.TryGetValue(property, out var v))
{
priorityValue = v as PriorityValue;
@@ -39,13 +33,13 @@ namespace Avalonia
{
priorityValue = CreatePriorityValue(property);
priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
- SetValueInternal(property, priorityValue);
+ _propertyValues.SetValue(property, priorityValue);
}
}
else
{
priorityValue = CreatePriorityValue(property);
- AddValueInternal(property, priorityValue);
+ _propertyValues.AddValue(property, priorityValue);
}
return priorityValue.Add(source, (int)priority);
@@ -55,7 +49,7 @@ namespace Avalonia
{
PriorityValue priorityValue;
- if (TryGetValue(property, out var v))
+ if (_propertyValues.TryGetValue(property, out var v))
{
priorityValue = v as PriorityValue;
@@ -63,7 +57,7 @@ namespace Avalonia
{
if (priority == (int)BindingPriority.LocalValue)
{
- SetValueInternal(property, Validate(property, value));
+ _propertyValues.SetValue(property, Validate(property, value));
Changed(property, priority, v, value);
return;
}
@@ -71,7 +65,7 @@ namespace Avalonia
{
priorityValue = CreatePriorityValue(property);
priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
- SetValueInternal(property, priorityValue);
+ _propertyValues.SetValue(property, priorityValue);
}
}
}
@@ -84,14 +78,14 @@ namespace Avalonia
if (priority == (int)BindingPriority.LocalValue)
{
- AddValueInternal(property, Validate(property, value));
+ _propertyValues.AddValue(property, Validate(property, value));
Changed(property, priority, AvaloniaProperty.UnsetValue, value);
return;
}
else
{
priorityValue = CreatePriorityValue(property);
- AddValueInternal(property, priorityValue);
+ _propertyValues.AddValue(property, priorityValue);
}
}
@@ -110,14 +104,9 @@ namespace Avalonia
public IDictionary GetSetValues()
{
- var dict = new Dictionary(_entries.Length - 1);
- for (int i = 0; i < _entries.Length - 1; ++i)
- {
- dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value);
- }
-
- return dict;
+ return _propertyValues.ToDictionary();
}
+
public void LogError(AvaloniaProperty property, Exception e)
{
_owner.LogBindingError(property, e);
@@ -127,7 +116,7 @@ namespace Avalonia
{
var result = AvaloniaProperty.UnsetValue;
- if (TryGetValue(property, out var value))
+ if (_propertyValues.TryGetValue(property, out var value))
{
result = (value is PriorityValue priorityValue) ? priorityValue.Value : value;
}
@@ -137,12 +126,12 @@ namespace Avalonia
public bool IsAnimating(AvaloniaProperty property)
{
- return TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
+ return _propertyValues.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
}
public bool IsSet(AvaloniaProperty property)
{
- if (TryGetValue(property, out var value))
+ if (_propertyValues.TryGetValue(property, out var value))
{
return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue;
}
@@ -152,7 +141,7 @@ namespace Avalonia
public void Revalidate(AvaloniaProperty property)
{
- if (TryGetValue(property, out var value))
+ if (_propertyValues.TryGetValue(property, out var value))
{
(value as PriorityValue)?.Revalidate();
}
@@ -189,113 +178,28 @@ namespace Avalonia
return value;
}
- private DeferredSetter _deferredSetter;
-
- public DeferredSetter Setter
+ private DeferredSetter GetDeferredSetter(AvaloniaProperty property)
{
- get
+ if (_deferredSetters.TryGetValue(property, out var deferredSetter))
{
- return _deferredSetter ??
- (_deferredSetter = new DeferredSetter());
+ return (DeferredSetter)deferredSetter;
}
- }
- private bool TryGetValue(AvaloniaProperty property, out object value)
- {
- (int index, bool found) = TryFindEntry(property.Id);
- if (!found)
- {
- value = null;
- return false;
- }
-
- value = _entries[index].Value;
- return true;
- }
-
- private void AddValueInternal(AvaloniaProperty property, object value)
- {
- Entry[] entries = new Entry[_entries.Length + 1];
-
- for (int i = 0; i < _entries.Length; ++i)
- {
- if (_entries[i].PropertyId > property.Id)
- {
- if (i > 0)
- {
- Array.Copy(_entries, 0, entries, 0, i);
- }
+ var newDeferredSetter = new DeferredSetter();
- entries[i] = new Entry { PropertyId = property.Id, Value = value };
- Array.Copy(_entries, i, entries, i + 1, _entries.Length - i);
- break;
- }
- }
+ _deferredSetters.AddValue(property, newDeferredSetter);
- _entries = entries;
+ return newDeferredSetter;
}
- private void SetValueInternal(AvaloniaProperty property, object value)
+ public DeferredSetter GetNonDirectDeferredSetter(AvaloniaProperty property)
{
- _entries[TryFindEntry(property.Id).Item1].Value = value;
+ return GetDeferredSetter(property);
}
- private (int, bool) TryFindEntry(int propertyId)
+ public DeferredSetter GetDirectDeferredSetter(AvaloniaProperty property)
{
- if (_entries.Length <= 12)
- {
- // For small lists, we use an optimized linear search. Since the last item in the list
- // is always int.MaxValue, we can skip a conditional branch in each iteration.
- // By unrolling the loop, we can skip another unconditional branch in each iteration.
-
- if (_entries[0].PropertyId >= propertyId) return (0, _entries[0].PropertyId == propertyId);
- if (_entries[1].PropertyId >= propertyId) return (1, _entries[1].PropertyId == propertyId);
- if (_entries[2].PropertyId >= propertyId) return (2, _entries[2].PropertyId == propertyId);
- if (_entries[3].PropertyId >= propertyId) return (3, _entries[3].PropertyId == propertyId);
- if (_entries[4].PropertyId >= propertyId) return (4, _entries[4].PropertyId == propertyId);
- if (_entries[5].PropertyId >= propertyId) return (5, _entries[5].PropertyId == propertyId);
- if (_entries[6].PropertyId >= propertyId) return (6, _entries[6].PropertyId == propertyId);
- if (_entries[7].PropertyId >= propertyId) return (7, _entries[7].PropertyId == propertyId);
- if (_entries[8].PropertyId >= propertyId) return (8, _entries[8].PropertyId == propertyId);
- if (_entries[9].PropertyId >= propertyId) return (9, _entries[9].PropertyId == propertyId);
- if (_entries[10].PropertyId >= propertyId) return (10, _entries[10].PropertyId == propertyId);
- }
- else
- {
- int low = 0;
- int high = _entries.Length;
- int id;
-
- while (high - low > 3)
- {
- int pivot = (high + low) / 2;
- id = _entries[pivot].PropertyId;
-
- if (propertyId == id)
- return (pivot, true);
-
- if (propertyId <= id)
- high = pivot;
- else
- low = pivot + 1;
- }
-
- do
- {
- id = _entries[low].PropertyId;
-
- if (id == propertyId)
- return (low, true);
-
- if (id > propertyId)
- break;
-
- ++low;
- }
- while (low < high);
- }
-
- return (0, false);
+ return GetDeferredSetter(property);
}
}
}
diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
index c54d8e19a1..0dd52c9b48 100644
--- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
+++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
@@ -75,9 +75,9 @@ namespace Avalonia.Build.Tasks
.First(c => c.Parameters.Count == 1));
var runtimeHelpers = typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.XamlIlRuntimeHelpers");
- var rootServiceProviderField = asm.MainModule.ImportReference(
- typeSystem.GetTypeReference(runtimeHelpers).Resolve().Fields
- .First(x => x.Name == "RootServiceProviderV1"));
+ var createRootServiceProviderMethod = asm.MainModule.ImportReference(
+ typeSystem.GetTypeReference(runtimeHelpers).Resolve().Methods
+ .First(x => x.Name == "CreateRootServiceProviderV2"));
var loaderDispatcherDef = new TypeDefinition("CompiledAvaloniaXaml", "!XamlLoader",
TypeAttributes.Class, asm.MainModule.TypeSystem.Object);
@@ -211,7 +211,7 @@ namespace Avalonia.Build.Tasks
trampoline.Parameters.Add(new ParameterDefinition(classTypeDefinition));
classTypeDefinition.Methods.Add(trampoline);
- var regularStart = Instruction.Create(OpCodes.Ldsfld, rootServiceProviderField);
+ var regularStart = Instruction.Create(OpCodes.Call, createRootServiceProviderMethod);
trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldsfld, designLoaderField));
trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Brfalse, regularStart));
@@ -307,7 +307,7 @@ namespace Avalonia.Build.Tasks
i.Add(Instruction.Create(OpCodes.Newobj, parameterlessConstructor));
else
{
- i.Add(Instruction.Create(OpCodes.Ldsfld, rootServiceProviderField));
+ i.Add(Instruction.Create(OpCodes.Call, createRootServiceProviderMethod));
i.Add(Instruction.Create(OpCodes.Call, compiledBuildMethod));
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs
index bcd12fbfbb..490a724eda 100644
--- a/src/Avalonia.Controls.DataGrid/DataGrid.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs
@@ -24,6 +24,7 @@ using System.Linq;
using Avalonia.Input.Platform;
using System.ComponentModel.DataAnnotations;
using Avalonia.Controls.Utils;
+using Avalonia.Layout;
namespace Avalonia.Controls
{
diff --git a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml
index 2889a5c77c..eaa267ba66 100644
--- a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml
+++ b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml
@@ -195,7 +195,6 @@
-
@@ -230,4 +229,4 @@
-
\ No newline at end of file
+
diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs
index d4c3b27f7a..2963c019cc 100644
--- a/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs
+++ b/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs
@@ -6,7 +6,7 @@ using System;
namespace Avalonia.Controls.ApplicationLifetimes
{
///
- /// Contains the arguments for the event.
+ /// Contains the arguments for the event.
///
public class ControlledApplicationLifetimeExitEventArgs : EventArgs
{
diff --git a/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs
index 4c08712707..423832793e 100644
--- a/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs
+++ b/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs
@@ -8,7 +8,7 @@ using System.Linq;
namespace Avalonia.Controls.ApplicationLifetimes
{
///
- /// Contains the arguments for the event.
+ /// Contains the arguments for the event.
///
public class ControlledApplicationLifetimeStartupEventArgs : EventArgs
{
diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs
index b87e10d284..1e2fc9f9d0 100644
--- a/src/Avalonia.Controls/AutoCompleteBox.cs
+++ b/src/Avalonia.Controls/AutoCompleteBox.cs
@@ -345,7 +345,6 @@ namespace Avalonia.Controls
///
private IDisposable _collectionChangeSubscription;
- private IMemberSelector _valueMemberSelector;
private Func>> _asyncPopulator;
private CancellationTokenSource _populationCancellationTokenSource;
@@ -541,12 +540,6 @@ namespace Avalonia.Controls
o => o.Items,
(o, v) => o.Items = v);
- public static readonly DirectProperty ValueMemberSelectorProperty =
- AvaloniaProperty.RegisterDirect(
- nameof(ValueMemberSelector),
- o => o.ValueMemberSelector,
- (o, v) => o.ValueMemberSelector = v);
-
public static readonly DirectProperty>>> AsyncPopulatorProperty =
AvaloniaProperty.RegisterDirect>>>(
nameof(AsyncPopulator),
@@ -795,7 +788,7 @@ namespace Avalonia.Controls
var template =
new FuncDataTemplate(
typeof(object),
- o =>
+ (o, _) =>
{
var control = new ContentControl();
control.Bind(ContentControl.ContentProperty, value);
@@ -958,20 +951,6 @@ namespace Avalonia.Controls
}
}
- ///
- /// Gets or sets the MemberSelector that is used to get values for
- /// display in the text portion of the
- /// control.
- ///
- /// The MemberSelector that is used to get values for display in
- /// the text portion of the
- /// control.
- public IMemberSelector ValueMemberSelector
- {
- get { return _valueMemberSelector; }
- set { SetAndRaise(ValueMemberSelectorProperty, ref _valueMemberSelector, value); }
- }
-
///
/// Gets or sets the selected item in the drop-down.
///
@@ -1841,11 +1820,6 @@ namespace Avalonia.Controls
return _valueBindingEvaluator.GetDynamicValue(value) ?? String.Empty;
}
- if (_valueMemberSelector != null)
- {
- value = _valueMemberSelector.Select(value);
- }
-
return value == null ? String.Empty : value.ToString();
}
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 5d427df5a6..f32b8fabc6 100644
--- a/src/Avalonia.Controls/ComboBox.cs
+++ b/src/Avalonia.Controls/ComboBox.cs
@@ -333,8 +333,7 @@ namespace Avalonia.Controls
}
else
{
- var selector = MemberSelector;
- SelectionBoxItem = selector != null ? selector.Select(item) : item;
+ SelectionBoxItem = item;
}
}
diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs
index 92293a32d6..d0804107b3 100644
--- a/src/Avalonia.Controls/ContextMenu.cs
+++ b/src/Avalonia.Controls/ContextMenu.cs
@@ -1,12 +1,13 @@
using System;
using System.ComponentModel;
using System.Linq;
-using System.Reactive.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Layout;
using Avalonia.LogicalTree;
namespace Avalonia.Controls
@@ -90,9 +91,14 @@ namespace Avalonia.Controls
/// The control.
public void Open(Control control)
{
+ if (IsOpen)
+ {
+ return;
+ }
+
if (_popup == null)
{
- _popup = new Popup()
+ _popup = new Popup
{
PlacementMode = PlacementMode.Pointer,
PlacementTarget = control,
@@ -107,7 +113,14 @@ namespace Avalonia.Controls
((ISetLogicalParent)_popup).SetParent(control);
_popup.Child = this;
_popup.IsOpen = true;
+
IsOpen = true;
+
+ RaiseEvent(new RoutedEventArgs
+ {
+ RoutedEvent = MenuOpenedEvent,
+ Source = this,
+ });
}
///
@@ -115,13 +128,15 @@ namespace Avalonia.Controls
///
public override void Close()
{
+ if (!IsOpen)
+ {
+ return;
+ }
+
if (_popup != null && _popup.IsVisible)
{
_popup.IsOpen = false;
}
-
- SelectedIndex = -1;
- IsOpen = false;
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
@@ -129,6 +144,18 @@ namespace Avalonia.Controls
return new MenuItemContainerGenerator(this);
}
+ private void CloseCore()
+ {
+ SelectedIndex = -1;
+ IsOpen = false;
+
+ RaiseEvent(new RoutedEventArgs
+ {
+ RoutedEvent = MenuClosedEvent,
+ Source = this,
+ });
+ }
+
private void PopupOpened(object sender, EventArgs e)
{
Focus();
@@ -145,8 +172,7 @@ namespace Avalonia.Controls
i.IsSubMenuOpen = false;
}
- contextMenu.IsOpen = false;
- contextMenu.SelectedIndex = -1;
+ contextMenu.CloseCore();
}
}
diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs
index 83d1c4aae3..2d48a7d33b 100644
--- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs
+++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs
@@ -8,7 +8,7 @@ using JetBrains.Annotations;
namespace Avalonia.Controls.Embedding
{
- public class EmbeddableControlRoot : TopLevel, IStyleable, IFocusScope, INameScope, IDisposable
+ public class EmbeddableControlRoot : TopLevel, IStyleable, IFocusScope, IDisposable
{
public EmbeddableControlRoot(IEmbeddableWindowImpl impl) : base(impl)
{
@@ -51,25 +51,6 @@ namespace Avalonia.Controls.Embedding
return rv;
}
- private readonly NameScope _nameScope = new NameScope();
- public event EventHandler Registered
- {
- add { _nameScope.Registered += value; }
- remove { _nameScope.Registered -= value; }
- }
-
- public event EventHandler Unregistered
- {
- add { _nameScope.Unregistered += value; }
- remove { _nameScope.Unregistered -= value; }
- }
-
- public void Register(string name, object element) => _nameScope.Register(name, element);
-
- public object Find(string name) => _nameScope.Find(name);
-
- public void Unregister(string name) => _nameScope.Unregister(name);
-
Type IStyleable.StyleKey => typeof(EmbeddableControlRoot);
public void Dispose() => PlatformImpl?.Dispose();
}
diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs
index c4f83ffd54..d326ab5734 100644
--- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs
+++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs
@@ -30,25 +30,6 @@ namespace Avalonia.Controls.Embedding.Offscreen
init.EndInit();
}
}
-
- private readonly NameScope _nameScope = new NameScope();
- public event EventHandler Registered
- {
- add { _nameScope.Registered += value; }
- remove { _nameScope.Registered -= value; }
- }
-
- public event EventHandler Unregistered
- {
- add { _nameScope.Unregistered += value; }
- remove { _nameScope.Unregistered -= value; }
- }
-
- public void Register(string name, object element) => _nameScope.Register(name, element);
-
- public object Find(string name) => _nameScope.Find(name);
-
- public void Unregister(string name) => _nameScope.Unregister(name);
Type IStyleable.StyleKey => typeof(EmbeddableControlRoot);
public void Dispose()
diff --git a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
index 653a4f5dcb..2d6757219d 100644
--- a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
+++ b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
@@ -49,12 +49,8 @@ namespace Avalonia.Controls.Generators
/// The index of the item of data in the control's items.
///
/// The item.
- /// An optional member selector.
/// The created controls.
- ItemContainerInfo Materialize(
- int index,
- object item,
- IMemberSelector selector);
+ ItemContainerInfo Materialize(int index, object item);
///
/// Removes a set of created containers.
@@ -84,11 +80,7 @@ namespace Avalonia.Controls.Generators
/// The removed containers.
IEnumerable RemoveRange(int startingIndex, int count);
- bool TryRecycle(
- int oldIndex,
- int newIndex,
- object item,
- IMemberSelector selector);
+ bool TryRecycle(int oldIndex, int newIndex, object item);
///
/// Clears all created containers and returns the removed controls.
diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
index f1a1f94a01..4fd6f4135c 100644
--- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
+++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
@@ -54,13 +54,9 @@ namespace Avalonia.Controls.Generators
public virtual Type ContainerType => null;
///
- public ItemContainerInfo Materialize(
- int index,
- object item,
- IMemberSelector selector)
+ public ItemContainerInfo Materialize(int index, object item)
{
- var i = selector != null ? selector.Select(item) : item;
- var container = new ItemContainerInfo(CreateContainer(i), item, index);
+ var container = new ItemContainerInfo(CreateContainer(item), item, index);
_containers.Add(container.Index, container);
Materialized?.Invoke(this, new ItemContainerEventArgs(container));
@@ -138,14 +134,7 @@ namespace Avalonia.Controls.Generators
}
///
- public virtual bool TryRecycle(
- int oldIndex,
- int newIndex,
- object item,
- IMemberSelector selector)
- {
- return false;
- }
+ public virtual bool TryRecycle(int oldIndex, int newIndex, object item) => false;
///
public virtual IEnumerable Clear()
diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
index 320d6c8faf..d1d1c2b172 100644
--- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
+++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
@@ -79,11 +79,7 @@ namespace Avalonia.Controls.Generators
}
///
- public override bool TryRecycle(
- int oldIndex,
- int newIndex,
- object item,
- IMemberSelector selector)
+ public override bool TryRecycle(int oldIndex, int newIndex, object item)
{
var container = ContainerFromIndex(oldIndex);
@@ -92,16 +88,14 @@ namespace Avalonia.Controls.Generators
throw new IndexOutOfRangeException("Could not recycle container: not materialized.");
}
- var i = selector != null ? selector.Select(item) : item;
-
- container.SetValue(ContentProperty, i);
+ container.SetValue(ContentProperty, item);
if (!(item is IControl))
{
- container.DataContext = i;
+ container.DataContext = item;
}
- var info = MoveContainer(oldIndex, newIndex, i);
+ var info = MoveContainer(oldIndex, newIndex, item);
RaiseRecycled(new ItemContainerEventArgs(info));
return true;
diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
index 304c86dbf7..c06a64443c 100644
--- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
+++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
@@ -92,7 +92,6 @@ namespace Avalonia.Controls.Generators
result.DataContext = item;
}
- NameScope.SetNameScope((Control)(object)result, new NameScope());
Index.Add(item, result);
return result;
@@ -118,16 +117,22 @@ namespace Avalonia.Controls.Generators
return base.RemoveRange(startingIndex, count);
}
- public override bool TryRecycle(int oldIndex, int newIndex, object item, IMemberSelector selector)
+ public override bool TryRecycle(int oldIndex, int newIndex, object item) => false;
+
+ class WrapperTreeDataTemplate : ITreeDataTemplate
{
- return false;
+ private readonly IDataTemplate _inner;
+ public WrapperTreeDataTemplate(IDataTemplate inner) => _inner = inner;
+ public IControl Build(object param) => _inner.Build(param);
+ public bool SupportsRecycling => _inner.SupportsRecycling;
+ public bool Match(object data) => _inner.Match(data);
+ public InstancedBinding ItemsSelector(object item) => null;
}
private ITreeDataTemplate GetTreeDataTemplate(object item, IDataTemplate primary)
{
var template = Owner.FindDataTemplate(item, primary) ?? FuncDataTemplate.Default;
- var treeTemplate = template as ITreeDataTemplate ??
- new FuncTreeDataTemplate(typeof(object), template.Build, x => null);
+ var treeTemplate = template as ITreeDataTemplate ?? new WrapperTreeDataTemplate(template);
return treeTemplate;
}
}
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/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs
index a292ff7d0a..902e55bde6 100644
--- a/src/Avalonia.Controls/ItemsControl.cs
+++ b/src/Avalonia.Controls/ItemsControl.cs
@@ -54,12 +54,6 @@ namespace Avalonia.Controls
public static readonly StyledProperty ItemTemplateProperty =
AvaloniaProperty.Register(nameof(ItemTemplate));
- ///
- /// Defines the property.
- ///
- public static readonly StyledProperty MemberSelectorProperty =
- AvaloniaProperty.Register(nameof(MemberSelector));
-
private IEnumerable _items = new AvaloniaList();
private int _itemCount;
private IItemContainerGenerator _itemContainerGenerator;
@@ -144,15 +138,6 @@ namespace Avalonia.Controls
set { SetValue(ItemTemplateProperty, value); }
}
- ///
- /// Selects a member from to use as the list item.
- ///
- public IMemberSelector MemberSelector
- {
- get { return GetValue(MemberSelectorProperty); }
- set { SetValue(MemberSelectorProperty, value); }
- }
-
///
/// Gets the items presenter control.
///
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/ListBox.cs b/src/Avalonia.Controls/ListBox.cs
index 3150b6be91..f26cd47bcb 100644
--- a/src/Avalonia.Controls/ListBox.cs
+++ b/src/Avalonia.Controls/ListBox.cs
@@ -68,7 +68,13 @@ namespace Avalonia.Controls
///
public new IList SelectedItems => base.SelectedItems;
- ///
+ ///
+ /// Gets or sets the selection mode.
+ ///
+ ///
+ /// Note that the selection mode only applies to selections made via user interaction.
+ /// Multiple selections can be made programatically regardless of the value of this property.
+ ///
public new SelectionMode SelectionMode
{
get { return base.SelectionMode; }
diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs
index b0fb3f2b3b..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
{
@@ -40,37 +41,41 @@ namespace Avalonia.Controls
///
public override void Close()
{
- if (IsOpen)
+ if (!IsOpen)
{
- foreach (var i in ((IMenu)this).SubItems)
- {
- i.Close();
- }
-
- IsOpen = false;
- SelectedIndex = -1;
+ return;
+ }
- RaiseEvent(new RoutedEventArgs
- {
- RoutedEvent = MenuClosedEvent,
- Source = this,
- });
+ foreach (var i in ((IMenu)this).SubItems)
+ {
+ i.Close();
}
+
+ IsOpen = false;
+ SelectedIndex = -1;
+
+ RaiseEvent(new RoutedEventArgs
+ {
+ RoutedEvent = MenuClosedEvent,
+ Source = this,
+ });
}
///
public override void Open()
{
- if (!IsOpen)
+ if (IsOpen)
{
- IsOpen = true;
-
- RaiseEvent(new RoutedEventArgs
- {
- RoutedEvent = MenuOpenedEvent,
- Source = this,
- });
+ return;
}
+
+ IsOpen = true;
+
+ RaiseEvent(new RoutedEventArgs
+ {
+ RoutedEvent = MenuOpenedEvent,
+ Source = this,
+ });
}
///
diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs
index d6eb40360b..8eed58bb4d 100644
--- a/src/Avalonia.Controls/MenuBase.cs
+++ b/src/Avalonia.Controls/MenuBase.cs
@@ -7,7 +7,6 @@ using System.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
-using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
@@ -31,13 +30,13 @@ namespace Avalonia.Controls
/// Defines the event.
///
public static readonly RoutedEvent MenuOpenedEvent =
- RoutedEvent.Register
protected void SelectAll()
{
- if ((SelectionMode & (SelectionMode.Multiple | SelectionMode.Toggle)) == 0)
- {
- throw new NotSupportedException("Multiple selection is not enabled on this control.");
- }
-
UpdateSelectedItems(() =>
{
_selection.Clear();
@@ -523,7 +539,14 @@ namespace Avalonia.Controls.Primitives
var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0);
var range = multi && rangeModifier;
- if (range)
+ if (rightButton)
+ {
+ if (!_selection.Contains(index))
+ {
+ UpdateSelectedItem(index);
+ }
+ }
+ else if (range)
{
UpdateSelectedItems(() =>
{
@@ -582,7 +605,7 @@ namespace Avalonia.Controls.Primitives
}
else
{
- UpdateSelectedItem(index, !(rightButton && _selection.Contains(index)));
+ UpdateSelectedItem(index);
}
if (Presenter?.Panel != null)
diff --git a/src/Avalonia.Controls/Primitives/TabStrip.cs b/src/Avalonia.Controls/Primitives/TabStrip.cs
index 0e15ae4d7b..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
{
@@ -12,11 +13,8 @@ namespace Avalonia.Controls.Primitives
private static readonly FuncTemplate DefaultPanel =
new FuncTemplate(() => new WrapPanel { Orientation = Orientation.Horizontal });
- private static IMemberSelector s_MemberSelector = new FuncMemberSelector(SelectHeader);
-
static TabStrip()
{
- MemberSelectorProperty.OverrideDefaultValue(s_MemberSelector);
SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected);
FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false);
ItemsPanelProperty.OverrideDefaultValue(DefaultPanel);
@@ -51,11 +49,5 @@ namespace Avalonia.Controls.Primitives
e.Handled = UpdateSelectionFromEventSource(e.Source);
}
}
-
- private static object SelectHeader(object o)
- {
- var headered = o as IHeadered;
- return (headered != null) ? (headered.Header ?? string.Empty) : o;
- }
}
}
diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs
index 32e220b789..47c3240374 100644
--- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs
+++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs
@@ -257,13 +257,14 @@ namespace Avalonia.Controls.Primitives
{
Logger.Verbose(LogArea.Control, this, "Creating control template");
- var child = template.Build(this);
- var nameScope = new NameScope();
- NameScope.SetNameScope((Control)child, nameScope);
+ var (child, nameScope) = template.Build(this);
ApplyTemplatedParent(child);
- RegisterNames(child, nameScope);
((ISetLogicalParent)child).SetParent(this);
VisualChildren.Add(child);
+
+ // Existing code kinda expect to see a NameScope even if it's empty
+ if (nameScope == null)
+ nameScope = new NameScope();
OnTemplateApplied(new TemplateAppliedEventArgs(nameScope));
}
@@ -342,26 +343,5 @@ namespace Avalonia.Controls.Primitives
}
}
}
-
- ///
- /// Registers each control with its name scope.
- ///
- /// The control.
- /// The name scope.
- private void RegisterNames(IControl control, INameScope nameScope)
- {
- if (control.Name != null)
- {
- nameScope.Register(control.Name, control);
- }
-
- if (control.TemplatedParent == this)
- {
- foreach (IControl child in control.GetLogicalChildren())
- {
- RegisterNames(child, nameScope);
- }
- }
- }
}
}
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/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..44783e2c97
--- /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 InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateMeasure();
+
+ private void InvalidateMeasureForLayout(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..732ba8501c
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/ItemsSourceView.cs
@@ -0,0 +1,143 @@
+// 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);
+
+ _inner = source as IList;
+
+ if (_inner == null && source is IEnumerable objectEnumerable)
+ {
+ _inner = new List(objectEnumerable);
+ }
+ else
+ {
+ _inner = new List(source.Cast());
+ }
+
+ ListenToCollectionChanges();
+ }
+
+ ///
+ /// Gets the number of items in the collection.
+ ///
+ public int Count
+ {
+ get
+ {
+ if (_cachedSize == -1)
+ {
+ _cachedSize = _inner.Count;
+ }
+
+ return _cachedSize;
+ }
+ }
+
+ ///
+ /// Gets a value that indicates whether the items source can provide a unique key for each item.
+ ///
+ ///
+ /// TODO: Not yet implemented in Avalonia.
+ ///
+ public bool HasKeyIndexMapping => false;
+
+ ///
+ /// Occurs when the collection has changed to indicate the reason for the change and which items changed.
+ ///
+ public event NotifyCollectionChangedEventHandler CollectionChanged;
+
+ ///
+ public void Dispose()
+ {
+ if (_notifyCollectionChanged != null)
+ {
+ _notifyCollectionChanged.CollectionChanged -= OnCollectionChanged;
+ }
+ }
+
+ ///
+ /// Retrieves the item at the specified index.
+ ///
+ /// The index.
+ /// the item.
+ public object GetAt(int index) => _inner[index];
+
+ ///
+ /// Retrieves the index of the item that has the specified unique identifier (key).
+ ///
+ /// The index.
+ /// The key
+ ///
+ /// TODO: Not yet implemented in Avalonia.
+ ///
+ public string KeyFromIndex(int index)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Retrieves the unique identifier (key) for the item at the specified index.
+ ///
+ /// The key.
+ /// The index.
+ ///
+ /// TODO: Not yet implemented in Avalonia.
+ ///
+ public int IndexFromKey(string key)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args)
+ {
+ _cachedSize = _inner.Count;
+ CollectionChanged?.Invoke(this, args);
+ }
+
+ private void ListenToCollectionChanges()
+ {
+ if (_inner is INotifyCollectionChanged incc)
+ {
+ incc.CollectionChanged += OnCollectionChanged;
+ _notifyCollectionChanged = incc;
+ }
+ }
+
+ private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ OnItemsSourceChanged(e);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Repeater/RecyclePool.cs b/src/Avalonia.Controls/Repeater/RecyclePool.cs
new file mode 100644
index 0000000000..4e5950bdc5
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/RecyclePool.cs
@@ -0,0 +1,106 @@
+// 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.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Avalonia.Controls.Templates;
+
+namespace Avalonia.Controls
+{
+ internal class RecyclePool
+ {
+ public static readonly AttachedProperty OriginTemplateProperty =
+ AvaloniaProperty.RegisterAttached("OriginTemplate", typeof(RecyclePool));
+
+ private static ConditionalWeakTable s_pools = new ConditionalWeakTable();
+ private readonly Dictionary> _elements = new Dictionary>();
+
+ public static RecyclePool GetPoolInstance(IDataTemplate dataTemplate)
+ {
+ s_pools.TryGetValue(dataTemplate, out var result);
+ return result;
+ }
+
+ public static void SetPoolInstance(IDataTemplate dataTemplate, RecyclePool value) => s_pools.Add(dataTemplate, value);
+
+ public void PutElement(IControl element, string key, IControl owner)
+ {
+ var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner);
+ var elementInfo = new ElementInfo(element, ownerAsPanel);
+
+ if (!_elements.TryGetValue(key, out var pool))
+ {
+ pool = new List();
+ _elements.Add(key, pool);
+ }
+
+ pool.Add(elementInfo);
+ }
+
+ public IControl TryGetElement(string key, IControl owner)
+ {
+ if (_elements.TryGetValue(key, out var elements))
+ {
+ if (elements.Count > 0)
+ {
+ // Prefer an element from the same owner or with no owner so that we don't incur
+ // the enter/leave cost during recycling.
+ // TODO: prioritize elements with the same owner to those without an owner.
+ var elementInfo = elements.FirstOrDefault(x => x.Owner == owner) ?? elements.LastOrDefault();
+ elements.Remove(elementInfo);
+
+ var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner);
+ if (elementInfo.Owner != null && elementInfo.Owner != ownerAsPanel)
+ {
+ // Element is still under its parent. remove it from its parent.
+ var panel = elementInfo.Owner;
+ if (panel != null)
+ {
+ int childIndex = panel.Children.IndexOf(elementInfo.Element);
+ if (childIndex == -1)
+ {
+ throw new KeyNotFoundException("ItemsRepeater's child not found in its Children collection.");
+ }
+
+ panel.Children.RemoveAt(childIndex);
+ }
+ }
+
+ return elementInfo.Element;
+ }
+ }
+
+ return null;
+ }
+
+ private IPanel EnsureOwnerIsPanelOrNull(IControl owner)
+ {
+ if (owner is IPanel panel)
+ {
+ return panel;
+ }
+ else if (owner != null)
+ {
+ throw new InvalidOperationException("Owner must be IPanel or null.");
+ }
+
+ return null;
+ }
+
+ private class ElementInfo
+ {
+ public ElementInfo(IControl element, IPanel owner)
+ {
+ Element = element;
+ Owner = owner;
+ }
+
+ public IControl Element { get; }
+ public IPanel Owner { get;}
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs b/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs
new file mode 100644
index 0000000000..977d9d794c
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs
@@ -0,0 +1,65 @@
+// 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.Generic;
+using System.Text;
+using Avalonia.Layout;
+
+namespace Avalonia.Controls
+{
+ internal class RepeaterLayoutContext : VirtualizingLayoutContext
+ {
+ private readonly ItemsRepeater _owner;
+
+ public RepeaterLayoutContext(ItemsRepeater owner)
+ {
+ _owner = owner;
+ }
+
+ protected override Point LayoutOriginCore
+ {
+ get => _owner.LayoutOrigin;
+ set => _owner.LayoutOrigin = value;
+ }
+
+ protected override object LayoutStateCore
+ {
+ get => _owner.LayoutState;
+ set => _owner.LayoutState = value;
+ }
+
+ protected override int RecommendedAnchorIndexCore
+ {
+ get
+ {
+ int anchorIndex = -1;
+ var anchor = _owner.SuggestedAnchor;
+ if (anchor != null)
+ {
+ anchorIndex = _owner.GetElementIndex(anchor);
+ }
+
+ return anchorIndex;
+ }
+ }
+
+ protected override int ItemCountCore() => _owner.ItemsSourceView?.Count ?? 0;
+
+ protected override ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options)
+ {
+ return _owner.GetElementImpl(
+ index,
+ (options & ElementRealizationOptions.ForceCreate) != 0,
+ (options & ElementRealizationOptions.SuppressAutoRecycle) != 0);
+ }
+
+ protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index);
+
+ protected override void RecycleElementCore(ILayoutable element) => _owner.ClearElementImpl((IControl)element);
+
+ protected override Rect RealizationRectCore() => _owner.RealizationWindow;
+ }
+}
diff --git a/src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs b/src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs
new file mode 100644
index 0000000000..775aa3f113
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/UniqueIdElementPool.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 System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Avalonia.Controls
+{
+ internal class UniqueIdElementPool : IEnumerable>
+ {
+ private readonly Dictionary _elementMap = new Dictionary();
+ private readonly ItemsRepeater _owner;
+
+ public UniqueIdElementPool(ItemsRepeater owner) => _owner = owner;
+
+ public void Add(IControl element)
+ {
+ var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
+ var key = virtInfo.UniqueId;
+
+ if (_elementMap.ContainsKey(key))
+ {
+ throw new InvalidOperationException($"The unique id provided ({key}) is not unique.");
+ }
+
+ _elementMap.Add(key, element);
+ }
+
+ public IControl Remove(int index)
+ {
+ // Check if there is already a element in the mapping and if so, use it.
+ string key = _owner.ItemsSourceView.KeyFromIndex(index);
+
+ if (_elementMap.TryGetValue(key, out var element))
+ {
+ _elementMap.Remove(key);
+ }
+
+ return element;
+ }
+
+ public void Clear()
+ {
+ _elementMap.Clear();
+ }
+
+ public IEnumerator> GetEnumerator() => _elementMap.GetEnumerator();
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
+}
diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs
new file mode 100644
index 0000000000..833e708e9e
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/ViewManager.cs
@@ -0,0 +1,682 @@
+// 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.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+using Avalonia.Controls.Templates;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Layout;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls
+{
+ internal sealed class ViewManager
+ {
+ private const int FirstRealizedElementIndexDefault = int.MaxValue;
+ private const int LastRealizedElementIndexDefault = int.MinValue;
+
+ private readonly ItemsRepeater _owner;
+ private readonly List _pinnedPool = new List();
+ private readonly UniqueIdElementPool _resetPool;
+ private IControl _lastFocusedElement;
+ private bool _isDataSourceStableResetPending;
+ private int _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault;
+ private int _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault;
+ private bool _eventsSubscribed;
+
+ public ViewManager(ItemsRepeater owner)
+ {
+ _owner = owner;
+ _resetPool = new UniqueIdElementPool(owner);
+ }
+
+ public IControl GetElement(int index, bool forceCreate, bool suppressAutoRecycle)
+ {
+ var element = forceCreate ? null : GetElementIfAlreadyHeldByLayout(index);
+ if (element == null)
+ {
+ // check if this is the anchor made through repeater in preparation
+ // for a bring into view.
+ var madeAnchor = _owner.MadeAnchor;
+ if (madeAnchor != null)
+ {
+ var anchorVirtInfo = ItemsRepeater.TryGetVirtualizationInfo(madeAnchor);
+ if (anchorVirtInfo.Index == index)
+ {
+ element = madeAnchor;
+ }
+ }
+ }
+ if (element == null) { element = GetElementFromUniqueIdResetPool(index); };
+ if (element == null) { element = GetElementFromPinnedElements(index); }
+ if (element == null) { element = GetElementFromElementFactory(index); }
+
+ var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element);
+ if (suppressAutoRecycle)
+ {
+ virtInfo.AutoRecycleCandidate = false;
+ }
+ else
+ {
+ virtInfo.AutoRecycleCandidate = true;
+ virtInfo.KeepAlive = true;
+ }
+
+ return element;
+ }
+
+ public void ClearElement(IControl element, bool isClearedDueToCollectionChange)
+ {
+ var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
+ var index = virtInfo.Index;
+ bool cleared =
+ ClearElementToUniqueIdResetPool(element, virtInfo) ||
+ ClearElementToPinnedPool(element, virtInfo, isClearedDueToCollectionChange);
+
+ if (!cleared)
+ {
+ ClearElementToElementFactory(element);
+ }
+
+ // Both First and Last indices need to be valid or default.
+ if (index == _firstRealizedElementIndexHeldByLayout && index == _lastRealizedElementIndexHeldByLayout)
+ {
+ // First and last were pointing to the same element and that is going away.
+ InvalidateRealizedIndicesHeldByLayout();
+ }
+ else if (index == _firstRealizedElementIndexHeldByLayout)
+ {
+ // The FirstElement is going away, shrink the range by one.
+ ++_firstRealizedElementIndexHeldByLayout;
+ }
+ else if (index == _lastRealizedElementIndexHeldByLayout)
+ {
+ // Last element is going away, shrink the range by one at the end.
+ --_lastRealizedElementIndexHeldByLayout;
+ }
+ else
+ {
+ // Index is either outside the range we are keeping track of or inside the range.
+ // In both these cases, we just keep the range we have. If this clear was due to
+ // a collection change, then in the CollectionChanged event, we will invalidate these guys.
+ }
+ }
+
+ public void ClearElementToElementFactory(IControl element)
+ {
+ var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
+ var clearedIndex = virtInfo.Index;
+ _owner.OnElementClearing(element);
+ _owner.ItemTemplateShim.RecycleElement(_owner, element);
+
+ virtInfo.MoveOwnershipToElementFactory();
+
+ if (_lastFocusedElement == element)
+ {
+ // Focused element is going away. Remove the tracked last focused element
+ // and pick a reasonable next focus if we can find one within the layout
+ // realized elements.
+ MoveFocusFromClearedIndex(clearedIndex);
+ }
+
+ }
+
+ private void MoveFocusFromClearedIndex(int clearedIndex)
+ {
+ IControl focusedChild = null;
+ var focusCandidate = FindFocusCandidate(clearedIndex, focusedChild);
+ if (focusCandidate != null)
+ {
+ focusCandidate.Focus();
+ _lastFocusedElement = focusedChild;
+
+ // Add pin to hold the focused element.
+ UpdatePin(focusedChild, true /* addPin */);
+ }
+ else
+ {
+ // We could not find a candiate.
+ _lastFocusedElement = null;
+ }
+ }
+
+ IControl FindFocusCandidate(int clearedIndex, IControl focusedChild)
+ {
+ // Walk through all the children and find elements with index before and after the cleared index.
+ // Note that during a delete the next element would now have the same index.
+ int previousIndex = int.MinValue;
+ int nextIndex = int.MaxValue;
+ IControl nextElement = null;
+ IControl previousElement = null;
+
+ foreach (var child in _owner.Children)
+ {
+ var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child);
+ if (virtInfo?.IsHeldByLayout == true)
+ {
+ int currentIndex = virtInfo.Index;
+ if (currentIndex < clearedIndex)
+ {
+ if (currentIndex > previousIndex)
+ {
+ previousIndex = currentIndex;
+ previousElement = child;
+ }
+ }
+ else if (currentIndex >= clearedIndex)
+ {
+ // Note that we use >= above because if we deleted the focused element,
+ // the next element would have the same index now.
+ if (currentIndex < nextIndex)
+ {
+ nextIndex = currentIndex;
+ nextElement = child;
+ }
+ }
+ }
+ }
+
+ // TODO: Find the next element if one exists, if not use the previous element.
+ // If the container itself is not focusable, find a descendent that is.
+
+ return nextElement;
+ }
+
+ public int GetElementIndex(VirtualizationInfo virtInfo)
+ {
+ if (virtInfo == null)
+ {
+ throw new ArgumentException("Element is not a child of this ItemsRepeater.");
+ }
+
+ return virtInfo.IsRealized || virtInfo.IsInUniqueIdResetPool ? virtInfo.Index : -1;
+ }
+
+ public void PrunePinnedElements()
+ {
+ EnsureEventSubscriptions();
+
+ // Go through pinned elements and make sure they still have
+ // a reason to be pinned.
+ for (var i = 0; i < _pinnedPool.Count; ++i)
+ {
+ var elementInfo = _pinnedPool[i];
+ var virtInfo = elementInfo.VirtualizationInfo;
+
+ if (!virtInfo.IsPinned)
+ {
+ _pinnedPool.RemoveAt(i);
+ --i;
+
+ // Pinning was the only thing keeping this element alive.
+ ClearElementToElementFactory(elementInfo.PinnedElement);
+ }
+ }
+ }
+
+ public void UpdatePin(IControl element, bool addPin)
+ {
+ var parent = element.VisualParent;
+ var child = (IVisual)element;
+
+ while (parent != null)
+ {
+ if (parent is ItemsRepeater repeater)
+ {
+ var virtInfo = ItemsRepeater.GetVirtualizationInfo((IControl)child);
+ if (virtInfo.IsRealized)
+ {
+ if (addPin)
+ {
+ virtInfo.AddPin();
+ }
+ else if (virtInfo.IsPinned)
+ {
+ if (virtInfo.RemovePin() == 0)
+ {
+ // ElementFactory is invoked during the measure pass.
+ // We will clear the element then.
+ repeater.InvalidateMeasure();
+ }
+ }
+ }
+ }
+
+ child = parent;
+ parent = child.VisualParent;
+ }
+ }
+
+ public void OnItemsSourceChanged(object sender, NotifyCollectionChangedEventArgs args)
+ {
+ // Note: For items that have been removed, the index will not be touched. It will hold
+ // the old index before it was removed. It is not valid anymore.
+ switch (args.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ {
+ var newIndex = args.NewStartingIndex;
+ var newCount = args.NewItems.Count;
+ EnsureFirstLastRealizedIndices();
+ if (newIndex <= _lastRealizedElementIndexHeldByLayout)
+ {
+ _lastRealizedElementIndexHeldByLayout += newCount;
+ foreach (var element in _owner.Children)
+ {
+ var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
+ var dataIndex = virtInfo.Index;
+
+ if (virtInfo.IsRealized && dataIndex >= newIndex)
+ {
+ UpdateElementIndex(element, virtInfo, dataIndex + newCount);
+ }
+ }
+ }
+ else
+ {
+ // Indices held by layout are not affected
+ // We could still have items in the pinned elements that need updates. This is usually a very small vector.
+ for (var i = 0; i < _pinnedPool.Count; ++i)
+ {
+ var elementInfo = _pinnedPool[i];
+ var virtInfo = elementInfo.VirtualizationInfo;
+ var dataIndex = virtInfo.Index;
+
+ if (virtInfo.IsRealized && dataIndex >= newIndex)
+ {
+ var element = elementInfo.PinnedElement;
+ UpdateElementIndex(element, virtInfo, dataIndex + newCount);
+ }
+ }
+ }
+ break;
+ }
+
+ case NotifyCollectionChangedAction.Replace:
+ {
+ // Requirement: oldStartIndex == newStartIndex. It is not a replace if this is not true.
+ // Two cases here
+ // case 1: oldCount == newCount
+ // indices are not affected. nothing to do here.
+ // case 2: oldCount != newCount
+ // Replaced with less or more items. This is like an insert or remove
+ // depending on the counts.
+ var oldStartIndex = args.OldStartingIndex;
+ var newStartingIndex = args.NewStartingIndex;
+ var oldCount = args.OldItems.Count;
+ var newCount = args.NewItems.Count;
+ if (oldStartIndex != newStartingIndex)
+ {
+ throw new NotSupportedException("Replace is only allowed with OldStartingIndex equals to NewStartingIndex.");
+ }
+
+ if (oldCount == 0)
+ {
+ throw new NotSupportedException("Replace notification with args.OldItemsCount value of 0 is not allowed. Use Insert action instead.");
+ }
+
+ if (newCount == 0)
+ {
+ throw new NotSupportedException("Replace notification with args.NewItemCount value of 0 is not allowed. Use Remove action instead.");
+ }
+
+ int countChange = newCount - oldCount;
+ if (countChange != 0)
+ {
+ // countChange > 0 : countChange items were added
+ // countChange < 0 : -countChange items were removed
+ foreach (var element in _owner.Children)
+ {
+ var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
+ var dataIndex = virtInfo.Index;
+
+ if (virtInfo.IsRealized)
+ {
+ if (dataIndex >= oldStartIndex + oldCount)
+ {
+ UpdateElementIndex(element, virtInfo, dataIndex + countChange);
+ }
+ }
+ }
+
+ EnsureFirstLastRealizedIndices();
+ _lastRealizedElementIndexHeldByLayout += countChange;
+ }
+ break;
+ }
+
+ case NotifyCollectionChangedAction.Remove:
+ {
+ var oldStartIndex = args.OldStartingIndex;
+ var oldCount = args.OldItems.Count;
+ foreach (var element in _owner.Children)
+ {
+ var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
+ var dataIndex = virtInfo.Index;
+
+ if (virtInfo.IsRealized)
+ {
+ if (virtInfo.AutoRecycleCandidate && oldStartIndex <= dataIndex && dataIndex < oldStartIndex + oldCount)
+ {
+ // If we are doing the mapping, remove the element who's data was removed.
+ _owner.ClearElementImpl(element);
+ }
+ else if (dataIndex >= (oldStartIndex + oldCount))
+ {
+ UpdateElementIndex(element, virtInfo, dataIndex - oldCount);
+ }
+ }
+ }
+
+ InvalidateRealizedIndicesHeldByLayout();
+ break;
+ }
+
+ case NotifyCollectionChangedAction.Reset:
+ if (_owner.ItemsSourceView.HasKeyIndexMapping)
+ {
+ _isDataSourceStableResetPending = true;
+ }
+
+ // Walk through all the elements and make sure they are cleared, they will go into
+ // the stable id reset pool.
+ foreach (var element in _owner.Children)
+ {
+ var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
+ if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate)
+ {
+ _owner.ClearElementImpl(element);
+ }
+ }
+
+ InvalidateRealizedIndicesHeldByLayout();
+ break;
+ }
+ }
+
+ private void EnsureFirstLastRealizedIndices()
+ {
+ if (_firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault)
+ {
+ // This will ensure that the indexes are updated.
+ GetElementIfAlreadyHeldByLayout(0);
+ }
+ }
+
+ public void OnLayoutChanging()
+ {
+ if (_owner.ItemsSourceView?.HasKeyIndexMapping == true)
+ {
+ _isDataSourceStableResetPending = true;
+ }
+ }
+
+ public void OnOwnerArranged()
+ {
+ if (_isDataSourceStableResetPending)
+ {
+ _isDataSourceStableResetPending = false;
+
+ foreach (var entry in _resetPool)
+ {
+ // TODO: Task 14204306: ItemsRepeater: Find better focus candidate when focused element is deleted in the ItemsSource.
+ // Focused element is getting cleared. Need to figure out semantics on where
+ // focus should go when the focused element is removed from the data collection.
+ ClearElement(entry.Value, true /* isClearedDueToCollectionChange */);
+ }
+
+ _resetPool.Clear();
+ }
+ }
+
+ // We optimize for the case where index is not realized to return null as quickly as we can.
+ // Flow layouts manage containers on their own and will never ask for an index that is already realized.
+ // If an index that is realized is requested by the layout, we unfortunately have to walk the
+ // children. Not ideal, but a reasonable default to provide consistent behavior between virtualizing
+ // and non-virtualizing hosts.
+ private IControl GetElementIfAlreadyHeldByLayout(int index)
+ {
+ IControl element = null;
+
+ bool cachedFirstLastIndicesInvalid = _firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault;
+ bool isRequestedIndexInRealizedRange = (_firstRealizedElementIndexHeldByLayout <= index && index <= _lastRealizedElementIndexHeldByLayout);
+
+ if (cachedFirstLastIndicesInvalid || isRequestedIndexInRealizedRange)
+ {
+ foreach (var child in _owner.Children)
+ {
+ var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child);
+ if (virtInfo?.IsHeldByLayout == true)
+ {
+ // Only give back elements held by layout. If someone else is holding it, they will be served by other methods.
+ int childIndex = virtInfo.Index;
+ _firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, childIndex);
+ _lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, childIndex);
+ if (virtInfo.Index == index)
+ {
+ element = child;
+ // If we have valid first/last indices, we don't have to walk the rest, but if we
+ // do not, then we keep walking through the entire children collection to get accurate
+ // indices once.
+ if (!cachedFirstLastIndicesInvalid)
+ {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ return element;
+ }
+
+ private IControl GetElementFromUniqueIdResetPool(int index)
+ {
+ IControl element = null;
+ // See if you can get it from the reset pool.
+ if (_isDataSourceStableResetPending)
+ {
+ element = _resetPool.Remove(index);
+ if (element != null)
+ {
+ // Make sure that the index is updated to the current one
+ var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
+ virtInfo.MoveOwnershipToLayoutFromUniqueIdResetPool();
+ UpdateElementIndex(element, virtInfo, index);
+ }
+ }
+
+ return element;
+ }
+
+ private IControl GetElementFromPinnedElements(int index)
+ {
+ IControl element = null;
+
+ // See if you can find something among the pinned elements.
+ for (var i = 0; i < _pinnedPool.Count; ++i)
+ {
+ var elementInfo = _pinnedPool[i];
+ var virtInfo = elementInfo.VirtualizationInfo;
+
+ if (virtInfo.Index == index)
+ {
+ _pinnedPool.RemoveAt(i);
+ element = elementInfo.PinnedElement;
+ elementInfo.VirtualizationInfo.MoveOwnershipToLayoutFromPinnedPool();
+ break;
+ }
+ }
+
+ return element;
+ }
+
+ private IControl GetElementFromElementFactory(int index)
+ {
+ // The view generator is the provider of last resort.
+
+ var itemTemplateFactory = _owner.ItemTemplateShim;
+ if (itemTemplateFactory == null)
+ {
+ // If no ItemTemplate was provided, use a default
+ var factory = FuncDataTemplate.Default;
+ _owner.ItemTemplate = factory;
+ itemTemplateFactory = _owner.ItemTemplateShim;
+ }
+
+ var data = _owner.ItemsSourceView.GetAt(index);
+ var element = itemTemplateFactory.GetElement(_owner, data);
+
+ var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element);
+ if (virtInfo == null)
+ {
+ virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element);
+ }
+
+ // Prepare the element
+ element.DataContext = data;
+
+ virtInfo.MoveOwnershipToLayoutFromElementFactory(
+ index,
+ /* uniqueId: */
+ _owner.ItemsSourceView.HasKeyIndexMapping ?
+ _owner.ItemsSourceView.KeyFromIndex(index) :
+ string.Empty);
+
+ // The view generator is the only provider that prepares the element.
+ var repeater = _owner;
+
+ // Add the element to the children collection here before raising OnElementPrepared so
+ // that handlers can walk up the tree in case they want to find their IndexPath in the
+ // nested case.
+ var children = repeater.Children;
+ if (element.VisualParent != repeater)
+ {
+ children.Add(element);
+ }
+
+ repeater.OnElementPrepared(element, index);
+
+ // Update realized indices
+ _firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);
+ _lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index);
+
+ return element;
+ }
+
+ private bool ClearElementToUniqueIdResetPool(IControl element, VirtualizationInfo virtInfo)
+ {
+ if (_isDataSourceStableResetPending)
+ {
+ _resetPool.Add(element);
+ virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout();
+ }
+
+ return _isDataSourceStableResetPending;
+ }
+
+ private bool ClearElementToPinnedPool(IControl element, VirtualizationInfo virtInfo, bool isClearedDueToCollectionChange)
+ {
+ if (_isDataSourceStableResetPending)
+ {
+ _resetPool.Add(element);
+ virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout();
+ }
+
+ return _isDataSourceStableResetPending;
+ }
+
+ private void UpdateFocusedElement()
+ {
+ IControl focusedElement = null;
+
+ var child = FocusManager.Instance.Current;
+
+ if (child != null)
+ {
+ var parent = child.VisualParent;
+ var owner = _owner;
+
+ // Find out if the focused element belongs to one of our direct
+ // children.
+ while (parent != null)
+ {
+ if (parent is ItemsRepeater repeater)
+ {
+ var element = child as IControl;
+ if (repeater == owner && ItemsRepeater.GetVirtualizationInfo(element).IsRealized)
+ {
+ focusedElement = element;
+ }
+
+ break;
+ }
+
+ child = parent as IInputElement;
+ parent = child.VisualParent;
+ }
+ }
+
+ // If the focused element has changed,
+ // we need to unpin the old one and pin the new one.
+ if (_lastFocusedElement != focusedElement)
+ {
+ if (_lastFocusedElement != null)
+ {
+ UpdatePin(_lastFocusedElement, false /* addPin */);
+ }
+
+ if (focusedElement != null)
+ {
+ UpdatePin(focusedElement, true /* addPin */);
+ }
+
+ _lastFocusedElement = focusedElement;
+ }
+ }
+
+ private void OnFocusChanged(object sender, RoutedEventArgs e) => UpdateFocusedElement();
+
+ private void EnsureEventSubscriptions()
+ {
+ if (!_eventsSubscribed)
+ {
+ _owner.GotFocus += OnFocusChanged;
+ _owner.LostFocus += OnFocusChanged;
+ }
+ }
+
+ private void UpdateElementIndex(IControl element, VirtualizationInfo virtInfo, int index)
+ {
+ var oldIndex = virtInfo.Index;
+ if (oldIndex != index)
+ {
+ virtInfo.UpdateIndex(index);
+ _owner.OnElementIndexChanged(element, oldIndex, index);
+ }
+ }
+
+ private void InvalidateRealizedIndicesHeldByLayout()
+ {
+ _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault;
+ _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault;
+ }
+
+ private struct PinnedElementInfo
+ {
+ public PinnedElementInfo(IControl element)
+ {
+ PinnedElement = element;
+ VirtualizationInfo = ItemsRepeater.GetVirtualizationInfo(element);
+ }
+
+ public IControl PinnedElement { get; }
+ public VirtualizationInfo VirtualizationInfo { get; }
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs
new file mode 100644
index 0000000000..10c11889d0
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs
@@ -0,0 +1,501 @@
+// 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.Generic;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using Avalonia.Layout;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls
+{
+ internal class ViewportManager
+ {
+ private const double CacheBufferPerSideInflationPixelDelta = 40.0;
+ private readonly ItemsRepeater _owner;
+ private bool _ensuredScroller;
+ private IScrollAnchorProvider _scroller;
+ private IControl _makeAnchorElement;
+ private bool _isAnchorOutsideRealizedRange;
+ private Task _cacheBuildAction;
+ private Rect _visibleWindow;
+ private Rect _layoutExtent;
+ // This is the expected shift by the layout.
+ private Point _expectedViewportShift;
+ // This is what is pending and not been accounted for.
+ // Sometimes the scrolling surface cannot service a shift (for example
+ // it is already at the top and cannot shift anymore.)
+ private Point _pendingViewportShift;
+ // Unshiftable shift amount that this view manager can
+ // handle on its own to fake it to the layout as if the shift
+ // actually happened. This can happen in cases where no scrollviewer
+ // in the parent chain can scroll in the shift direction.
+ private Point _unshiftableShift;
+ private double _maximumHorizontalCacheLength = 0.0;
+ private double _maximumVerticalCacheLength = 0.0;
+ private double _horizontalCacheBufferPerSide;
+ private double _verticalCacheBufferPerSide;
+ private bool _isBringIntoViewInProgress;
+ // For non-virtualizing layouts, we do not need to keep
+ // updating viewports and invalidating measure often. So when
+ // a non virtualizing layout is used, we stop doing all that work.
+ bool _managingViewportDisabled;
+ private IDisposable _effectiveViewportChangedRevoker;
+ private bool _layoutUpdatedSubscribed;
+
+ public ViewportManager(ItemsRepeater owner)
+ {
+ _owner = owner;
+ }
+
+ public IControl SuggestedAnchor
+ {
+ get
+ {
+ // The element generated during the ItemsRepeater.MakeAnchor call has precedence over the next tick.
+ var suggestedAnchor = _makeAnchorElement;
+ var owner = _owner;
+
+ if (suggestedAnchor == null)
+ {
+ var anchorElement = _scroller?.CurrentAnchor;
+
+ if (anchorElement != null)
+ {
+ // We can't simply return anchorElement because, in case of nested Repeaters, it may not
+ // be a direct child of ours, or even an indirect child. We need to walk up the tree starting
+ // from anchorElement to figure out what child of ours (if any) to use as the suggested element.
+ var child = anchorElement;
+ var parent = child.VisualParent as IControl;
+
+ while (parent != null)
+ {
+ if (parent == owner)
+ {
+ suggestedAnchor = child;
+ break;
+ }
+
+ child = parent;
+ parent = parent.VisualParent as IControl;
+ }
+ }
+ }
+
+ return suggestedAnchor;
+ }
+ }
+
+ public bool HasScroller => _scroller != null;
+
+ public IControl MadeAnchor => _makeAnchorElement;
+
+ public double HorizontalCacheLength
+ {
+ get => _maximumHorizontalCacheLength;
+ set
+ {
+ if (_maximumHorizontalCacheLength != value)
+ {
+ ValidateCacheLength(value);
+ _maximumHorizontalCacheLength = value;
+ }
+ }
+ }
+
+ public double VerticalCacheLength
+ {
+ get => _maximumVerticalCacheLength;
+ set
+ {
+ if (_maximumVerticalCacheLength != value)
+ {
+ ValidateCacheLength(value);
+ _maximumVerticalCacheLength = value;
+ }
+ }
+ }
+
+ private Rect GetLayoutVisibleWindowDiscardAnchor()
+ {
+ var visibleWindow = _visibleWindow;
+
+ if (HasScroller)
+ {
+ visibleWindow = new Rect(
+ visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X,
+ visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y,
+ visibleWindow.Width,
+ visibleWindow.Height);
+ }
+
+ return visibleWindow;
+ }
+
+ public Rect GetLayoutVisibleWindow()
+ {
+ var visibleWindow = _visibleWindow;
+
+ if (_makeAnchorElement != null)
+ {
+ // The anchor is not necessarily laid out yet. Its position should default
+ // to zero and the layout origin is expected to change once layout is done.
+ // Until then, we need a window that's going to protect the anchor from
+ // getting recycled.
+ visibleWindow = visibleWindow.WithX(0).WithY(0);
+ }
+ else if (HasScroller)
+ {
+ visibleWindow = new Rect(
+ visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X,
+ visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y,
+ visibleWindow.Width,
+ visibleWindow.Height);
+ }
+
+ return visibleWindow;
+ }
+
+ public Rect GetLayoutRealizationWindow()
+ {
+ var realizationWindow = GetLayoutVisibleWindow();
+ if (HasScroller)
+ {
+ realizationWindow = new Rect(
+ realizationWindow.X - _horizontalCacheBufferPerSide,
+ realizationWindow.Y - _verticalCacheBufferPerSide,
+ realizationWindow.Width + _horizontalCacheBufferPerSide * 2.0,
+ realizationWindow.Height + _verticalCacheBufferPerSide * 2.0);
+ }
+
+ return realizationWindow;
+ }
+
+ public void SetLayoutExtent(Rect extent)
+ {
+ _expectedViewportShift = new Point(
+ _expectedViewportShift.X + _layoutExtent.X - extent.X,
+ _expectedViewportShift.Y + _layoutExtent.Y - extent.Y);
+
+ // We tolerate viewport imprecisions up to 1 pixel to avoid invaliding layout too much.
+ if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1)
+ {
+ // There are cases where we might be expecting a shift but not get it. We will
+ // be waiting for the effective viewport event but if the scroll viewer is not able
+ // to perform the shift (perhaps because it cannot scroll in negative offset),
+ // then we will end up not realizing elements in the visible
+ // window. To avoid this, we register to layout updated for this layout pass. If we
+ // get an effective viewport, we know we have a new viewport and we unregister from
+ // layout updated. If we get the layout updated handler, then we know that the
+ // scroller was unable to perform the shift and we invalidate measure and unregister
+ // from the layout updated event.
+ if (!_layoutUpdatedSubscribed)
+ {
+ _owner.LayoutUpdated += OnLayoutUpdated;
+ _layoutUpdatedSubscribed = true;
+ }
+ }
+
+ _layoutExtent = extent;
+ _pendingViewportShift = _expectedViewportShift;
+
+ // We just finished a measure pass and have a new extent.
+ // Let's make sure the scrollers will run its arrange so that they track the anchor.
+ ((IControl)_scroller)?.InvalidateArrange();
+ }
+
+ public Point GetOrigin() => _layoutExtent.TopLeft;
+
+ public void OnLayoutChanged(bool isVirtualizing)
+ {
+ _managingViewportDisabled = !isVirtualizing;
+
+ _layoutExtent = default;
+ _expectedViewportShift = default;
+ _pendingViewportShift = default;
+ _unshiftableShift = default;
+
+ _effectiveViewportChangedRevoker?.Dispose();
+
+ if (!_managingViewportDisabled)
+ {
+ _effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner);
+ }
+ }
+
+ public void OnElementPrepared(IControl element)
+ {
+ // If we have an anchor element, we do not want the
+ // scroll anchor provider to start anchoring some other element.
+ ////element.CanBeScrollAnchor(true);
+ }
+
+ public void OnElementCleared(ILayoutable element)
+ {
+ ////element.CanBeScrollAnchor(false);
+ }
+
+ public void OnOwnerMeasuring()
+ {
+ // This is because of a bug that causes effective viewport to not
+ // fire if you register during arrange.
+ // Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport
+ EnsureScroller();
+ }
+
+ public void OnOwnerArranged()
+ {
+ _expectedViewportShift = default;
+
+ if (!_managingViewportDisabled)
+ {
+ // This is because of a bug that causes effective viewport to not
+ // fire if you register during arrange.
+ // Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport
+ // EnsureScroller();
+
+ if (HasScroller)
+ {
+ double maximumHorizontalCacheBufferPerSide = _maximumHorizontalCacheLength * _visibleWindow.Width / 2.0;
+ double maximumVerticalCacheBufferPerSide = _maximumVerticalCacheLength * _visibleWindow.Height / 2.0;
+
+ bool continueBuildingCache =
+ _horizontalCacheBufferPerSide < maximumHorizontalCacheBufferPerSide ||
+ _verticalCacheBufferPerSide < maximumVerticalCacheBufferPerSide;
+
+ if (continueBuildingCache)
+ {
+ _horizontalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta;
+ _verticalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta;
+
+ _horizontalCacheBufferPerSide = Math.Min(_horizontalCacheBufferPerSide, maximumHorizontalCacheBufferPerSide);
+ _verticalCacheBufferPerSide = Math.Min(_verticalCacheBufferPerSide, maximumVerticalCacheBufferPerSide);
+ }
+ }
+ }
+ }
+
+ private void OnLayoutUpdated(object sender, EventArgs args)
+ {
+ _owner.LayoutUpdated -= OnLayoutUpdated;
+ if (_managingViewportDisabled)
+ {
+ return;
+ }
+
+ // We were expecting a viewport shift but we never got one and we are not going to in this
+ // layout pass. We likely will never get this shift, so lets assume that we are never going to get it and
+ // adjust our expected shift to track that. One case where this can happen is when there is no scrollviewer
+ // that can scroll in the direction where the shift is expected.
+ if (_pendingViewportShift.X != 0 || _pendingViewportShift.Y != 0)
+ {
+ // Assume this is never going to come.
+ _unshiftableShift = new Point(
+ _unshiftableShift.X + _pendingViewportShift.X,
+ _unshiftableShift.Y + _pendingViewportShift.Y);
+ _pendingViewportShift = default;
+ _expectedViewportShift = default;
+
+ TryInvalidateMeasure();
+ }
+ }
+
+ public void OnMakeAnchor(IControl anchor, bool isAnchorOutsideRealizedRange)
+ {
+ _makeAnchorElement = anchor;
+ _isAnchorOutsideRealizedRange = isAnchorOutsideRealizedRange;
+ }
+
+ public void OnBringIntoViewRequested(RequestBringIntoViewEventArgs args)
+ {
+ if (!_managingViewportDisabled)
+ {
+ // During the time between a bring into view request and the element coming into view we do not
+ // want the anchor provider to pick some anchor and jump to it. Instead we want to anchor on the
+ // element that is being brought into view. We can do this by making just that element as a potential
+ // anchor candidate and ensure no other element of this repeater is an anchor candidate.
+ // Once the layout pass is done and we render the frame, the element will be in frame and we can
+ // switch back to letting the anchor provider pick a suitable anchor.
+
+ // get the targetChild - i.e the immediate child of this repeater that is being brought into view.
+ // Note that the element being brought into view could be a descendant.
+ var targetChild = GetImmediateChildOfRepeater((IControl)args.TargetObject);
+
+ // Make sure that only the target child can be the anchor during the bring into view operation.
+ foreach (var child in _owner.Children)
+ {
+ ////if (child.CanBeScrollAnchor && child != targetChild)
+ ////{
+ //// child.CanBeScrollAnchor = false;
+ ////}
+ }
+
+ // Register to rendering event to go back to how things were before where any child can be the anchor.
+ _isBringIntoViewInProgress = true;
+ ////if (!m_renderingToken)
+ ////{
+ //// winrt::Windows::UI::Xaml::Media::CompositionTarget compositionTarget{ nullptr };
+ //// m_renderingToken = compositionTarget.Rendering(winrt::auto_revoke, { this, &ViewportManagerWithPlatformFeatures::OnCompositionTargetRendering });
+ ////}
+ }
+ }
+
+ private IControl GetImmediateChildOfRepeater(IControl descendant)
+ {
+ var targetChild = descendant;
+ var parent = descendant.Parent;
+ while (parent != null && parent != _owner)
+ {
+ targetChild = parent;
+ parent = (IControl)parent.VisualParent;
+ }
+
+ if (parent == null)
+ {
+ throw new InvalidOperationException("OnBringIntoViewRequested called with args.target element not under the ItemsRepeater that recieved the call");
+ }
+
+ return targetChild;
+ }
+
+ public void ResetScrollers()
+ {
+ _scroller = null;
+ _effectiveViewportChangedRevoker?.Dispose();
+ _effectiveViewportChangedRevoker = null;
+ _ensuredScroller = false;
+ }
+
+ private void OnEffectiveViewportChanged(TransformedBounds? bounds)
+ {
+ if (!bounds.HasValue)
+ {
+ return;
+ }
+
+ var globalClip = bounds.Value.Clip;
+ var transform = _owner.GetVisualRoot().TransformToVisual(_owner).Value;
+ var clip = globalClip.TransformToAABB(transform);
+ var effectiveViewport = clip.Intersect(bounds.Value.Bounds);
+
+ UpdateViewport(effectiveViewport);
+
+ _pendingViewportShift = default;
+ _unshiftableShift = default;
+ if (_visibleWindow.IsEmpty)
+ {
+ // We got cleared.
+ _layoutExtent = default;
+ }
+
+ // We got a new viewport, we dont need to wait for layout updated anymore to
+ // see if our request for a pending shift was handled.
+ if (_layoutUpdatedSubscribed)
+ {
+ _owner.LayoutUpdated -= OnLayoutUpdated;
+ }
+ }
+
+ private void EnsureScroller()
+ {
+ if (!_ensuredScroller)
+ {
+ ResetScrollers();
+
+ var parent = _owner.GetVisualParent();
+ while (parent != null)
+ {
+ if (parent is IScrollAnchorProvider scroller)
+ {
+ _scroller = scroller;
+ break;
+ }
+
+ parent = parent.VisualParent;
+ }
+
+ if (_scroller == null)
+ {
+ // We usually update the viewport in the post arrange handler. But, since we don't have
+ // a scroller, let's do it now.
+ UpdateViewport(Rect.Empty);
+ }
+ else if (!_managingViewportDisabled)
+ {
+ _effectiveViewportChangedRevoker?.Dispose();
+ _effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner);
+ }
+
+ _ensuredScroller = true;
+ }
+ }
+
+ private void UpdateViewport(Rect viewport)
+ {
+ var currentVisibleWindow = viewport;
+
+ if (-currentVisibleWindow.X <= ItemsRepeater.ClearedElementsArrangePosition.X &&
+ -currentVisibleWindow.Y <= ItemsRepeater.ClearedElementsArrangePosition.Y)
+ {
+ // We got cleared.
+ _visibleWindow = default;
+ }
+ else
+ {
+ _visibleWindow = currentVisibleWindow;
+ }
+
+ TryInvalidateMeasure();
+ }
+
+ private static void ValidateCacheLength(double cacheLength)
+ {
+ if (cacheLength < 0.0 || double.IsInfinity(cacheLength) || double.IsNaN(cacheLength))
+ {
+ throw new ArgumentException("The maximum cache length must be equal or superior to zero.");
+ }
+ }
+
+ private void TryInvalidateMeasure()
+ {
+ // Don't invalidate measure if we have an invalid window.
+ if (!_visibleWindow.IsEmpty)
+ {
+ // We invalidate measure instead of just invalidating arrange because
+ // we don't invalidate measure in UpdateViewport if the view is changing to
+ // avoid layout cycles.
+ _owner.InvalidateMeasure();
+ }
+ }
+
+ private IDisposable SubscribeToEffectiveViewportChanged(IControl control)
+ {
+ // HACK: This is a bit of a hack. We need the effective viewport of the ItemsRepeater -
+ // we can get this from TransformedBounds, but this property is updated after layout has
+ // run, resulting in the UI being updated too late when scrolling quickly. We can
+ // partially remedey this by triggering also on Bounds changes, but this won't work so
+ // well for nested ItemsRepeaters.
+ //
+ // UWP uses the EffectiveBoundsChanged event (which I think was implemented specially
+ // for this case): we need to implement that in Avalonia.
+ return control.GetObservable(Visual.TransformedBoundsProperty)
+ .Merge(control.GetObservable(Visual.BoundsProperty).Select(_ => control.TransformedBounds))
+ .Skip(1)
+ .Subscribe(OnEffectiveViewportChanged);
+ }
+
+ private class ScrollerInfo
+ {
+ public ScrollerInfo(ScrollViewer scroller)
+ {
+ Scroller = scroller;
+ }
+
+ public ScrollViewer Scroller { get; }
+ }
+ };
+}
diff --git a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs
new file mode 100644
index 0000000000..eb30c1b7cf
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs
@@ -0,0 +1,118 @@
+// 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
+{
+ internal enum ElementOwner
+ {
+ // All elements are originally owned by the view generator.
+ ElementFactory,
+ // Ownership is transferred to the layout when it calls GetElement.
+ Layout,
+ // Ownership is transferred to the pinned pool if the element is cleared (outside of
+ // a 'remove' collection change of course).
+ PinnedPool,
+ // Ownership is transfered to the reset pool if the element is cleared by a reset and
+ // the data source supports unique ids.
+ UniqueIdResetPool,
+ // Ownership is transfered to the animator if the element is cleared due to a
+ // 'remove'-like collection change.
+ Animator
+ }
+
+ internal class VirtualizationInfo
+ {
+ private int _pinCounter;
+ private object _data;
+
+ public Rect ArrangeBounds { get; set; }
+ public bool AutoRecycleCandidate { get; set; }
+ public int Index { get; private set; }
+ public bool IsPinned => _pinCounter > 0;
+ public bool IsHeldByLayout => Owner == ElementOwner.Layout;
+ public bool IsRealized => IsHeldByLayout || Owner == ElementOwner.PinnedPool;
+ public bool IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool;
+ public bool KeepAlive { get; set; }
+ public ElementOwner Owner { get; private set; } = ElementOwner.ElementFactory;
+ public string UniqueId { get; private set; }
+
+ public void MoveOwnershipToLayoutFromElementFactory(int index, string uniqueId)
+ {
+ Owner = ElementOwner.Layout;
+ Index = index;
+ UniqueId = uniqueId;
+ }
+
+ public void MoveOwnershipToLayoutFromUniqueIdResetPool()
+ {
+ Owner = ElementOwner.Layout;
+ }
+
+ public void MoveOwnershipToLayoutFromPinnedPool()
+ {
+ Owner = ElementOwner.Layout;
+ }
+
+ public void MoveOwnershipToElementFactory()
+ {
+ Owner = ElementOwner.ElementFactory;
+ _pinCounter = 0;
+ Index = -1;
+ UniqueId = string.Empty;
+ ArrangeBounds = ItemsRepeater.InvalidRect;
+ }
+
+ public void MoveOwnershipToUniqueIdResetPoolFromLayout()
+ {
+ Owner = ElementOwner.UniqueIdResetPool;
+ // Keep the pinCounter the same. If the container survives the reset
+ // it can go on being pinned as if nothing happened.
+ }
+
+ public void MoveOwnershipToAnimator()
+ {
+ // During a unique id reset, some elements might get removed.
+ // Their ownership will go from the UniqueIdResetPool to the Animator.
+ // The common path though is for ownership to go from Layout to Animator.
+ Owner = ElementOwner.Animator;
+ Index = -1;
+ _pinCounter = 0;
+ }
+
+ public void MoveOwnershipToPinnedPool()
+ {
+ Owner = ElementOwner.PinnedPool;
+ }
+
+ public int AddPin()
+ {
+ if (!IsRealized)
+ {
+ throw new InvalidOperationException("You can't pin an unrealized element.");
+ }
+
+ return ++_pinCounter;
+ }
+
+ public int RemovePin()
+ {
+ if (!IsRealized)
+ {
+ throw new InvalidOperationException("You can't unpin an unrealized element.");
+ }
+
+ if (!IsPinned)
+ {
+ throw new InvalidOperationException("UnpinElement was called more often than PinElement.");
+ }
+
+ return --_pinCounter;
+ }
+
+ public void UpdateIndex(int newIndex) => Index = newIndex;
+ }
+}
diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs
index 264b1fd2ce..c9b5cbb75b 100644
--- a/src/Avalonia.Controls/ScrollViewer.cs
+++ b/src/Avalonia.Controls/ScrollViewer.cs
@@ -11,7 +11,7 @@ namespace Avalonia.Controls
///
/// A control scrolls its content if the content is bigger than the space available.
///
- public class ScrollViewer : ContentControl, IScrollable
+ public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider
{
///
/// Defines the property.
@@ -333,6 +333,9 @@ namespace Avalonia.Controls
get { return _viewport.Height; }
}
+ ///
+ IControl IScrollAnchorProvider.CurrentAnchor => null; // TODO: Implement
+
///
/// Gets the value of the HorizontalScrollBarVisibility attached property.
///
@@ -373,6 +376,16 @@ namespace Avalonia.Controls
control.SetValue(VerticalScrollBarVisibilityProperty, value);
}
+ void IScrollAnchorProvider.RegisterAnchorCandidate(IControl element)
+ {
+ // TODO: Implement
+ }
+
+ void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element)
+ {
+ // TODO: Implement
+ }
+
internal static Vector CoerceOffset(Size extent, Size viewport, Vector offset)
{
var maxX = Math.Max(extent.Width - viewport.Width, 0);
diff --git a/src/Avalonia.Controls/ShutdownMode.cs b/src/Avalonia.Controls/ShutdownMode.cs
index 46e27ff4e1..ee593c28a8 100644
--- a/src/Avalonia.Controls/ShutdownMode.cs
+++ b/src/Avalonia.Controls/ShutdownMode.cs
@@ -1,10 +1,12 @@
// 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.ApplicationLifetimes;
+
namespace Avalonia.Controls
{
///
- /// Describes the possible values for .
+ /// Describes the possible values for .
///
public enum ShutdownMode
{
diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs
index bc4733296b..9eaa246434 100644
--- a/src/Avalonia.Controls/Slider.cs
+++ b/src/Avalonia.Controls/Slider.cs
@@ -5,6 +5,7 @@ using System;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
+using Avalonia.Layout;
namespace Avalonia.Controls
{
diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs
index c29faa1b4d..bd3441078d 100644
--- a/src/Avalonia.Controls/StackPanel.cs
+++ b/src/Avalonia.Controls/StackPanel.cs
@@ -1,8 +1,9 @@
-// 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.
+// This source file is adapted from the Windows Presentation Foundation project.
+// (https://github.com/dotnet/wpf/)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
-using System.Linq;
using Avalonia.Input;
using Avalonia.Layout;
@@ -17,13 +18,13 @@ namespace Avalonia.Controls
/// Defines the property.
///
public static readonly StyledProperty SpacingProperty =
- AvaloniaProperty.Register(nameof(Spacing));
+ StackLayout.SpacingProperty.AddOwner();
///
/// Defines the property.
///
public static readonly StyledProperty OrientationProperty =
- AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical);
+ StackLayout.OrientationProperty.AddOwner();
///
/// Initializes static members of the class.
@@ -155,106 +156,122 @@ namespace Avalonia.Controls
}
///
- /// Measures the control.
+ /// General StackPanel layout behavior is to grow unbounded in the "stacking" direction (Size To Content).
+ /// Children in this dimension are encouraged to be as large as they like. In the other dimension,
+ /// StackPanel will assume the maximum size of its children.
///
- /// The available size.
- /// The desired size of the control.
+ /// Constraint
+ /// Desired size
protected override Size MeasureOverride(Size availableSize)
{
- double childAvailableWidth = double.PositiveInfinity;
- double childAvailableHeight = double.PositiveInfinity;
+ Size stackDesiredSize = new Size();
+ var children = Children;
+ Size layoutSlotSize = availableSize;
+ bool fHorizontal = (Orientation == Orientation.Horizontal);
+ double spacing = Spacing;
+ bool hasVisibleChild = false;
- if (Orientation == Orientation.Vertical)
+ //
+ // Initialize child sizing and iterator data
+ // Allow children as much size as they want along the stack.
+ //
+ if (fHorizontal)
{
- childAvailableWidth = availableSize.Width;
-
- if (!double.IsNaN(Width))
- {
- childAvailableWidth = Width;
- }
-
- childAvailableWidth = Math.Min(childAvailableWidth, MaxWidth);
- childAvailableWidth = Math.Max(childAvailableWidth, MinWidth);
+ layoutSlotSize = layoutSlotSize.WithWidth(Double.PositiveInfinity);
}
else
{
- childAvailableHeight = availableSize.Height;
+ layoutSlotSize = layoutSlotSize.WithHeight(Double.PositiveInfinity);
+ }
- if (!double.IsNaN(Height))
- {
- childAvailableHeight = Height;
- }
+ //
+ // Iterate through children.
+ // While we still supported virtualization, this was hidden in a child iterator (see source history).
+ //
+ for (int i = 0, count = children.Count; i < count; ++i)
+ {
+ // Get next child.
+ var child = children[i];
- childAvailableHeight = Math.Min(childAvailableHeight, MaxHeight);
- childAvailableHeight = Math.Max(childAvailableHeight, MinHeight);
- }
+ if (child == null)
+ { continue; }
- double measuredWidth = 0;
- double measuredHeight = 0;
- double spacing = Spacing;
- bool hasVisibleChild = Children.Any(c => c.IsVisible);
+ bool isVisible = child.IsVisible;
- foreach (Control child in Children)
- {
- child.Measure(new Size(childAvailableWidth, childAvailableHeight));
- Size size = child.DesiredSize;
+ if (isVisible && !hasVisibleChild)
+ {
+ hasVisibleChild = true;
+ }
- if (Orientation == Orientation.Vertical)
+ // Measure the child.
+ child.Measure(layoutSlotSize);
+ Size childDesiredSize = child.DesiredSize;
+
+ // Accumulate child size.
+ if (fHorizontal)
{
- measuredHeight += size.Height + (child.IsVisible ? spacing : 0);
- measuredWidth = Math.Max(measuredWidth, size.Width);
+ stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width + (isVisible ? spacing : 0) + childDesiredSize.Width);
+ stackDesiredSize = stackDesiredSize.WithHeight(Math.Max(stackDesiredSize.Height, childDesiredSize.Height));
}
else
{
- measuredWidth += size.Width + (child.IsVisible ? spacing : 0);
- measuredHeight = Math.Max(measuredHeight, size.Height);
+ stackDesiredSize = stackDesiredSize.WithWidth(Math.Max(stackDesiredSize.Width, childDesiredSize.Width));
+ stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height + (isVisible ? spacing : 0) + childDesiredSize.Height);
}
}
- if (Orientation == Orientation.Vertical)
+ if (fHorizontal)
{
- measuredHeight -= (hasVisibleChild ? spacing : 0);
+ stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width - (hasVisibleChild ? spacing : 0));
}
else
- {
- measuredWidth -= (hasVisibleChild ? spacing : 0);
+ {
+ stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height - (hasVisibleChild ? spacing : 0));
}
- return new Size(measuredWidth, measuredHeight).Constrain(availableSize);
+ return stackDesiredSize;
}
- ///
+ ///
+ /// Content arrangement.
+ ///
+ /// Arrange size
protected override Size ArrangeOverride(Size finalSize)
{
- var orientation = Orientation;
+ var children = Children;
+ bool fHorizontal = (Orientation == Orientation.Horizontal);
+ Rect rcChild = new Rect(finalSize);
+ double previousChildSize = 0.0;
var spacing = Spacing;
- var finalRect = new Rect(finalSize);
- var pos = 0.0;
- foreach (Control child in Children)
+ //
+ // Arrange and Position Children.
+ //
+ for (int i = 0, count = children.Count; i < count; ++i)
{
- if (!child.IsVisible)
- {
- continue;
- }
+ var child = children[i];
- double childWidth = child.DesiredSize.Width;
- double childHeight = child.DesiredSize.Height;
+ if (child == null)
+ { continue; }
- if (orientation == Orientation.Vertical)
+ if (fHorizontal)
{
- var rect = new Rect(0, pos, childWidth, childHeight)
- .Align(finalRect, child.HorizontalAlignment, VerticalAlignment.Top);
- ArrangeChild(child, rect, finalSize, orientation);
- pos += childHeight + spacing;
+ rcChild = rcChild.WithX(rcChild.X + previousChildSize);
+ previousChildSize = child.DesiredSize.Width;
+ rcChild = rcChild.WithWidth(previousChildSize);
+ rcChild = rcChild.WithHeight(Math.Max(finalSize.Height, child.DesiredSize.Height));
+ previousChildSize += spacing;
}
else
{
- var rect = new Rect(pos, 0, childWidth, childHeight)
- .Align(finalRect, HorizontalAlignment.Left, child.VerticalAlignment);
- ArrangeChild(child, rect, finalSize, orientation);
- pos += childWidth + spacing;
+ rcChild = rcChild.WithY(rcChild.Y + previousChildSize);
+ previousChildSize = child.DesiredSize.Height;
+ rcChild = rcChild.WithHeight(previousChildSize);
+ rcChild = rcChild.WithWidth(Math.Max(finalSize.Width, child.DesiredSize.Width));
+ previousChildSize += spacing;
}
+
+ ArrangeChild(child, rcChild, finalSize, Orientation);
}
return finalSize;
diff --git a/src/Avalonia.Controls/Templates/FuncControlTemplate.cs b/src/Avalonia.Controls/Templates/FuncControlTemplate.cs
index 4c3c92309e..19458cc6b8 100644
--- a/src/Avalonia.Controls/Templates/FuncControlTemplate.cs
+++ b/src/Avalonia.Controls/Templates/FuncControlTemplate.cs
@@ -16,9 +16,15 @@ namespace Avalonia.Controls.Templates
/// Initializes a new instance of the class.
///
/// The build function.
- public FuncControlTemplate(Func build)
+ public FuncControlTemplate(Func build)
: base(build)
{
}
+
+ public new ControlTemplateResult Build(ITemplatedControl param)
+ {
+ var (control, scope) = BuildWithNameScope(param);
+ return new ControlTemplateResult(control, scope);
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Controls/Templates/FuncControlTemplate`2.cs b/src/Avalonia.Controls/Templates/FuncControlTemplate`2.cs
index 8e49b51cb8..eec7a6030f 100644
--- a/src/Avalonia.Controls/Templates/FuncControlTemplate`2.cs
+++ b/src/Avalonia.Controls/Templates/FuncControlTemplate`2.cs
@@ -17,9 +17,9 @@ namespace Avalonia.Controls.Templates
/// Initializes a new instance of the class.
///
/// The build function.
- public FuncControlTemplate(Func build)
- : base(x => build((T)x))
+ public FuncControlTemplate(Func build)
+ : base((x, s) => build((T)x, s))
{
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs
index 1d90fcd01e..204540e431 100644
--- a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs
+++ b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs
@@ -17,7 +17,7 @@ namespace Avalonia.Controls.Templates
///
public static readonly FuncDataTemplate Default =
new FuncDataTemplate(
- data =>
+ (data, s) =>
{
if (data != null)
{
@@ -49,7 +49,7 @@ namespace Avalonia.Controls.Templates
/// Whether the control can be recycled.
public FuncDataTemplate(
Type type,
- Func build,
+ Func build,
bool supportsRecycling = false)
: this(o => IsInstance(o, type), build, supportsRecycling)
{
@@ -67,7 +67,7 @@ namespace Avalonia.Controls.Templates
/// Whether the control can be recycled.
public FuncDataTemplate(
Func match,
- Func build,
+ Func build,
bool supportsRecycling = false)
: base(build)
{
@@ -105,4 +105,4 @@ namespace Avalonia.Controls.Templates
return (o != null) && t.GetTypeInfo().IsAssignableFrom(o.GetType().GetTypeInfo());
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs
index 9339aa6924..68b737928d 100644
--- a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs
+++ b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs
@@ -18,7 +18,7 @@ namespace Avalonia.Controls.Templates
/// A function which when passed an object of returns a control.
///
/// Whether the control can be recycled.
- public FuncDataTemplate(Func build, bool supportsRecycling = false)
+ public FuncDataTemplate(Func build, bool supportsRecycling = false)
: base(typeof(T), CastBuild(build), supportsRecycling)
{
}
@@ -35,12 +35,30 @@ namespace Avalonia.Controls.Templates
/// Whether the control can be recycled.
public FuncDataTemplate(
Func match,
- Func build,
+ Func build,
bool supportsRecycling = false)
: base(CastMatch(match), CastBuild(build), supportsRecycling)
{
}
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// A function which determines whether the data template matches the specified data.
+ ///
+ ///
+ /// A function which when passed an object of returns a control.
+ ///
+ /// Whether the control can be recycled.
+ public FuncDataTemplate(
+ Func match,
+ Func build,
+ bool supportsRecycling = false)
+ : this(match, (a, _) => build(a), supportsRecycling)
+ {
+ }
+
///
/// Casts a strongly typed match function to a weakly typed one.
///
@@ -57,9 +75,9 @@ namespace Avalonia.Controls.Templates
/// The strong data type.
/// The strongly typed function.
/// The weakly typed function.
- private static Func CastBuild(Func f)
+ private static Func CastBuild(Func f)
{
- return o => f((T)o);
+ return (o, s) => f((T)o, s);
}
}
}
diff --git a/src/Avalonia.Controls/Templates/FuncMemberSelector.cs b/src/Avalonia.Controls/Templates/FuncMemberSelector.cs
deleted file mode 100644
index 5ab186261e..0000000000
--- a/src/Avalonia.Controls/Templates/FuncMemberSelector.cs
+++ /dev/null
@@ -1,35 +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;
-
-namespace Avalonia.Controls.Templates
-{
- ///
- /// Selects a member of an object using a .
- ///
- public class FuncMemberSelector : IMemberSelector
- {
- private readonly Func _selector;
-
- ///
- /// Initializes a new instance of the
- /// class.
- ///
- /// The selector.
- public FuncMemberSelector(Func selector)
- {
- this._selector = selector;
- }
-
- ///
- /// Selects a member of an object.
- ///
- /// The object.
- /// The selected member.
- public object Select(object o)
- {
- return (o is TObject) ? _selector((TObject)o) : default(TMember);
- }
- }
-}
diff --git a/src/Avalonia.Controls/Templates/FuncTemplateNameScopeExtensions.cs b/src/Avalonia.Controls/Templates/FuncTemplateNameScopeExtensions.cs
new file mode 100644
index 0000000000..76bc298e30
--- /dev/null
+++ b/src/Avalonia.Controls/Templates/FuncTemplateNameScopeExtensions.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Avalonia.Controls.Templates
+{
+ public static class FuncTemplateNameScopeExtensions
+ {
+ public static T RegisterInNameScope(this T control, INameScope scope)
+ where T : StyledElement
+ {
+ scope.Register(control.Name, control);
+ return control;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Templates/FuncTemplate`2.cs b/src/Avalonia.Controls/Templates/FuncTemplate`2.cs
index b1ce63fdf1..e0608d0471 100644
--- a/src/Avalonia.Controls/Templates/FuncTemplate`2.cs
+++ b/src/Avalonia.Controls/Templates/FuncTemplate`2.cs
@@ -13,13 +13,13 @@ namespace Avalonia.Controls.Templates
public class FuncTemplate : ITemplate
where TControl : IControl
{
- private readonly Func _func;
+ private readonly Func _func;
///
/// Initializes a new instance of the class.
///
/// The function used to create the control.
- public FuncTemplate(Func func)
+ public FuncTemplate(Func func)
{
Contract.Requires(func != null);
@@ -35,7 +35,14 @@ namespace Avalonia.Controls.Templates
///
public TControl Build(TParam param)
{
- return _func(param);
+ return BuildWithNameScope(param).control;
+ }
+
+ protected (TControl control, INameScope nameScope) BuildWithNameScope(TParam param)
+ {
+ var scope = new NameScope();
+ var rv = _func(param, scope);
+ return (rv, scope);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs
index e7c9cf8608..998ef0e9f2 100644
--- a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs
+++ b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs
@@ -28,7 +28,7 @@ namespace Avalonia.Controls.Templates
///
public FuncTreeDataTemplate(
Type type,
- Func build,
+ Func build,
Func itemsSelector)
: this(o => IsInstance(o, type), build, itemsSelector)
{
@@ -48,7 +48,7 @@ namespace Avalonia.Controls.Templates
///
public FuncTreeDataTemplate(
Func match,
- Func build,
+ Func build,
Func itemsSelector)
: base(match, build)
{
diff --git a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs
index 4ca96f60bd..41f870ab42 100644
--- a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs
+++ b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs
@@ -23,7 +23,7 @@ namespace Avalonia.Controls.Templates
/// items.
///
public FuncTreeDataTemplate(
- Func build,
+ Func build,
Func itemsSelector)
: base(
typeof(T),
@@ -46,7 +46,7 @@ namespace Avalonia.Controls.Templates
///
public FuncTreeDataTemplate(
Func match,
- Func build,
+ Func build,
Func itemsSelector)
: base(
CastMatch(match),
@@ -65,6 +65,17 @@ namespace Avalonia.Controls.Templates
return o => (o is T) && f((T)o);
}
+ ///
+ /// Casts a function with a typed parameter to an untyped function.
+ ///
+ /// The result.
+ /// The typed function.
+ /// The untyped function.
+ private static Func Cast(Func f)
+ {
+ return (o, s) => f((T)o, s);
+ }
+
///
/// Casts a function with a typed parameter to an untyped function.
///
diff --git a/src/Avalonia.Controls/Templates/IControlTemplate.cs b/src/Avalonia.Controls/Templates/IControlTemplate.cs
index 408329c450..a3ef0fa954 100644
--- a/src/Avalonia.Controls/Templates/IControlTemplate.cs
+++ b/src/Avalonia.Controls/Templates/IControlTemplate.cs
@@ -9,7 +9,25 @@ namespace Avalonia.Controls.Templates
///
/// Interface representing a template used to build a .
///
- public interface IControlTemplate : ITemplate
+ public interface IControlTemplate : ITemplate
{
}
-}
\ No newline at end of file
+
+ public class ControlTemplateResult
+ {
+ public IControl Control { get; }
+ public INameScope NameScope { get; }
+
+ public ControlTemplateResult(IControl control, INameScope nameScope)
+ {
+ Control = control;
+ NameScope = nameScope;
+ }
+
+ public void Deconstruct(out IControl control, out INameScope scope)
+ {
+ control = Control;
+ scope = NameScope;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Templates/IMemberSelector.cs b/src/Avalonia.Controls/Templates/IMemberSelector.cs
deleted file mode 100644
index e1ec42a849..0000000000
--- a/src/Avalonia.Controls/Templates/IMemberSelector.cs
+++ /dev/null
@@ -1,18 +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.
-
-namespace Avalonia.Controls.Templates
-{
- ///
- /// Selects a member of an object.
- ///
- public interface IMemberSelector
- {
- ///
- /// Selects a member of an object.
- ///
- /// The object.
- /// The selected member.
- object Select(object o);
- }
-}
diff --git a/src/Avalonia.Controls/Templates/ITemplate`2.cs b/src/Avalonia.Controls/Templates/ITemplate`2.cs
index f61292c64b..a0c0e88eca 100644
--- a/src/Avalonia.Controls/Templates/ITemplate`2.cs
+++ b/src/Avalonia.Controls/Templates/ITemplate`2.cs
@@ -8,7 +8,7 @@ namespace Avalonia.Controls.Templates
///
/// The type of the parameter.
/// The type of control.
- public interface ITemplate where TControl : IControl
+ public interface ITemplate
{
///
/// Creates the control.
@@ -19,4 +19,4 @@ namespace Avalonia.Controls.Templates
///
TControl Build(TParam param);
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index 6b0c48b97b..b9603b91ed 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -1,12 +1,9 @@
// 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.Reactive;
using System.Reactive.Linq;
using Avalonia.LogicalTree;
using Avalonia.Media;
-using Avalonia.Media.Immutable;
using Avalonia.Metadata;
namespace Avalonia.Controls
@@ -106,6 +103,14 @@ namespace Avalonia.Controls
FontWeightProperty,
FontSizeProperty,
FontStyleProperty);
+
+ Observable.Merge(
+ TextProperty.Changed,
+ TextAlignmentProperty.Changed,
+ FontSizeProperty.Changed,
+ FontStyleProperty.Changed,
+ FontWeightProperty.Changed
+ ).AddClassHandler((x,_) => x.OnTextPropertiesChanged());
}
///
@@ -114,18 +119,6 @@ namespace Avalonia.Controls
public TextBlock()
{
_text = string.Empty;
-
- Observable.Merge(
- this.GetObservable(TextProperty).Select(_ => Unit.Default),
- this.GetObservable(TextAlignmentProperty).Select(_ => Unit.Default),
- this.GetObservable(FontSizeProperty).Select(_ => Unit.Default),
- this.GetObservable(FontStyleProperty).Select(_ => Unit.Default),
- this.GetObservable(FontWeightProperty).Select(_ => Unit.Default))
- .Subscribe(_ =>
- {
- InvalidateFormattedText();
- InvalidateMeasure();
- });
}
///
@@ -408,5 +401,11 @@ namespace Avalonia.Controls
InvalidateFormattedText();
InvalidateMeasure();
}
+
+ private void OnTextPropertiesChanged()
+ {
+ InvalidateFormattedText();
+ InvalidateMeasure();
+ }
}
}
diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs
index 888f4a2013..4514109e12 100644
--- a/src/Avalonia.Controls/TreeView.cs
+++ b/src/Avalonia.Controls/TreeView.cs
@@ -105,32 +105,21 @@ namespace Avalonia.Controls
get => _selectedItem;
set
{
- SetAndRaise(SelectedItemProperty, ref _selectedItem,
- (object val, ref object backing, Action notifyWrapper) =>
- {
- var old = backing;
- backing = val;
-
- notifyWrapper(() =>
- RaisePropertyChanged(
- SelectedItemProperty,
- old,
- val));
+ SetAndRaise(SelectedItemProperty, ref _selectedItem, value);
- if (val != null)
- {
- if (SelectedItems.Count != 1 || SelectedItems[0] != val)
- {
- _syncingSelectedItems = true;
- SelectSingleItem(val);
- _syncingSelectedItems = false;
- }
- }
- else if (SelectedItems.Count > 0)
- {
- SelectedItems.Clear();
- }
- }, value);
+ if (value != null)
+ {
+ if (SelectedItems.Count != 1 || SelectedItems[0] != value)
+ {
+ _syncingSelectedItems = true;
+ SelectSingleItem(value);
+ _syncingSelectedItems = false;
+ }
+ }
+ else if (SelectedItems.Count > 0)
+ {
+ SelectedItems.Clear();
+ }
}
}
@@ -164,6 +153,48 @@ namespace Avalonia.Controls
}
}
+ ///
+ /// Expands the specified all descendent s.
+ ///
+ /// The item to expand.
+ public void ExpandSubTree(TreeViewItem item)
+ {
+ item.IsExpanded = true;
+
+ var panel = item.Presenter.Panel;
+
+ if (panel != null)
+ {
+ foreach (var child in panel.Children)
+ {
+ if (child is TreeViewItem treeViewItem)
+ {
+ ExpandSubTree(treeViewItem);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Selects all items in the .
+ ///
+ ///
+ /// Note that this method only selects nodes currently visible due to their parent nodes
+ /// being expanded: it does not expand nodes.
+ ///
+ public void SelectAll()
+ {
+ SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items);
+ }
+
+ ///
+ /// Deselects all items in the .
+ ///
+ public void UnselectAll()
+ {
+ SelectedItems.Clear();
+ }
+
///
/// Subscribes to the CollectionChanged event, if any.
///
@@ -409,7 +440,7 @@ namespace Avalonia.Controls
if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll))
{
- SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items);
+ SelectAll();
e.Handled = true;
}
}
@@ -479,7 +510,8 @@ namespace Avalonia.Controls
e.Source,
true,
(e.InputModifiers & InputModifiers.Shift) != 0,
- (e.InputModifiers & InputModifiers.Control) != 0);
+ (e.InputModifiers & InputModifiers.Control) != 0,
+ e.MouseButton == MouseButton.Right);
}
}
@@ -490,11 +522,13 @@ namespace Avalonia.Controls
/// Whether the item should be selected or unselected.
/// Whether the range modifier is enabled (i.e. shift key).
/// Whether the toggle modifier is enabled (i.e. ctrl key).
+ /// Whether the event is a right-click.
protected void UpdateSelectionFromContainer(
IControl container,
bool select = true,
bool rangeModifier = false,
- bool toggleModifier = false)
+ bool toggleModifier = false,
+ bool rightButton = false)
{
var item = ItemContainerGenerator.Index.ItemFromContainer(container);
@@ -515,7 +549,14 @@ namespace Avalonia.Controls
var multi = (mode & SelectionMode.Multiple) != 0;
var range = multi && selectedContainer != null && rangeModifier;
- if (!toggle && !range)
+ if (rightButton)
+ {
+ if (!SelectedItems.Contains(item))
+ {
+ SelectSingleItem(item);
+ }
+ }
+ else if (!toggle && !range)
{
SelectSingleItem(item);
}
@@ -684,6 +725,7 @@ namespace Avalonia.Controls
/// Whether the container should be selected or unselected.
/// Whether the range modifier is enabled (i.e. shift key).
/// Whether the toggle modifier is enabled (i.e. ctrl key).
+ /// Whether the event is a right-click.
///
/// True if the event originated from a container that belongs to the control; otherwise
/// false.
@@ -692,13 +734,14 @@ namespace Avalonia.Controls
IInteractive eventSource,
bool select = true,
bool rangeModifier = false,
- bool toggleModifier = false)
+ bool toggleModifier = false,
+ bool rightButton = false)
{
var container = GetContainerFromEventSource(eventSource);
if (container != null)
{
- UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier);
+ UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier, rightButton);
return true;
}
diff --git a/src/Avalonia.Controls/UserControl.cs b/src/Avalonia.Controls/UserControl.cs
index e42ca5e0e6..4f15839821 100644
--- a/src/Avalonia.Controls/UserControl.cs
+++ b/src/Avalonia.Controls/UserControl.cs
@@ -9,40 +9,8 @@ namespace Avalonia.Controls
///
/// Provides the base class for defining a new control that encapsulates related existing controls and provides its own logic.
///
- public class UserControl : ContentControl, IStyleable, INameScope
+ public class UserControl : ContentControl, IStyleable
{
- private readonly NameScope _nameScope = new NameScope();
- ///
- event EventHandler INameScope.Registered
- {
- add { _nameScope.Registered += value; }
- remove { _nameScope.Registered -= value; }
- }
-
- ///
- event EventHandler INameScope.Unregistered
- {
- add { _nameScope.Unregistered += value; }
- remove { _nameScope.Unregistered -= value; }
- }
-
- ///
- void INameScope.Register(string name, object element)
- {
- _nameScope.Register(name, element);
- }
-
- ///
- object INameScope.Find(string name)
- {
- return _nameScope.Find(name);
- }
-
- ///
- void INameScope.Unregister(string name)
- {
- _nameScope.Unregister(name);
- }
}
}
diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs
index 5c117f508b..d2793fe0dd 100644
--- a/src/Avalonia.Controls/Window.cs
+++ b/src/Avalonia.Controls/Window.cs
@@ -48,7 +48,7 @@ namespace Avalonia.Controls
///
/// A top-level window.
///
- public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot, INameScope
+ public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot
{
///
/// Defines the property.
@@ -157,20 +157,6 @@ namespace Avalonia.Controls
_maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size);
}
- ///
- event EventHandler INameScope.Registered
- {
- add { _nameScope.Registered += value; }
- remove { _nameScope.Registered -= value; }
- }
-
- ///
- event EventHandler INameScope.Unregistered
- {
- add { _nameScope.Unregistered += value; }
- remove { _nameScope.Unregistered -= value; }
- }
-
///
/// Gets the platform-specific window implementation.
///
@@ -494,24 +480,6 @@ namespace Avalonia.Controls
}
}
- ///
- void INameScope.Register(string name, object element)
- {
- _nameScope.Register(name, element);
- }
-
- ///
- object INameScope.Find(string name)
- {
- return _nameScope.Find(name);
- }
-
- ///
- void INameScope.Unregister(string name)
- {
- _nameScope.Unregister(name);
- }
-
///
protected override Size MeasureOverride(Size availableSize)
{
diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs
index 6f53d853c7..3acf341c35 100644
--- a/src/Avalonia.Controls/WrapPanel.cs
+++ b/src/Avalonia.Controls/WrapPanel.cs
@@ -4,6 +4,7 @@
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using Avalonia.Input;
+using Avalonia.Layout;
using Avalonia.Utilities;
using static System.Math;
diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
index 3b6d071583..a7a94130ea 100644
--- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
+++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
@@ -54,7 +54,7 @@ namespace Avalonia.DesignerSupport.Remote
.Bind().ToConstant(instance)
.Bind().ToConstant(threading)
.Bind().ToConstant(new RenderLoop())
- .Bind().ToConstant(threading)
+ .Bind().ToConstant(new DefaultRenderTimer(60))
.Bind().ToSingleton()
.Bind().ToConstant(instance)
.Bind().ToSingleton()
diff --git a/src/Avalonia.Diagnostics/DevTools.xaml b/src/Avalonia.Diagnostics/DevTools.xaml
index 0f55d42e33..1df0f3a097 100644
--- a/src/Avalonia.Diagnostics/DevTools.xaml
+++ b/src/Avalonia.Diagnostics/DevTools.xaml
@@ -1,23 +1,24 @@
-
-
-
-
-
-
+
-
-
-
+
+
+
+
+
+
+
+
+
Hold Ctrl+Shift over a control to inspect.
-
+
Focused:
-
-
+
+
Pointer Over:
-
+
diff --git a/src/Avalonia.Diagnostics/DevTools.xaml.cs b/src/Avalonia.Diagnostics/DevTools.xaml.cs
index e0bacf326b..cc3c545d84 100644
--- a/src/Avalonia.Diagnostics/DevTools.xaml.cs
+++ b/src/Avalonia.Diagnostics/DevTools.xaml.cs
@@ -1,10 +1,12 @@
+// 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.Linq;
using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
-using Avalonia.Controls.Templates;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Input.Raw;
@@ -15,22 +17,22 @@ using Avalonia.VisualTree;
namespace Avalonia
{
- public static class DevToolsExtensions
- {
- public static void AttachDevTools(this TopLevel control)
- {
- Avalonia.Diagnostics.DevTools.Attach(control);
- }
- }
+ public static class DevToolsExtensions
+ {
+ public static void AttachDevTools(this TopLevel control)
+ {
+ Diagnostics.DevTools.Attach(control);
+ }
+ }
}
namespace Avalonia.Diagnostics
{
- public class DevTools : UserControl
+ public class DevTools : UserControl
{
- private static Dictionary s_open = new Dictionary();
- private static HashSet s_visualTreeRoots = new HashSet();
- private IDisposable _keySubscription;
+ private static readonly Dictionary s_open = new Dictionary();
+ private static readonly HashSet s_visualTreeRoots = new HashSet();
+ private readonly IDisposable _keySubscription;
public DevTools(IControl root)
{
@@ -46,7 +48,6 @@ namespace Avalonia.Diagnostics
// HACK: needed for XAMLIL, will fix that later
public DevTools()
{
-
}
public IControl Root { get; }
@@ -64,9 +65,8 @@ namespace Avalonia.Diagnostics
if (e.Key == Key.F12)
{
var control = (TopLevel)sender;
- var devToolsWindow = default(Window);
- if (s_open.TryGetValue(control, out devToolsWindow))
+ if (s_open.TryGetValue(control, out var devToolsWindow))
{
devToolsWindow.Activate();
}
@@ -79,10 +79,8 @@ namespace Avalonia.Diagnostics
Width = 1024,
Height = 512,
Content = devTools,
- DataTemplates =
- {
- new ViewLocator(),
- }
+ DataTemplates = { new ViewLocator() },
+ Title = "Avalonia DevTools"
};
devToolsWindow.Closed += devTools.DevToolsClosed;
@@ -114,7 +112,6 @@ namespace Avalonia.Diagnostics
if ((e.Modifiers) == modifiers)
{
-
var point = (Root.VisualRoot as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default(Point);
var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible))
.FirstOrDefault();
diff --git a/src/Avalonia.Diagnostics/Models/EventChainLink.cs b/src/Avalonia.Diagnostics/Models/EventChainLink.cs
index aab50a13dd..464187a048 100644
--- a/src/Avalonia.Diagnostics/Models/EventChainLink.cs
+++ b/src/Avalonia.Diagnostics/Models/EventChainLink.cs
@@ -12,9 +12,9 @@ namespace Avalonia.Diagnostics.Models
{
Contract.Requires(handler != null);
- this.Handler = handler;
- this.Handled = handled;
- this.Route = route;
+ Handler = handler;
+ Handled = handled;
+ Route = route;
}
public object Handler { get; }
@@ -27,6 +27,7 @@ namespace Avalonia.Diagnostics.Models
{
return named.Name + " (" + Handler.GetType().Name + ")";
}
+
return Handler.GetType().Name;
}
}
diff --git a/src/Avalonia.Diagnostics/ViewLocator.cs b/src/Avalonia.Diagnostics/ViewLocator.cs
index b107338aec..a66703301d 100644
--- a/src/Avalonia.Diagnostics/ViewLocator.cs
+++ b/src/Avalonia.Diagnostics/ViewLocator.cs
@@ -7,7 +7,7 @@ using Avalonia.Controls.Templates;
namespace Avalonia.Diagnostics
{
- public class ViewLocator : IDataTemplate
+ internal class ViewLocator : IDataTemplate
{
public bool SupportsRecycling => false;
@@ -31,4 +31,4 @@ namespace Avalonia.Diagnostics
return data is TViewModel;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs
index d723890196..4b832f7ce6 100644
--- a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs
@@ -20,16 +20,6 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
- public IEnumerable Classes
- {
- get;
- private set;
- }
-
- public IEnumerable Properties
- {
- get;
- private set;
- }
+ public IEnumerable Properties { get; }
}
}
diff --git a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
index c6d3f02e8b..9f524a21eb 100644
--- a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
@@ -2,7 +2,8 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
-using System.Reactive.Linq;
+using System.Collections.ObjectModel;
+using System.Linq;
using Avalonia.Controls;
using Avalonia.Input;
@@ -10,21 +11,23 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class DevToolsViewModel : ViewModelBase
{
- private ViewModelBase _content;
- private int _selectedTab;
- private TreePageViewModel _logicalTree;
- private TreePageViewModel _visualTree;
- private EventsViewModel _eventsView;
+ private IDevToolViewModel _selectedTool;
private string _focusedControl;
private string _pointerOverElement;
public DevToolsViewModel(IControl root)
{
- _logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root));
- _visualTree = new TreePageViewModel(VisualTreeNode.Create(root));
- _eventsView = new EventsViewModel(root);
+ Tools = new ObservableCollection
+ {
+ new TreePageViewModel(LogicalTreeNode.Create(root), "Logical Tree"),
+ new TreePageViewModel(VisualTreeNode.Create(root), "Visual Tree"),
+ new EventsViewModel(root)
+ };
+
+ SelectedTool = Tools.First();
UpdateFocusedControl();
+
KeyboardDevice.Instance.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement))
@@ -33,58 +36,33 @@ namespace Avalonia.Diagnostics.ViewModels
}
};
- SelectedTab = 0;
root.GetObservable(TopLevel.PointerOverElementProperty)
.Subscribe(x => PointerOverElement = x?.GetType().Name);
}
- public ViewModelBase Content
+ public IDevToolViewModel SelectedTool
{
- get { return _content; }
- private set { RaiseAndSetIfChanged(ref _content, value); }
+ get => _selectedTool;
+ set => RaiseAndSetIfChanged(ref _selectedTool, value);
}
- public int SelectedTab
- {
- get { return _selectedTab; }
- set
- {
- _selectedTab = value;
-
- switch (value)
- {
- case 0:
- Content = _logicalTree;
- break;
- case 1:
- Content = _visualTree;
- break;
- case 2:
- Content = _eventsView;
- break;
- }
-
- RaisePropertyChanged();
- }
- }
+ public ObservableCollection Tools { get; }
public string FocusedControl
{
- get { return _focusedControl; }
- private set { RaiseAndSetIfChanged(ref _focusedControl, value); }
+ get => _focusedControl;
+ private set => RaiseAndSetIfChanged(ref _focusedControl, value);
}
public string PointerOverElement
{
- get { return _pointerOverElement; }
- private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); }
+ get => _pointerOverElement;
+ private set => RaiseAndSetIfChanged(ref _pointerOverElement, value);
}
public void SelectControl(IControl control)
{
- var tree = Content as TreePageViewModel;
-
- if (tree != null)
+ if (SelectedTool is TreePageViewModel tree)
{
tree.SelectControl(control);
}
diff --git a/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs
index 0674918400..7e38749a6f 100644
--- a/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs
@@ -13,22 +13,18 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class EventOwnerTreeNode : EventTreeNodeBase
{
- private static readonly RoutedEvent[] s_defaultEvents = new RoutedEvent[]
+ private static readonly RoutedEvent[] s_defaultEvents =
{
- Button.ClickEvent,
- InputElement.KeyDownEvent,
- InputElement.KeyUpEvent,
- InputElement.TextInputEvent,
- InputElement.PointerReleasedEvent,
- InputElement.PointerPressedEvent,
+ Button.ClickEvent, InputElement.KeyDownEvent, InputElement.KeyUpEvent, InputElement.TextInputEvent,
+ InputElement.PointerReleasedEvent, InputElement.PointerPressedEvent
};
public EventOwnerTreeNode(Type type, IEnumerable events, EventsViewModel vm)
: base(null, type.Name)
{
- this.Children = new AvaloniaList(events.OrderBy(e => e.Name)
+ Children = new AvaloniaList(events.OrderBy(e => e.Name)
.Select(e => new EventTreeNode(this, e, vm) { IsEnabled = s_defaultEvents.Contains(e) }));
- this.IsExpanded = true;
+ IsExpanded = true;
}
public override bool? IsEnabled
@@ -39,6 +35,7 @@ namespace Avalonia.Diagnostics.ViewModels
if (base.IsEnabled != value)
{
base.IsEnabled = value;
+
if (_updateChildren && value != null)
{
foreach (var child in Children)
diff --git a/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs
index 7ece790310..36f1904253 100644
--- a/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
-
using Avalonia.Diagnostics.Models;
using Avalonia.Interactivity;
using Avalonia.Threading;
@@ -12,8 +11,8 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class EventTreeNode : EventTreeNodeBase
{
- private RoutedEvent _event;
- private EventsViewModel _parentViewModel;
+ private readonly RoutedEvent _event;
+ private readonly EventsViewModel _parentViewModel;
private bool _isRegistered;
private FiredEvent _currentEvent;
@@ -23,8 +22,8 @@ namespace Avalonia.Diagnostics.ViewModels
Contract.Requires(@event != null);
Contract.Requires(vm != null);
- this._event = @event;
- this._parentViewModel = vm;
+ _event = @event;
+ _parentViewModel = vm;
}
public override bool? IsEnabled
diff --git a/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs b/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs
index 146a8cea8e..4be4d8f74e 100644
--- a/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs
@@ -12,10 +12,10 @@ namespace Avalonia.Diagnostics.ViewModels
private bool _isExpanded;
private bool? _isEnabled = false;
- public EventTreeNodeBase(EventTreeNodeBase parent, string text)
+ protected EventTreeNodeBase(EventTreeNodeBase parent, string text)
{
- this.Parent = parent;
- this.Text = text;
+ Parent = parent;
+ Text = text;
}
public IAvaloniaReadOnlyList Children
@@ -26,14 +26,14 @@ namespace Avalonia.Diagnostics.ViewModels
public bool IsExpanded
{
- get { return _isExpanded; }
- set { RaiseAndSetIfChanged(ref _isExpanded, value); }
+ get => _isExpanded;
+ set => RaiseAndSetIfChanged(ref _isExpanded, value);
}
public virtual bool? IsEnabled
{
- get { return _isEnabled; }
- set { RaiseAndSetIfChanged(ref _isEnabled, value); }
+ get => _isEnabled;
+ set => RaiseAndSetIfChanged(ref _isEnabled, value);
}
public EventTreeNodeBase Parent
@@ -44,7 +44,6 @@ namespace Avalonia.Diagnostics.ViewModels
public string Text
{
get;
- private set;
}
internal void UpdateChecked()
@@ -55,7 +54,9 @@ namespace Avalonia.Diagnostics.ViewModels
{
if (Children == null)
return false;
+
bool? value = false;
+
for (int i = 0; i < Children.Count; i++)
{
if (i == 0)
diff --git a/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs
index a23677afc8..1c868148ce 100644
--- a/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs
@@ -5,8 +5,6 @@ using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
-using System.Windows.Input;
-
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Interactivity;
@@ -14,21 +12,24 @@ using Avalonia.Media;
namespace Avalonia.Diagnostics.ViewModels
{
- internal class EventsViewModel : ViewModelBase
+ internal class EventsViewModel : ViewModelBase, IDevToolViewModel
{
private readonly IControl _root;
private FiredEvent _selectedEvent;
public EventsViewModel(IControl root)
{
- this._root = root;
- this.Nodes = RoutedEventRegistry.Instance.GetAllRegistered()
+ _root = root;
+
+ Nodes = RoutedEventRegistry.Instance.GetAllRegistered()
.GroupBy(e => e.OwnerType)
.OrderBy(e => e.Key.Name)
.Select(g => new EventOwnerTreeNode(g.Key, g, this))
.ToArray();
}
+ public string Name => "Events";
+
public EventTreeNodeBase[] Nodes { get; }
public ObservableCollection RecordedEvents { get; } = new ObservableCollection();
@@ -49,7 +50,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
- return (bool)value ? Brushes.LightGreen : Brushes.Transparent;
+ return (bool)value ? Brushes.Green : Brushes.Transparent;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
diff --git a/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs b/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs
index 049280c390..daf8ebd0f6 100644
--- a/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.ObjectModel;
-
using Avalonia.Diagnostics.Models;
using Avalonia.Interactivity;
@@ -11,7 +10,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class FiredEvent : ViewModelBase
{
- private RoutedEventArgs _eventArgs;
+ private readonly RoutedEventArgs _eventArgs;
private EventChainLink _handledBy;
public FiredEvent(RoutedEventArgs eventArgs, EventChainLink originator)
@@ -19,8 +18,8 @@ namespace Avalonia.Diagnostics.ViewModels
Contract.Requires(eventArgs != null);
Contract.Requires(originator != null);
- this._eventArgs = eventArgs;
- this.Originator = originator;
+ _eventArgs = eventArgs;
+ Originator = originator;
AddToChain(originator);
}
@@ -42,8 +41,9 @@ namespace Avalonia.Diagnostics.ViewModels
if (IsHandled)
{
return $"{Event.Name} on {Originator.HandlerName};" + Environment.NewLine +
- $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}";
+ $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}";
}
+
return $"{Event.Name} on {Originator.HandlerName}; strategies: {Event.RoutingStrategies}";
}
}
@@ -52,7 +52,7 @@ namespace Avalonia.Diagnostics.ViewModels
public EventChainLink HandledBy
{
- get { return _handledBy; }
+ get => _handledBy;
set
{
if (_handledBy != value)
diff --git a/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs
new file mode 100644
index 0000000000..0434230a63
--- /dev/null
+++ b/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs
@@ -0,0 +1,16 @@
+// 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.
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+ ///
+ /// View model interface for tool showing up in DevTools
+ ///
+ public interface IDevToolViewModel
+ {
+ ///
+ /// Name of a tool.
+ ///
+ string Name { get; }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs
index 638cf6c88f..0b9bd85b4f 100644
--- a/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs
@@ -17,8 +17,7 @@ namespace Avalonia.Diagnostics.ViewModels
public static LogicalTreeNode[] Create(object control)
{
- var logical = control as ILogical;
- return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null;
+ return control is ILogical logical ? new[] { new LogicalTreeNode(logical, null) } : null;
}
}
}
diff --git a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs b/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs
index 2609b74ce0..523be406c8 100644
--- a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs
@@ -26,7 +26,9 @@ namespace Avalonia.Diagnostics.ViewModels
Value = diagnostic.Value ?? "(null)";
Priority = (diagnostic.Priority != BindingPriority.Unset) ?
diagnostic.Priority.ToString() :
- diagnostic.Property.Inherits ? "Inherited" : "Unset";
+ diagnostic.Property.Inherits ?
+ "Inherited" :
+ "Unset";
Diagnostic = diagnostic.Diagnostic;
});
}
@@ -37,20 +39,20 @@ namespace Avalonia.Diagnostics.ViewModels
public string Priority
{
- get { return _priority; }
- private set { RaiseAndSetIfChanged(ref _priority, value); }
+ get => _priority;
+ private set => RaiseAndSetIfChanged(ref _priority, value);
}
public string Diagnostic
{
- get { return _diagnostic; }
- private set { RaiseAndSetIfChanged(ref _diagnostic, value); }
+ get => _diagnostic;
+ private set => RaiseAndSetIfChanged(ref _diagnostic, value);
}
public object Value
{
- get { return _value; }
- private set { RaiseAndSetIfChanged(ref _value, value); }
+ get => _value;
+ private set => RaiseAndSetIfChanged(ref _value, value);
}
}
}
diff --git a/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs
index 7c403e1b04..902eb81bd9 100644
--- a/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs
@@ -27,9 +27,9 @@ namespace Avalonia.Diagnostics.ViewModels
var classesChanged = Observable.FromEventPattern<
NotifyCollectionChangedEventHandler,
NotifyCollectionChangedEventArgs>(
- x => styleable.Classes.CollectionChanged += x,
- x => styleable.Classes.CollectionChanged -= x)
- .TakeUntil(((IStyleable)styleable).StyleDetach);
+ x => styleable.Classes.CollectionChanged += x,
+ x => styleable.Classes.CollectionChanged -= x)
+ .TakeUntil(styleable.StyleDetach);
classesChanged.Select(_ => Unit.Default)
.StartWith(Unit.Default)
@@ -55,8 +55,8 @@ namespace Avalonia.Diagnostics.ViewModels
public string Classes
{
- get { return _classes; }
- private set { RaiseAndSetIfChanged(ref _classes, value); }
+ get => _classes;
+ private set => RaiseAndSetIfChanged(ref _classes, value);
}
public IVisual Visual
@@ -66,8 +66,8 @@ namespace Avalonia.Diagnostics.ViewModels
public bool IsExpanded
{
- get { return _isExpanded; }
- set { RaiseAndSetIfChanged(ref _isExpanded, value); }
+ get => _isExpanded;
+ set => RaiseAndSetIfChanged(ref _isExpanded, value);
}
public TreeNode Parent
@@ -78,7 +78,6 @@ namespace Avalonia.Diagnostics.ViewModels
public string Type
{
get;
- private set;
}
}
}
diff --git a/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs
index dba44c5d0c..b2b1aaa723 100644
--- a/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs
@@ -6,21 +6,24 @@ using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
- internal class TreePageViewModel : ViewModelBase
+ internal class TreePageViewModel : ViewModelBase, IDevToolViewModel
{
private TreeNode _selected;
private ControlDetailsViewModel _details;
- public TreePageViewModel(TreeNode[] nodes)
+ public TreePageViewModel(TreeNode[] nodes, string name)
{
Nodes = nodes;
+ Name = name;
}
+ public string Name { get; }
+
public TreeNode[] Nodes { get; protected set; }
public TreeNode SelectedNode
{
- get { return _selected; }
+ get => _selected;
set
{
if (RaiseAndSetIfChanged(ref _selected, value))
@@ -32,8 +35,8 @@ namespace Avalonia.Diagnostics.ViewModels
public ControlDetailsViewModel Details
{
- get { return _details; }
- private set { RaiseAndSetIfChanged(ref _details, value); }
+ get => _details;
+ private set => RaiseAndSetIfChanged(ref _details, value);
}
public TreeNode FindNode(IControl control)
@@ -63,7 +66,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
control = control.GetVisualParent();
}
- }
+ }
if (node != null)
{
@@ -87,16 +90,14 @@ namespace Avalonia.Diagnostics.ViewModels
{
return node;
}
- else
+
+ foreach (var child in node.Children)
{
- foreach (var child in node.Children)
- {
- var result = FindNode(child, control);
+ var result = FindNode(child, control);
- if (result != null)
- {
- return result;
- }
+ if (result != null)
+ {
+ return result;
}
}
diff --git a/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs b/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs
index 00660754c0..a6ff4dd853 100644
--- a/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs
@@ -1,11 +1,14 @@
-using System.Collections.Generic;
+// 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 System.ComponentModel;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
namespace Avalonia.Diagnostics.ViewModels
{
- public class ViewModelBase : INotifyPropertyChanged
+ internal class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
diff --git a/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs
index 8c070261d9..47ef91507a 100644
--- a/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs
@@ -29,12 +29,11 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
- public bool IsInTemplate { get; private set; }
+ public bool IsInTemplate { get; }
public static VisualTreeNode[] Create(object control)
{
- var visual = control as IVisual;
- return visual != null ? new[] { new VisualTreeNode(visual, null) } : null;
+ return control is IVisual visual ? new[] { new VisualTreeNode(visual, null) } : null;
}
}
}
diff --git a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs
index 381b2e04b4..fb867ab55e 100644
--- a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs
+++ b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs
@@ -7,7 +7,6 @@ using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Media;
-using Avalonia.Styling;
namespace Avalonia.Diagnostics.Views
{
@@ -15,6 +14,7 @@ namespace Avalonia.Diagnostics.Views
{
private static readonly StyledProperty ViewModelProperty =
AvaloniaProperty.Register(nameof(ViewModel));
+
private SimpleGrid _grid;
public ControlDetailsView()
@@ -26,7 +26,7 @@ namespace Avalonia.Diagnostics.Views
public ControlDetailsViewModel ViewModel
{
- get { return GetValue(ViewModelProperty); }
+ get => GetValue(ViewModelProperty);
private set
{
SetValue(ViewModelProperty, value);
@@ -38,48 +38,37 @@ namespace Avalonia.Diagnostics.Views
{
Func> pt = PropertyTemplate;
- Content = new ScrollViewer
- {
- Content = _grid = new SimpleGrid
- {
- Styles =
- {
- new Style(x => x.Is())
- {
- Setters = new[]
- {
- new Setter(MarginProperty, new Thickness(2)),
- }
- },
- },
- [GridRepeater.TemplateProperty] = pt,
- }
- };
+ Content = new ScrollViewer { Content = _grid = new SimpleGrid { [GridRepeater.TemplateProperty] = pt } };
}
private IEnumerable PropertyTemplate(object i)
{
var property = (PropertyDetails)i;
+ var margin = new Thickness(2);
+
yield return new TextBlock
{
+ Margin = margin,
Text = property.Name,
TextWrapping = TextWrapping.NoWrap,
- [!ToolTip.TipProperty] = property.GetObservable(nameof(property.Diagnostic)).ToBinding(),
+ [!ToolTip.TipProperty] = property.GetObservable(nameof(property.Diagnostic)).ToBinding()
};
yield return new TextBlock
{
+ Margin = margin,
TextWrapping = TextWrapping.NoWrap,
[!TextBlock.TextProperty] = property.GetObservable(nameof(property.Value))
.Select(v => v?.ToString())
- .ToBinding(),
+ .ToBinding()
};
yield return new TextBlock
{
+ Margin = margin,
TextWrapping = TextWrapping.NoWrap,
- [!TextBlock.TextProperty] = property.GetObservable((nameof(property.Priority))).ToBinding(),
+ [!TextBlock.TextProperty] = property.GetObservable((nameof(property.Priority))).ToBinding()
};
}
}
diff --git a/src/Avalonia.Diagnostics/Views/EventsView.xaml b/src/Avalonia.Diagnostics/Views/EventsView.xaml
index 8d4d37f7b3..406dd433a2 100644
--- a/src/Avalonia.Diagnostics/Views/EventsView.xaml
+++ b/src/Avalonia.Diagnostics/Views/EventsView.xaml
@@ -2,53 +2,57 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
x:Class="Avalonia.Diagnostics.Views.EventsView">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Diagnostics/Views/EventsView.xaml.cs b/src/Avalonia.Diagnostics/Views/EventsView.xaml.cs
index a51cb4b7d9..41d74659ca 100644
--- a/src/Avalonia.Diagnostics/Views/EventsView.xaml.cs
+++ b/src/Avalonia.Diagnostics/Views/EventsView.xaml.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Linq;
-
using Avalonia.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Markup.Xaml;
@@ -11,15 +10,16 @@ namespace Avalonia.Diagnostics.Views
{
public class EventsView : UserControl
{
- private ListBox _events;
+ private readonly ListBox _events;
public EventsView()
{
- this.InitializeComponent();
+ InitializeComponent();
_events = this.FindControl("events");
}
- private void RecordedEvents_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+ private void RecordedEvents_CollectionChanged(object sender,
+ System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
_events.ScrollIntoView(_events.Items.OfType().LastOrDefault());
}
diff --git a/src/Avalonia.Diagnostics/Views/GridRepeater.cs b/src/Avalonia.Diagnostics/Views/GridRepeater.cs
index ff12fde25b..b0ff26c7b6 100644
--- a/src/Avalonia.Diagnostics/Views/GridRepeater.cs
+++ b/src/Avalonia.Diagnostics/Views/GridRepeater.cs
@@ -14,7 +14,8 @@ namespace Avalonia.Diagnostics.Views
AvaloniaProperty.RegisterAttached("Items", typeof(GridRepeater));
public static readonly AttachedProperty>> TemplateProperty =
- AvaloniaProperty.RegisterAttached>>("Template", typeof(GridRepeater));
+ AvaloniaProperty.RegisterAttached>>("Template",
+ typeof(GridRepeater));
static GridRepeater()
{
diff --git a/src/Avalonia.Diagnostics/Views/PropertyChangedExtenions.cs b/src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs
similarity index 63%
rename from src/Avalonia.Diagnostics/Views/PropertyChangedExtenions.cs
rename to src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs
index 2d833763a6..24b2f29463 100644
--- a/src/Avalonia.Diagnostics/Views/PropertyChangedExtenions.cs
+++ b/src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs
@@ -1,4 +1,7 @@
-using System;
+// 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.ComponentModel;
using System.Reactive.Linq;
using System.Reflection;
@@ -20,11 +23,11 @@ namespace Avalonia.Diagnostics.Views
}
return Observable.FromEventPattern(
- e => source.PropertyChanged += e,
- e => source.PropertyChanged -= e)
- .Where(e => e.EventArgs.PropertyName == propertyName)
- .Select(_ => (T)property.GetValue(source))
- .StartWith((T)property.GetValue(source));
+ e => source.PropertyChanged += e,
+ e => source.PropertyChanged -= e)
+ .Where(e => e.EventArgs.PropertyName == propertyName)
+ .Select(_ => (T)property.GetValue(source))
+ .StartWith((T)property.GetValue(source));
}
}
}
diff --git a/src/Avalonia.Diagnostics/Views/SimpleGrid.cs b/src/Avalonia.Diagnostics/Views/SimpleGrid.cs
index 33d9d81ed7..4fc77666e1 100644
--- a/src/Avalonia.Diagnostics/Views/SimpleGrid.cs
+++ b/src/Avalonia.Diagnostics/Views/SimpleGrid.cs
@@ -15,8 +15,8 @@ namespace Avalonia.Diagnostics.Views
///
public class SimpleGrid : Panel
{
- private List _columnWidths = new List();
- private List _rowHeights = new List();
+ private readonly List _columnWidths = new List();
+ private readonly List _rowHeights = new List();
private double _totalWidth;
private double _totalHeight;
@@ -31,7 +31,7 @@ namespace Avalonia.Diagnostics.Views
///
public static readonly AttachedProperty RowProperty =
AvaloniaProperty.RegisterAttached("Row");
-
+
///
/// Gets the value of the Column attached property for a control.
///
@@ -62,7 +62,7 @@ namespace Avalonia.Diagnostics.Views
control.SetValue(ColumnProperty, value);
}
-
+
///
/// Sets the value of the Row attached property for a control.
///
diff --git a/src/Avalonia.Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Views/TreePageView.xaml
index e927bcdf29..ca7314264a 100644
--- a/src/Avalonia.Diagnostics/Views/TreePageView.xaml
+++ b/src/Avalonia.Diagnostics/Views/TreePageView.xaml
@@ -2,25 +2,25 @@
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Avalonia.Diagnostics.Views.TreePageView">
-
+
-
-
+
+
-
-
+
+
diff --git a/src/Avalonia.Diagnostics/Views/TreePage.xaml.cs b/src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs
similarity index 91%
rename from src/Avalonia.Diagnostics/Views/TreePage.xaml.cs
rename to src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs
index d445f1cd70..ddf1473b45 100644
--- a/src/Avalonia.Diagnostics/Views/TreePage.xaml.cs
+++ b/src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs
@@ -1,3 +1,6 @@
+// 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;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Primitives;
@@ -16,7 +19,7 @@ namespace Avalonia.Diagnostics.Views
public TreePageView()
{
- this.InitializeComponent();
+ InitializeComponent();
_tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized;
}
@@ -36,7 +39,7 @@ namespace Avalonia.Diagnostics.Views
_adorner = new Rectangle
{
Fill = new SolidColorBrush(0x80a0c5e8),
- [AdornerLayer.AdornedElementProperty] = node.Visual,
+ [AdornerLayer.AdornedElementProperty] = node.Visual
};
layer.Children.Add(_adorner);
diff --git a/src/Avalonia.Diagnostics/Debug.cs b/src/Avalonia.Diagnostics/VisualTreeDebug.cs
similarity index 93%
rename from src/Avalonia.Diagnostics/Debug.cs
rename to src/Avalonia.Diagnostics/VisualTreeDebug.cs
index af3b5a8552..47511e0508 100644
--- a/src/Avalonia.Diagnostics/Debug.cs
+++ b/src/Avalonia.Diagnostics/VisualTreeDebug.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
-using System.Linq;
using System.Text;
using Avalonia.Controls;
using Avalonia.Data;
@@ -10,7 +9,7 @@ using Avalonia.VisualTree;
namespace Avalonia.Diagnostics
{
- public static class Debug
+ public static class VisualTreeDebug
{
public static string PrintVisualTree(IVisual visual)
{
@@ -67,7 +66,7 @@ namespace Avalonia.Diagnostics
private static string Indent(int indent)
{
- return string.Join(string.Empty, Enumerable.Repeat(" ", Math.Max(indent, 0)));
+ return new string(' ' , Math.Max(indent, 0) * 4);
}
}
}
diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs
index 65195394ab..02dda45e99 100644
--- a/src/Avalonia.Input/Gestures.cs
+++ b/src/Avalonia.Input/Gestures.cs
@@ -18,6 +18,11 @@ namespace Avalonia.Input
RoutingStrategies.Bubble,
typeof(Gestures));
+ public static readonly RoutedEvent RightTappedEvent = RoutedEvent.Register(
+ "RightTapped",
+ RoutingStrategies.Bubble,
+ typeof(Gestures));
+
public static readonly RoutedEvent ScrollGestureEvent =
RoutedEvent.Register(
"ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures));
@@ -46,7 +51,7 @@ namespace Avalonia.Input
}
else if (s_lastPress?.IsAlive == true && e.ClickCount == 2 && s_lastPress.Target == e.Source)
{
- if (!ev.Handled)
+ if (e.MouseButton != MouseButton.Right)
{
e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent));
}
@@ -62,10 +67,8 @@ namespace Avalonia.Input
if (s_lastPress?.IsAlive == true && s_lastPress.Target == e.Source)
{
- if (!ev.Handled)
- {
- ((IInteractive)s_lastPress.Target).RaiseEvent(new RoutedEventArgs(TappedEvent));
- }
+ var et = e.MouseButton != MouseButton.Right ? TappedEvent : RightTappedEvent;
+ ((IInteractive)s_lastPress.Target).RaiseEvent(new RoutedEventArgs(et));
}
}
}
diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs
index b728844e97..6fac90f255 100644
--- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs
+++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs
@@ -19,7 +19,8 @@ namespace Avalonia.Input.Raw
NonClientLeftButtonDown,
TouchBegin,
TouchUpdate,
- TouchEnd
+ TouchEnd,
+ TouchCancel
}
///
diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs
index 7f473bb320..c85f98b04a 100644
--- a/src/Avalonia.Input/TouchDevice.cs
+++ b/src/Avalonia.Input/TouchDevice.cs
@@ -61,6 +61,12 @@ namespace Avalonia.Input
pointer.IsPrimary ? MouseButton.Left : MouseButton.None));
}
}
+ if (args.Type == RawPointerEventType.TouchCancel)
+ {
+ _pointers.Remove(args.TouchPointId);
+ using (pointer)
+ pointer.Capture(null);
+ }
if (args.Type == RawPointerEventType.TouchUpdate)
{
@@ -68,6 +74,8 @@ namespace Avalonia.Input
target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root,
args.Position, ev.Timestamp, new PointerPointProperties(modifiers), modifiers));
}
+
+
}
}
diff --git a/src/Avalonia.Layout/AttachedLayout.cs b/src/Avalonia.Layout/AttachedLayout.cs
new file mode 100644
index 0000000000..5622731a7c
--- /dev/null
+++ b/src/Avalonia.Layout/AttachedLayout.cs
@@ -0,0 +1,106 @@
+// 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.Layout
+{
+ ///
+ /// Represents the base class for an object that sizes and arranges child elements for a host.
+ ///
+ public abstract class AttachedLayout : AvaloniaObject
+ {
+ internal string LayoutId { get; set; }
+
+ ///
+ /// Occurs when the measurement state (layout) has been invalidated.
+ ///
+ public event EventHandler MeasureInvalidated;
+
+ ///
+ /// Occurs when the arrange state (layout) has been invalidated.
+ ///
+ public event EventHandler ArrangeInvalidated;
+
+ ///
+ /// Initializes any per-container state the layout requires when it is attached to an
+ /// container.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ ///
+ /// Container elements that support attached layouts should call this method when a layout
+ /// instance is first assigned. The container is expected to give the attached layout
+ /// instance a way to store and retrieve any per-container state by way of the provided
+ /// context. It is also the responsibility of the container to not reuse the context, or
+ /// otherwise expose the state from one layout to another.
+ ///
+ /// When an attached layout is removed the container should release any reference to the
+ /// layout state it stored.
+ ///
+ /// Override or
+ /// to provide the behavior for
+ /// this method in a derived class.
+ ///
+ public abstract void InitializeForContext(LayoutContext context);
+
+ ///
+ /// Removes any state the layout previously stored on the ILayoutable container.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ public abstract void UninitializeForContext(LayoutContext context);
+
+ ///
+ /// Suggests a DesiredSize for a container element. A container element that supports
+ /// attached layouts should call this method from their own MeasureOverride implementations
+ /// to form a recursive layout update. The attached layout is expected to call the Measure
+ /// for each of the container’s ILayoutable children.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ ///
+ /// The available space that a container can allocate to a child object. A child object can
+ /// request a larger space than what is available; the provided size might be accommodated
+ /// if scrolling or other resize behavior is possible in that particular container.
+ ///
+ ///
+ public abstract Size Measure(LayoutContext context, Size availableSize);
+
+ ///
+ /// Positions child elements and determines a size for a container UIElement. Container
+ /// elements that support attached layouts should call this method from their layout
+ /// override implementations to form a recursive layout update.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ ///
+ /// The final size that the container computes for the child in layout.
+ ///
+ /// The actual size that is used after the element is arranged in layout.
+ public abstract Size Arrange(LayoutContext context, Size finalSize);
+
+ ///
+ /// Invalidates the measurement state (layout) for all ILayoutable containers that reference
+ /// this layout.
+ ///
+ protected void InvalidateMeasure() => MeasureInvalidated?.Invoke(this, EventArgs.Empty);
+
+ ///
+ /// Invalidates the arrange state (layout) for all UIElement containers that reference this
+ /// layout. After the invalidation, the UIElement will have its layout updated, which
+ /// occurs asynchronously.
+ ///
+ protected void InvalidateArrange() => ArrangeInvalidated?.Invoke(this, EventArgs.Empty);
+ }
+}
diff --git a/src/Avalonia.Layout/ElementManager.cs b/src/Avalonia.Layout/ElementManager.cs
new file mode 100644
index 0000000000..1748a3be03
--- /dev/null
+++ b/src/Avalonia.Layout/ElementManager.cs
@@ -0,0 +1,460 @@
+// 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.Generic;
+using System.Collections.Specialized;
+using Avalonia.Layout.Utils;
+
+namespace Avalonia.Layout
+{
+ internal class ElementManager
+ {
+ private readonly List _realizedElements = new List();
+ private readonly List _realizedElementLayoutBounds = new List();
+ private int _firstRealizedDataIndex;
+ private VirtualizingLayoutContext _context;
+
+ private bool IsVirtualizingContext
+ {
+ get
+ {
+ if (_context != null)
+ {
+ var rect = _context.RealizationRect;
+ bool hasInfiniteSize = double.IsInfinity(rect.Height) || double.IsInfinity(rect.Width);
+ return !hasInfiniteSize;
+ }
+ return false;
+ }
+ }
+
+ public void SetContext(VirtualizingLayoutContext virtualContext) => _context = virtualContext;
+
+ public void OnBeginMeasure(ScrollOrientation orientation)
+ {
+ if (_context != null)
+ {
+ if (IsVirtualizingContext)
+ {
+ // We proactively clear elements laid out outside of the realizaton
+ // rect so that they are available for reuse during the current
+ // measure pass.
+ // This is useful during fast panning scenarios in which the realization
+ // window is constantly changing and we want to reuse elements from
+ // the end that's opposite to the panning direction.
+ DiscardElementsOutsideWindow(_context.RealizationRect, orientation);
+ }
+ else
+ {
+ // If we are initialized with a non-virtualizing context, make sure that
+ // we have enough space to hold the bounds for all the elements.
+ int count = _context.ItemCount;
+ if (_realizedElementLayoutBounds.Count != count)
+ {
+ // Make sure there is enough space for the bounds.
+ // Note: We could optimize when the count becomes smaller, but keeping
+ // it always up to date is the simplest option for now.
+ _realizedElementLayoutBounds.Resize(count);
+ }
+ }
+ }
+ }
+
+ public int GetRealizedElementCount()
+ {
+ return IsVirtualizingContext ? _realizedElements.Count : _context.ItemCount;
+ }
+
+ public ILayoutable GetAt(int realizedIndex)
+ {
+ ILayoutable element;
+
+ if (IsVirtualizingContext)
+ {
+ if (_realizedElements[realizedIndex] == null)
+ {
+ // Sentinel. Create the element now since we need it.
+ int dataIndex = GetDataIndexFromRealizedRangeIndex(realizedIndex);
+ element = _context.GetOrCreateElementAt(
+ dataIndex,
+ ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
+ _realizedElements[realizedIndex] = element;
+ }
+ else
+ {
+ element = _realizedElements[realizedIndex];
+ }
+ }
+ else
+ {
+ // realizedIndex and dataIndex are the same (everything is realized)
+ element = _context.GetOrCreateElementAt(
+ realizedIndex,
+ ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
+ }
+
+ return element;
+ }
+
+ public void Add(ILayoutable element, int dataIndex)
+ {
+ if (_realizedElements.Count == 0)
+ {
+ _firstRealizedDataIndex = dataIndex;
+ }
+
+ _realizedElements.Add(element);
+ _realizedElementLayoutBounds.Add(default);
+ }
+
+ public void Insert(int realizedIndex, int dataIndex, ILayoutable element)
+ {
+ if (realizedIndex == 0)
+ {
+ _firstRealizedDataIndex = dataIndex;
+ }
+
+ _realizedElements.Insert(realizedIndex, element);
+
+ // Set bounds to an invalid rect since we do not know it yet.
+ _realizedElementLayoutBounds.Insert(realizedIndex, new Rect(-1, -1, -1, -1));
+ }
+
+ public void ClearRealizedRange(int realizedIndex, int count)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ // Clear from the edges so that ItemsRepeater can optimize on maintaining
+ // realized indices without walking through all the children every time.
+ int index = realizedIndex == 0 ? realizedIndex + i : (realizedIndex + count - 1) - i;
+ var elementRef = _realizedElements[index];
+
+ if (elementRef != null)
+ {
+ _context.RecycleElement(elementRef);
+ }
+ }
+
+ int endIndex = realizedIndex + count;
+ _realizedElements.RemoveRange(realizedIndex, endIndex - realizedIndex);
+ _realizedElementLayoutBounds.RemoveRange(realizedIndex, endIndex - realizedIndex);
+
+ if (realizedIndex == 0)
+ {
+ _firstRealizedDataIndex = _realizedElements.Count == 0 ?
+ -1 : _firstRealizedDataIndex + count;
+ }
+ }
+
+ public void DiscardElementsOutsideWindow(bool forward, int startIndex)
+ {
+ // Remove layout elements that are outside the realized range.
+ if (IsDataIndexRealized(startIndex))
+ {
+ int rangeIndex = GetRealizedRangeIndexFromDataIndex(startIndex);
+
+ if (forward)
+ {
+ ClearRealizedRange(rangeIndex, GetRealizedElementCount() - rangeIndex);
+ }
+ else
+ {
+ ClearRealizedRange(0, rangeIndex + 1);
+ }
+ }
+ }
+
+ public void ClearRealizedRange() => ClearRealizedRange(0, GetRealizedElementCount());
+
+ public Rect GetLayoutBoundsForDataIndex(int dataIndex)
+ {
+ int realizedIndex = GetRealizedRangeIndexFromDataIndex(dataIndex);
+ return _realizedElementLayoutBounds[realizedIndex];
+ }
+
+ public void SetLayoutBoundsForDataIndex(int dataIndex, in Rect bounds)
+ {
+ int realizedIndex = GetRealizedRangeIndexFromDataIndex(dataIndex);
+ _realizedElementLayoutBounds[realizedIndex] = bounds;
+ }
+
+ public Rect GetLayoutBoundsForRealizedIndex(int realizedIndex) => _realizedElementLayoutBounds[realizedIndex];
+
+ public void SetLayoutBoundsForRealizedIndex(int realizedIndex, in Rect bounds)
+ {
+ _realizedElementLayoutBounds[realizedIndex] = bounds;
+ }
+
+ public bool IsDataIndexRealized(int index)
+ {
+ if (IsVirtualizingContext)
+ {
+ int realizedCount = GetRealizedElementCount();
+ return
+ realizedCount > 0 &&
+ GetDataIndexFromRealizedRangeIndex(0) <= index &&
+ GetDataIndexFromRealizedRangeIndex(realizedCount - 1) >= index;
+ }
+ else
+ {
+ // Non virtualized - everything is realized
+ return index >= 0 && index < _context.ItemCount;
+ }
+ }
+
+ public bool IsIndexValidInData(int currentIndex) => currentIndex >= 0 && currentIndex < _context.ItemCount;
+
+ public ILayoutable GetRealizedElement(int dataIndex)
+ {
+ return IsVirtualizingContext ?
+ GetAt(GetRealizedRangeIndexFromDataIndex(dataIndex)) :
+ _context.GetOrCreateElementAt(
+ dataIndex,
+ ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
+ }
+
+ public void EnsureElementRealized(bool forward, int dataIndex, string layoutId)
+ {
+ if (IsDataIndexRealized(dataIndex) == false)
+ {
+ var element = _context.GetOrCreateElementAt(
+ dataIndex,
+ ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
+
+ if (forward)
+ {
+ Add(element, dataIndex);
+ }
+ else
+ {
+ Insert(0, dataIndex, element);
+ }
+ }
+ }
+
+ public bool IsWindowConnected(in Rect window, ScrollOrientation orientation, bool scrollOrientationSameAsFlow)
+ {
+ bool intersects = false;
+
+ if (_realizedElementLayoutBounds.Count > 0)
+ {
+ var firstElementBounds = GetLayoutBoundsForRealizedIndex(0);
+ var lastElementBounds = GetLayoutBoundsForRealizedIndex(GetRealizedElementCount() - 1);
+
+ var effectiveOrientation = scrollOrientationSameAsFlow ?
+ (orientation == ScrollOrientation.Vertical ? ScrollOrientation.Horizontal : ScrollOrientation.Vertical) :
+ orientation;
+
+
+ var windowStart = effectiveOrientation == ScrollOrientation.Vertical ? window.Y : window.X;
+ var windowEnd = effectiveOrientation == ScrollOrientation.Vertical ? window.Y + window.Height : window.X + window.Width;
+ var firstElementStart = effectiveOrientation == ScrollOrientation.Vertical ? firstElementBounds.Y : firstElementBounds.X;
+ var lastElementEnd = effectiveOrientation == ScrollOrientation.Vertical ? lastElementBounds.Y + lastElementBounds.Height : lastElementBounds.X + lastElementBounds.Width;
+
+ intersects =
+ firstElementStart <= windowEnd &&
+ lastElementEnd >= windowStart;
+ }
+
+ return intersects;
+ }
+
+ public void DataSourceChanged(object source, NotifyCollectionChangedEventArgs args)
+ {
+ if (_realizedElements.Count > 0)
+ {
+ switch (args.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ {
+ OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
+ }
+ break;
+
+ case NotifyCollectionChangedAction.Replace:
+ {
+ int oldSize = args.OldItems.Count;
+ int newSize = args.NewItems.Count;
+ int oldStartIndex = args.OldStartingIndex;
+ int newStartIndex = args.NewStartingIndex;
+
+ if (oldSize == newSize &&
+ oldStartIndex == newStartIndex &&
+ IsDataIndexRealized(oldStartIndex) &&
+ IsDataIndexRealized(oldStartIndex + oldSize -1))
+ {
+ // Straight up replace of n items within the realization window.
+ // Removing and adding might causes us to lose the anchor causing us
+ // to throw away all containers and start from scratch.
+ // Instead, we can just clear those items and set the element to
+ // null (sentinel) and let the next measure get new containers for them.
+ var startRealizedIndex = GetRealizedRangeIndexFromDataIndex(oldStartIndex);
+ for (int realizedIndex = startRealizedIndex; realizedIndex < startRealizedIndex + oldSize; realizedIndex++)
+ {
+ var elementRef = _realizedElements[realizedIndex];
+
+ if (elementRef != null)
+ {
+ _context.RecycleElement(elementRef);
+ _realizedElements[realizedIndex] = null;
+ }
+ }
+ }
+ else
+ {
+ OnItemsRemoved(oldStartIndex, oldSize);
+ OnItemsAdded(newStartIndex, newSize);
+ }
+ }
+ break;
+
+ case NotifyCollectionChangedAction.Remove:
+ {
+ OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count);
+ }
+ break;
+
+ case NotifyCollectionChangedAction.Reset:
+ ClearRealizedRange();
+ break;
+
+ case NotifyCollectionChangedAction.Move:
+ throw new NotImplementedException();
+ }
+ }
+ }
+
+ public int GetElementDataIndex(ILayoutable suggestedAnchor)
+ {
+ var it = _realizedElements.IndexOf(suggestedAnchor);
+ return it != -1 ? GetDataIndexFromRealizedRangeIndex(it) : -1;
+ }
+
+ public int GetDataIndexFromRealizedRangeIndex(int rangeIndex)
+ {
+ return IsVirtualizingContext ? rangeIndex + _firstRealizedDataIndex : rangeIndex;
+ }
+
+ private int GetRealizedRangeIndexFromDataIndex(int dataIndex)
+ {
+ return IsVirtualizingContext ? dataIndex - _firstRealizedDataIndex : dataIndex;
+ }
+
+ private void DiscardElementsOutsideWindow(in Rect window, ScrollOrientation orientation)
+ {
+ // The following illustration explains the cutoff indices.
+ // We will clear all the realized elements from both ends
+ // up to the corresponding cutoff index.
+ // '-' means the element is outside the cutoff range.
+ // '*' means the element is inside the cutoff range and will be cleared.
+ //
+ // Window:
+ // |______________________________|
+ // Realization range:
+ // |*****----------------------------------*********|
+ // | |
+ // frontCutoffIndex backCutoffIndex
+ //
+ // Note that we tolerate at most one element outside of the window
+ // because the FlowLayoutAlgorithm.Generate routine stops *after*
+ // it laid out an element outside the realization window.
+ // This is also convenient because it protects the anchor
+ // during a BringIntoView operation during which the anchor may
+ // not be in the realization window (in fact, the realization window
+ // might be empty if the BringIntoView is issued before the first
+ // layout pass).
+
+ int realizedRangeSize = GetRealizedElementCount();
+ int frontCutoffIndex = -1;
+ int backCutoffIndex = realizedRangeSize;
+
+ for (int i = 0;
+ i= 0 &&
+ !Intersects(window, _realizedElementLayoutBounds[i], orientation);
+ --i)
+ {
+ --backCutoffIndex;
+ }
+
+ if (backCutoffIndex 0)
+ {
+ ClearRealizedRange(0, Math.Min(frontCutoffIndex, GetRealizedElementCount()));
+ }
+ }
+
+ private static bool Intersects(in Rect lhs, in Rect rhs, ScrollOrientation orientation)
+ {
+ var lhsStart = orientation == ScrollOrientation.Vertical ? lhs.Y : lhs.X;
+ var lhsEnd = orientation == ScrollOrientation.Vertical ? lhs.Y + lhs.Height : lhs.X + lhs.Width;
+ var rhsStart = orientation == ScrollOrientation.Vertical ? rhs.Y : rhs.X;
+ var rhsEnd = orientation == ScrollOrientation.Vertical ? rhs.Y + rhs.Height : rhs.X + rhs.Width;
+
+ return lhsEnd >= rhsStart && lhsStart <= rhsEnd;
+ }
+
+ private void OnItemsAdded(int index, int count)
+ {
+ // Using the old indices here (before it was updated by the collection change)
+ // if the insert data index is between the first and last realized data index, we need
+ // to insert items.
+ int lastRealizedDataIndex = _firstRealizedDataIndex + GetRealizedElementCount() - 1;
+ int newStartingIndex = index;
+ if (newStartingIndex > _firstRealizedDataIndex &&
+ newStartingIndex <= lastRealizedDataIndex)
+ {
+ // Inserted within the realized range
+ int insertRangeStartIndex = newStartingIndex - _firstRealizedDataIndex;
+ for (int i = 0; i < count; i++)
+ {
+ // Insert null (sentinel) here instead of an element, that way we dont
+ // end up creating a lot of elements only to be thrown out in the next layout.
+ int insertRangeIndex = insertRangeStartIndex + i;
+ int dataIndex = newStartingIndex + i;
+ // This is to keep the contiguousness of the mapping
+ Insert(insertRangeIndex, dataIndex, null);
+ }
+ }
+ else if (index <= _firstRealizedDataIndex)
+ {
+ // Items were inserted before the realized range.
+ // We need to update m_firstRealizedDataIndex;
+ _firstRealizedDataIndex += count;
+ }
+ }
+
+ private void OnItemsRemoved(int index, int count)
+ {
+ int lastRealizedDataIndex = _firstRealizedDataIndex + _realizedElements.Count - 1;
+ int startIndex = Math.Max(_firstRealizedDataIndex, index);
+ int endIndex = Math.Min(lastRealizedDataIndex, index + count - 1);
+ bool removeAffectsFirstRealizedDataIndex = (index <= _firstRealizedDataIndex);
+
+ if (endIndex >= startIndex)
+ {
+ ClearRealizedRange(GetRealizedRangeIndexFromDataIndex(startIndex), endIndex - startIndex + 1);
+ }
+
+ if (removeAffectsFirstRealizedDataIndex &&
+ _firstRealizedDataIndex != -1)
+ {
+ _firstRealizedDataIndex -= count;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs
new file mode 100644
index 0000000000..615ce725bd
--- /dev/null
+++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs
@@ -0,0 +1,712 @@
+// 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.Specialized;
+
+namespace Avalonia.Layout
+{
+ internal class FlowLayoutAlgorithm
+ {
+ private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures();
+ private readonly ElementManager _elementManager = new ElementManager();
+ private Size _lastAvailableSize;
+ private double _lastItemSpacing;
+ private bool _collectionChangePending;
+ private VirtualizingLayoutContext _context;
+ private IFlowLayoutAlgorithmDelegates _algorithmCallbacks;
+ private Rect _lastExtent;
+ private int _firstRealizedDataIndexInsideRealizationWindow = -1;
+ private int _lastRealizedDataIndexInsideRealizationWindow = -1;
+
+ // If the scroll orientation is the same as the folow orientation
+ // we will only have one line since we will never wrap. In that case
+ // we do not want to align the line. We could potentially switch the
+ // meaning of line alignment in this case, but I'll hold off on that
+ // feature until someone asks for it - This is not a common scenario
+ // anyway.
+ private bool _scrollOrientationSameAsFlow;
+
+ public Rect LastExtent => _lastExtent;
+
+ private bool IsVirtualizingContext
+ {
+ get
+ {
+ if (_context != null)
+ {
+ var rect = _context.RealizationRect;
+ bool hasInfiniteSize = double.IsInfinity(rect.Height) || double.IsInfinity(rect.Width);
+ return !hasInfiniteSize;
+ }
+ return false;
+ }
+ }
+
+ private Rect RealizationRect => IsVirtualizingContext ? _context.RealizationRect : new Rect(Size.Infinity);
+
+ public void InitializeForContext(VirtualizingLayoutContext context, IFlowLayoutAlgorithmDelegates callbacks)
+ {
+ _algorithmCallbacks = callbacks;
+ _context = context;
+ _elementManager.SetContext(context);
+ }
+
+ public void UninitializeForContext(VirtualizingLayoutContext context)
+ {
+ if (IsVirtualizingContext)
+ {
+ // This layout is about to be detached. Let go of all elements
+ // being held and remove the layout state from the context.
+ _elementManager.ClearRealizedRange();
+ }
+
+ context.LayoutState = null;
+ }
+
+ public Size Measure(
+ Size availableSize,
+ VirtualizingLayoutContext context,
+ bool isWrapping,
+ double minItemSpacing,
+ double lineSpacing,
+ ScrollOrientation orientation,
+ string layoutId)
+ {
+ _orientation.ScrollOrientation = orientation;
+
+ // If minor size is infinity, there is only one line and no need to align that line.
+ _scrollOrientationSameAsFlow = double.IsInfinity(_orientation.Minor(availableSize));
+ var realizationRect = RealizationRect;
+
+ var suggestedAnchorIndex = _context.RecommendedAnchorIndex;
+ if (_elementManager.IsIndexValidInData(suggestedAnchorIndex))
+ {
+ var anchorRealized = _elementManager.IsDataIndexRealized(suggestedAnchorIndex);
+ if (!anchorRealized)
+ {
+ MakeAnchor(_context, suggestedAnchorIndex, availableSize);
+ }
+ }
+
+ _elementManager.OnBeginMeasure(orientation);
+
+ int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId);
+ Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId);
+ Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId);
+ if (isWrapping && IsReflowRequired())
+ {
+ var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
+ _orientation.SetMinorStart(ref firstElementBounds, 0);
+ _elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds);
+ Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, layoutId);
+ }
+
+ RaiseLineArranged();
+ _collectionChangePending = false;
+ _lastExtent = EstimateExtent(availableSize, layoutId);
+ SetLayoutOrigin();
+
+ return new Size(_lastExtent.Width, _lastExtent.Height);
+ }
+
+ public Size Arrange(
+ Size finalSize,
+ VirtualizingLayoutContext context,
+ LineAlignment lineAlignment,
+ string layoutId)
+ {
+ ArrangeVirtualizingLayout(finalSize, lineAlignment, layoutId);
+
+ return new Size(
+ Math.Max(finalSize.Width, _lastExtent.Width),
+ Math.Max(finalSize.Height, _lastExtent.Height));
+ }
+
+ public void OnItemsSourceChanged(
+ object source,
+ NotifyCollectionChangedEventArgs args,
+ VirtualizingLayoutContext context)
+ {
+ _elementManager.DataSourceChanged(source, args);
+ _collectionChangePending = true;
+ }
+
+ public Size MeasureElement(
+ ILayoutable element,
+ int index,
+ Size availableSize,
+ VirtualizingLayoutContext context)
+ {
+ var measureSize = _algorithmCallbacks.Algorithm_GetMeasureSize(index, availableSize, context);
+ element.Measure(measureSize);
+ var provisionalArrangeSize = _algorithmCallbacks.Algorithm_GetProvisionalArrangeSize(index, measureSize, element.DesiredSize, context);
+ _algorithmCallbacks.Algorithm_OnElementMeasured(element, index, availableSize, measureSize, element.DesiredSize, provisionalArrangeSize, context);
+
+ return provisionalArrangeSize;
+ }
+
+ private int GetAnchorIndex(
+ Size availableSize,
+ bool isWrapping,
+ double minItemSpacing,
+ string layoutId)
+ {
+ int anchorIndex = -1;
+ var anchorPosition= new Point();
+ var context = _context;
+
+ if (!IsVirtualizingContext)
+ {
+ // Non virtualizing host, start generating from the element 0
+ anchorIndex = context.ItemCount > 0 ? 0 : -1;
+ }
+ else
+ {
+ bool isRealizationWindowConnected = _elementManager.IsWindowConnected(RealizationRect, _orientation.ScrollOrientation, _scrollOrientationSameAsFlow);
+ // Item spacing and size in non-virtualizing direction change can cause elements to reflow
+ // and get a new column position. In that case we need the anchor to be positioned in the
+ // correct column.
+ bool needAnchorColumnRevaluation = isWrapping && (
+ _orientation.Minor(_lastAvailableSize) != _orientation.Minor(availableSize) ||
+ _lastItemSpacing != minItemSpacing ||
+ _collectionChangePending);
+
+ var suggestedAnchorIndex = _context.RecommendedAnchorIndex;
+
+ var isAnchorSuggestionValid = suggestedAnchorIndex >= 0 &&
+ _elementManager.IsDataIndexRealized(suggestedAnchorIndex);
+
+ if (isAnchorSuggestionValid)
+ {
+ anchorIndex = _algorithmCallbacks.Algorithm_GetAnchorForTargetElement(
+ suggestedAnchorIndex,
+ availableSize,
+ context).Index;
+
+ if (_elementManager.IsDataIndexRealized(anchorIndex))
+ {
+ var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex);
+ if (needAnchorColumnRevaluation)
+ {
+ // We were provided a valid anchor, but its position might be incorrect because for example it is in
+ // the wrong column. We do know that the anchor is the first element in the row, so we can force the minor position
+ // to start at 0.
+ anchorPosition = _orientation.MinorMajorPoint(0, _orientation.MajorStart(anchorBounds));
+ }
+ else
+ {
+ anchorPosition = new Point(anchorBounds.X, anchorBounds.Y);
+ }
+ }
+ else
+ {
+ // It is possible to end up in a situation during a collection change where GetAnchorForTargetElement returns an index
+ // which is not in the realized range. Eg. insert one item at index 0 for a grid layout.
+ // SuggestedAnchor will be 1 (used to be 0) and GetAnchorForTargetElement will return 0 (left most item in row). However 0 is not in the
+ // realized range yet. In this case we realize the gap between the target anchor and the suggested anchor.
+ int firstRealizedDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);
+
+ for (int i = firstRealizedDataIndex - 1; i >= anchorIndex; --i)
+ {
+ _elementManager.EnsureElementRealized(false /*forward*/, i, layoutId);
+ }
+
+ var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(suggestedAnchorIndex);
+ anchorPosition = _orientation.MinorMajorPoint(0, _orientation.MajorStart(anchorBounds));
+ }
+ }
+ else if (needAnchorColumnRevaluation || !isRealizationWindowConnected)
+ {
+ // The anchor is based on the realization window because a connected ItemsRepeater might intersect the realization window
+ // but not the visible window. In that situation, we still need to produce a valid anchor.
+ var anchorInfo = _algorithmCallbacks.Algorithm_GetAnchorForRealizationRect(availableSize, context);
+ anchorIndex = anchorInfo.Index;
+ anchorPosition = _orientation.MinorMajorPoint(0, anchorInfo.Offset);
+ }
+ else
+ {
+ // No suggestion - just pick first in realized range
+ anchorIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);
+ var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
+ anchorPosition = new Point(firstElementBounds.X, firstElementBounds.Y);
+ }
+ }
+
+ _firstRealizedDataIndexInsideRealizationWindow = _lastRealizedDataIndexInsideRealizationWindow = anchorIndex;
+ if (_elementManager.IsIndexValidInData(anchorIndex))
+ {
+ if (!_elementManager.IsDataIndexRealized(anchorIndex))
+ {
+ // Disconnected, throw everything and create new anchor
+ _elementManager.ClearRealizedRange();
+
+ var anchor = _context.GetOrCreateElementAt(anchorIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
+ _elementManager.Add(anchor, anchorIndex);
+ }
+
+ var anchorElement = _elementManager.GetRealizedElement(anchorIndex);
+ var desiredSize = MeasureElement(anchorElement, anchorIndex, availableSize, _context);
+ var layoutBounds = new Rect(anchorPosition.X, anchorPosition.Y, desiredSize.Width, desiredSize.Height);
+ _elementManager.SetLayoutBoundsForDataIndex(anchorIndex, layoutBounds);
+ }
+ else
+ {
+ _elementManager.ClearRealizedRange();
+ }
+
+ // TODO: Perhaps we can track changes in the property setter
+ _lastAvailableSize = availableSize;
+ _lastItemSpacing = minItemSpacing;
+
+ return anchorIndex;
+ }
+
+ private void Generate(
+ GenerateDirection direction,
+ int anchorIndex,
+ Size availableSize,
+ double minItemSpacing,
+ double lineSpacing,
+ string layoutId)
+ {
+ if (anchorIndex != -1)
+ {
+ int step = (direction == GenerateDirection.Forward) ? 1 : -1;
+ int previousIndex = anchorIndex;
+ int currentIndex = anchorIndex + step;
+ var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex);
+ var lineOffset = _orientation.MajorStart(anchorBounds);
+ var lineMajorSize = _orientation.MajorSize(anchorBounds);
+ int countInLine = 1;
+ int count = 0;
+ bool lineNeedsReposition = false;
+
+ while (_elementManager.IsIndexValidInData(currentIndex) &&
+ ShouldContinueFillingUpSpace(previousIndex, direction))
+ {
+ // Ensure layout element.
+ _elementManager.EnsureElementRealized(direction == GenerateDirection.Forward, currentIndex, layoutId);
+ var currentElement = _elementManager.GetRealizedElement(currentIndex);
+ var desiredSize = MeasureElement(currentElement, currentIndex, availableSize, _context);
+ ++count;
+
+ // Lay it out.
+ var previousElement = _elementManager.GetRealizedElement(previousIndex);
+ var currentBounds = new Rect(0, 0, desiredSize.Width, desiredSize.Height);
+ var previousElementBounds = _elementManager.GetLayoutBoundsForDataIndex(previousIndex);
+
+ if (direction == GenerateDirection.Forward)
+ {
+ double remainingSpace = _orientation.Minor(availableSize) - (_orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing + _orientation.Minor(desiredSize));
+ if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace))
+ {
+ // No more space in this row. wrap to next row.
+ _orientation.SetMinorStart(ref currentBounds, 0);
+ _orientation.SetMajorStart(ref currentBounds, _orientation.MajorStart(previousElementBounds) + lineMajorSize + lineSpacing);
+
+ if (lineNeedsReposition)
+ {
+ // reposition the previous line (countInLine items)
+ for (int i = 0; i < countInLine; i++)
+ {
+ var dataIndex = currentIndex - 1 - i;
+ var bounds = _elementManager.GetLayoutBoundsForDataIndex(dataIndex);
+ _orientation.SetMajorSize(ref bounds, lineMajorSize);
+ _elementManager.SetLayoutBoundsForDataIndex(dataIndex, bounds);
+ }
+ }
+
+ // Setup for next line.
+ lineMajorSize = _orientation.MajorSize(currentBounds);
+ lineOffset = _orientation.MajorStart(currentBounds);
+ lineNeedsReposition = false;
+ countInLine = 1;
+ }
+ else
+ {
+ // More space is available in this row.
+ _orientation.SetMinorStart(ref currentBounds, _orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing);
+ _orientation.SetMajorStart(ref currentBounds, lineOffset);
+ lineMajorSize = Math.Max(lineMajorSize, _orientation.MajorSize(currentBounds));
+ lineNeedsReposition = _orientation.MajorSize(previousElementBounds) != _orientation.MajorSize(currentBounds);
+ countInLine++;
+ }
+ }
+ else
+ {
+ // Backward
+ double remainingSpace = _orientation.MinorStart(previousElementBounds) - (_orientation.Minor(desiredSize) + minItemSpacing);
+ if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace))
+ {
+ // Does not fit, wrap to the previous row
+ var availableSizeMinor = _orientation.Minor(availableSize);
+
+ _orientation.SetMinorStart(ref currentBounds, !double.IsInfinity(availableSizeMinor) ? availableSizeMinor - _orientation.Minor(desiredSize) : 0);
+ _orientation.SetMajorStart(ref currentBounds, lineOffset - _orientation.Major(desiredSize) - lineSpacing);
+
+ if (lineNeedsReposition)
+ {
+ var previousLineOffset = _orientation.MajorStart(_elementManager.GetLayoutBoundsForDataIndex(currentIndex + countInLine + 1));
+ // reposition the previous line (countInLine items)
+ for (int i = 0; i < countInLine; i++)
+ {
+ var dataIndex = currentIndex + 1 + i;
+ if (dataIndex != anchorIndex)
+ {
+ var bounds = _elementManager.GetLayoutBoundsForDataIndex(dataIndex);
+ _orientation.SetMajorStart(ref bounds, previousLineOffset - lineMajorSize - lineSpacing);
+ _orientation.SetMajorSize(ref bounds, lineMajorSize);
+ _elementManager.SetLayoutBoundsForDataIndex(dataIndex, bounds);
+ }
+ }
+ }
+
+ // Setup for next line.
+ lineMajorSize = _orientation.MajorSize(currentBounds);
+ lineOffset = _orientation.MajorStart(currentBounds);
+ lineNeedsReposition = false;
+ countInLine = 1;
+ }
+ else
+ {
+ // Fits in this row. put it in the previous position
+ _orientation.SetMinorStart(ref currentBounds, _orientation.MinorStart(previousElementBounds) - _orientation.Minor(desiredSize) - minItemSpacing);
+ _orientation.SetMajorStart(ref currentBounds, lineOffset);
+ lineMajorSize = Math.Max(lineMajorSize, _orientation.MajorSize(currentBounds));
+ lineNeedsReposition = _orientation.MajorSize(previousElementBounds) != _orientation.MajorSize(currentBounds);
+ countInLine++;
+ }
+ }
+
+ _elementManager.SetLayoutBoundsForDataIndex(currentIndex, currentBounds);
+ previousIndex = currentIndex;
+ currentIndex += step;
+ }
+
+ // If we did not reach the top or bottom of the extent, we realized one
+ // extra item before we knew we were outside the realization window. Do not
+ // account for that element in the indicies inside the realization window.
+ if (count > 0)
+ {
+ if (direction == GenerateDirection.Forward)
+ {
+ int dataCount = _context.ItemCount;
+ _lastRealizedDataIndexInsideRealizationWindow = previousIndex == dataCount - 1 ? dataCount - 1 : previousIndex - 1;
+ _lastRealizedDataIndexInsideRealizationWindow = Math.Max(0, _lastRealizedDataIndexInsideRealizationWindow);
+ }
+ else
+ {
+ int dataCount = _context.ItemCount;
+ _firstRealizedDataIndexInsideRealizationWindow = previousIndex == 0 ? 0 : previousIndex + 1;
+ _firstRealizedDataIndexInsideRealizationWindow = Math.Min(dataCount - 1, _firstRealizedDataIndexInsideRealizationWindow);
+ }
+ }
+
+ _elementManager.DiscardElementsOutsideWindow(direction == GenerateDirection.Forward, currentIndex);
+ }
+ }
+
+ private void MakeAnchor(
+ VirtualizingLayoutContext context,
+ int index,
+ Size availableSize)
+ {
+ _elementManager.ClearRealizedRange();
+ // FlowLayout requires that the anchor is the first element in the row.
+ var internalAnchor = _algorithmCallbacks.Algorithm_GetAnchorForTargetElement(index, availableSize, context);
+
+ // No need to set the position of the anchor.
+ // (0,0) is fine for now since the extent can
+ // grow in any direction.
+ for (int dataIndex = internalAnchor.Index; dataIndex < index + 1; ++dataIndex)
+ {
+ var element = context.GetOrCreateElementAt(dataIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
+ element.Measure(_algorithmCallbacks.Algorithm_GetMeasureSize(dataIndex, availableSize, context));
+ _elementManager.Add(element, dataIndex);
+ }
+ }
+
+ private bool IsReflowRequired()
+ {
+ // If first element is realized and is not at the very beginning we need to reflow.
+ return
+ _elementManager.GetRealizedElementCount() > 0 &&
+ _elementManager.GetDataIndexFromRealizedRangeIndex(0) == 0 &&
+ _orientation.MinorStart(_elementManager.GetLayoutBoundsForRealizedIndex(0)) != 0;
+ }
+
+ private bool ShouldContinueFillingUpSpace(
+ int index,
+ GenerateDirection direction)
+ {
+ bool shouldContinue = false;
+ if (!IsVirtualizingContext)
+ {
+ shouldContinue = true;
+ }
+ else
+ {
+ var realizationRect = _context.RealizationRect;
+ var elementBounds = _elementManager.GetLayoutBoundsForDataIndex(index);
+
+ var elementMajorStart = _orientation.MajorStart(elementBounds);
+ var elementMajorEnd = _orientation.MajorEnd(elementBounds);
+ var rectMajorStart = _orientation.MajorStart(realizationRect);
+ var rectMajorEnd = _orientation.MajorEnd(realizationRect);
+
+ var elementMinorStart = _orientation.MinorStart(elementBounds);
+ var elementMinorEnd = _orientation.MinorEnd(elementBounds);
+ var rectMinorStart = _orientation.MinorStart(realizationRect);
+ var rectMinorEnd = _orientation.MinorEnd(realizationRect);
+
+ // Ensure that both minor and major directions are taken into consideration so that if the scrolling direction
+ // is the same as the flow direction we still stop at the end of the viewport rectangle.
+ shouldContinue =
+ (direction == GenerateDirection.Forward && elementMajorStart < rectMajorEnd && elementMinorStart < rectMinorEnd) ||
+ (direction == GenerateDirection.Backward && elementMajorEnd > rectMajorStart && elementMinorEnd > rectMinorStart);
+ }
+
+ return shouldContinue;
+ }
+
+ private Rect EstimateExtent(Size availableSize, string layoutId)
+ {
+ ILayoutable firstRealizedElement = null;
+ Rect firstBounds = new Rect();
+ ILayoutable lastRealizedElement = null;
+ Rect lastBounds = new Rect();
+ int firstDataIndex = -1;
+ int lastDataIndex = -1;
+
+ if (_elementManager.GetRealizedElementCount() > 0)
+ {
+ firstRealizedElement = _elementManager.GetAt(0);
+ firstBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
+ firstDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);;
+
+ int last = _elementManager.GetRealizedElementCount() - 1;
+ lastRealizedElement = _elementManager.GetAt(last);
+ lastDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(last);
+ lastBounds = _elementManager.GetLayoutBoundsForRealizedIndex(last);
+ }
+
+ Rect extent = _algorithmCallbacks.Algorithm_GetExtent(
+ availableSize,
+ _context,
+ firstRealizedElement,
+ firstDataIndex,
+ firstBounds,
+ lastRealizedElement,
+ lastDataIndex,
+ lastBounds);
+
+ return extent;
+ }
+
+ private void RaiseLineArranged()
+ {
+ var realizationRect = RealizationRect;
+ if (realizationRect.Width != 0.0f || realizationRect.Height != 0.0f)
+ {
+ int realizedElementCount = _elementManager.GetRealizedElementCount();
+ if (realizedElementCount > 0)
+ {
+ int countInLine = 0;
+ var previousElementBounds = _elementManager.GetLayoutBoundsForDataIndex(_firstRealizedDataIndexInsideRealizationWindow);
+ var currentLineOffset = _orientation.MajorStart(previousElementBounds);
+ var currentLineSize = _orientation.MajorSize(previousElementBounds);
+ for (int currentDataIndex = _firstRealizedDataIndexInsideRealizationWindow; currentDataIndex <= _lastRealizedDataIndexInsideRealizationWindow; currentDataIndex++)
+ {
+ var currentBounds = _elementManager.GetLayoutBoundsForDataIndex(currentDataIndex);
+ if (_orientation.MajorStart(currentBounds) != currentLineOffset)
+ {
+ // Staring a new line
+ _algorithmCallbacks.Algorithm_OnLineArranged(currentDataIndex - countInLine, countInLine, currentLineSize, _context);
+ countInLine = 0;
+ currentLineOffset = _orientation.MajorStart(currentBounds);
+ currentLineSize = 0;
+ }
+
+ currentLineSize = Math.Max(currentLineSize, _orientation.MajorSize(currentBounds));
+ countInLine++;
+ previousElementBounds = currentBounds;
+ }
+
+ // Raise for the last line.
+ _algorithmCallbacks.Algorithm_OnLineArranged(_lastRealizedDataIndexInsideRealizationWindow - countInLine + 1, countInLine, currentLineSize, _context);
+ }
+ }
+ }
+
+ private void ArrangeVirtualizingLayout(
+ Size finalSize,
+ LineAlignment lineAlignment,
+ string layoutId)
+ {
+ // Walk through the realized elements one line at a time and
+ // align them, Then call element.Arrange with the arranged bounds.
+ int realizedElementCount = _elementManager.GetRealizedElementCount();
+ if (realizedElementCount > 0)
+ {
+ var countInLine = 1;
+ var previousElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
+ var currentLineOffset = _orientation.MajorStart(previousElementBounds);
+ var spaceAtLineStart = _orientation.MinorStart(previousElementBounds);
+ var spaceAtLineEnd = 0.0;
+ var currentLineSize = _orientation.MajorSize(previousElementBounds);
+ for (int i = 1; i < realizedElementCount; i++)
+ {
+ var currentBounds = _elementManager.GetLayoutBoundsForRealizedIndex(i);
+ if (_orientation.MajorStart(currentBounds) != currentLineOffset)
+ {
+ spaceAtLineEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds);
+ PerformLineAlignment(i - countInLine, countInLine, spaceAtLineStart, spaceAtLineEnd, currentLineSize, lineAlignment, layoutId);
+ spaceAtLineStart = _orientation.MinorStart(currentBounds);
+ countInLine = 0;
+ currentLineOffset = _orientation.MajorStart(currentBounds);
+ currentLineSize = 0;
+ }
+
+ countInLine++; // for current element
+ currentLineSize = Math.Max(currentLineSize, _orientation.MajorSize(currentBounds));
+ previousElementBounds = currentBounds;
+ }
+
+ // Last line - potentially have a property to customize
+ // aligning the last line or not.
+ if (countInLine > 0)
+ {
+ var spaceAtEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds);
+ PerformLineAlignment(realizedElementCount - countInLine, countInLine, spaceAtLineStart, spaceAtEnd, currentLineSize, lineAlignment, layoutId);
+ }
+ }
+ }
+
+ // Align elements within a line. Note that this does not modify LayoutBounds. So if we get
+ // repeated measures, the LayoutBounds remain the same in each layout.
+ private void PerformLineAlignment(
+ int lineStartIndex,
+ int countInLine,
+ double spaceAtLineStart,
+ double spaceAtLineEnd,
+ double lineSize,
+ LineAlignment lineAlignment,
+ string layoutId)
+ {
+ for (int rangeIndex = lineStartIndex; rangeIndex < lineStartIndex + countInLine; ++rangeIndex)
+ {
+ var bounds = _elementManager.GetLayoutBoundsForRealizedIndex(rangeIndex);
+ _orientation.SetMajorSize(ref bounds, lineSize);
+
+ if (!_scrollOrientationSameAsFlow)
+ {
+ // Note: Space at start could potentially be negative
+ if (spaceAtLineStart != 0 || spaceAtLineEnd != 0)
+ {
+ var totalSpace = spaceAtLineStart + spaceAtLineEnd;
+ var minorStart = _orientation.MinorStart(bounds);
+ switch (lineAlignment)
+ {
+ case LineAlignment.Start:
+ {
+ _orientation.SetMinorStart(ref bounds, minorStart - spaceAtLineStart);
+ break;
+ }
+
+ case LineAlignment.End:
+ {
+ _orientation.SetMinorStart(ref bounds, minorStart + spaceAtLineEnd);
+ break;
+ }
+
+ case LineAlignment.Center:
+ {
+ _orientation.SetMinorStart(ref bounds, (minorStart - spaceAtLineStart) + (totalSpace / 2));
+ break;
+ }
+
+ case LineAlignment.SpaceAround:
+ {
+ var interItemSpace = countInLine >= 1 ? totalSpace / (countInLine * 2) : 0;
+ _orientation.SetMinorStart(
+ ref bounds,
+ (minorStart - spaceAtLineStart) + (interItemSpace * ((rangeIndex - lineStartIndex + 1) * 2 - 1)));
+ break;
+ }
+
+ case LineAlignment.SpaceBetween:
+ {
+ var interItemSpace = countInLine > 1 ? totalSpace / (countInLine - 1) : 0;
+ _orientation.SetMinorStart(
+ ref bounds,
+ (minorStart - spaceAtLineStart) + (interItemSpace * (rangeIndex - lineStartIndex)));
+ break;
+ }
+
+ case LineAlignment.SpaceEvenly:
+ {
+ var interItemSpace = countInLine >= 1 ? totalSpace / (countInLine + 1) : 0;
+ _orientation.SetMinorStart(
+ ref bounds,
+ (minorStart - spaceAtLineStart) + (interItemSpace * (rangeIndex - lineStartIndex + 1)));
+ break;
+ }
+ }
+ }
+ }
+
+ bounds = bounds.Translate(-_lastExtent.Position);
+ var element = _elementManager.GetAt(rangeIndex);
+ element.Arrange(bounds);
+ }
+ }
+
+ private void SetLayoutOrigin()
+ {
+ if (IsVirtualizingContext)
+ {
+ _context.LayoutOrigin = new Point(_lastExtent.X, _lastExtent.Y);
+ }
+ }
+
+ public ILayoutable GetElementIfRealized(int dataIndex)
+ {
+ if (_elementManager.IsDataIndexRealized(dataIndex))
+ {
+ return _elementManager.GetRealizedElement(dataIndex);
+ }
+
+ return null;
+ }
+
+ public bool TryAddElement0(ILayoutable element)
+ {
+ if (_elementManager.GetRealizedElementCount() == 0)
+ {
+ _elementManager.Add(element, 0);
+ return true;
+ }
+
+ return false;
+ }
+
+ public enum LineAlignment
+ {
+ Start,
+ Center,
+ End,
+ SpaceAround,
+ SpaceBetween,
+ SpaceEvenly,
+ }
+
+ private enum GenerateDirection
+ {
+ Forward,
+ Backward,
+ }
+ }
+}
diff --git a/src/Avalonia.Layout/IFlowLayoutAlgorithmDelegates.cs b/src/Avalonia.Layout/IFlowLayoutAlgorithmDelegates.cs
new file mode 100644
index 0000000000..907a3adf0f
--- /dev/null
+++ b/src/Avalonia.Layout/IFlowLayoutAlgorithmDelegates.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.
+
+namespace Avalonia.Layout
+{
+ internal struct FlowLayoutAnchorInfo
+ {
+ public int Index { get; set; }
+ public double Offset { get; set; }
+ }
+
+ internal interface IFlowLayoutAlgorithmDelegates
+ {
+ Size Algorithm_GetMeasureSize(int index, Size availableSize, VirtualizingLayoutContext context);
+ Size Algorithm_GetProvisionalArrangeSize(int index, Size measureSize, Size desiredSize, VirtualizingLayoutContext context);
+ bool Algorithm_ShouldBreakLine(int index, double remainingSpace);
+ FlowLayoutAnchorInfo Algorithm_GetAnchorForRealizationRect(Size availableSize, VirtualizingLayoutContext context);
+ FlowLayoutAnchorInfo Algorithm_GetAnchorForTargetElement(int targetIndex, Size availableSize, VirtualizingLayoutContext context);
+ Rect Algorithm_GetExtent(
+ Size availableSize,
+ VirtualizingLayoutContext context,
+ ILayoutable firstRealized,
+ int firstRealizedItemIndex,
+ Rect firstRealizedLayoutBounds,
+ ILayoutable lastRealized,
+ int lastRealizedItemIndex,
+ Rect lastRealizedLayoutBounds);
+ void Algorithm_OnElementMeasured(
+ ILayoutable element,
+ int index,
+ Size availableSize,
+ Size measureSize,
+ Size desiredSize,
+ Size provisionalArrangeSize,
+ VirtualizingLayoutContext context);
+ void Algorithm_OnLineArranged(
+ int startIndex,
+ int countInLine,
+ double lineSize,
+ VirtualizingLayoutContext context);
+ }
+}
diff --git a/src/Avalonia.Layout/LayoutContext.cs b/src/Avalonia.Layout/LayoutContext.cs
new file mode 100644
index 0000000000..45a8048ea2
--- /dev/null
+++ b/src/Avalonia.Layout/LayoutContext.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.
+
+namespace Avalonia.Layout
+{
+ ///
+ /// Represents the base class for an object that facilitates communication between an attached
+ /// layout and its host container.
+ ///
+ public class LayoutContext : AvaloniaObject
+ {
+ ///
+ /// Gets or sets an object that represents the state of a layout.
+ ///
+ public object LayoutState { get; set; }
+
+ ///
+ /// Implements the behavior of in a derived or custom LayoutContext.
+ ///
+ protected virtual object LayoutStateCore { get; set; }
+ }
+}
diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs
index 662f48ec44..bd248d6d44 100644
--- a/src/Avalonia.Layout/Layoutable.cs
+++ b/src/Avalonia.Layout/Layoutable.cs
@@ -534,6 +534,9 @@ namespace Avalonia.Layout
height = Math.Min(height, MaxHeight);
height = Math.Max(height, MinHeight);
+ width = Math.Min(width, availableSize.Width);
+ height = Math.Min(height, availableSize.Height);
+
if (UseLayoutRounding)
{
var scale = GetLayoutScale();
diff --git a/src/Avalonia.Layout/NonVirtualizingLayout.cs b/src/Avalonia.Layout/NonVirtualizingLayout.cs
new file mode 100644
index 0000000000..fba91e66c7
--- /dev/null
+++ b/src/Avalonia.Layout/NonVirtualizingLayout.cs
@@ -0,0 +1,103 @@
+// 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.Layout
+{
+ ///
+ /// Represents the base class for an object that sizes and arranges child elements for a host
+ /// and and does not support virtualization.
+ ///
+ ///
+ /// NonVirtualizingLayout is the base class for layouts that do not support virtualization. You
+ /// can inherit from it to create your own layout.
+ ///
+ /// A non-virtualizing layout can measure and arrange child elements.
+ ///
+ public abstract class NonVirtualizingLayout : AttachedLayout
+ {
+ ///
+ public sealed override void InitializeForContext(LayoutContext context)
+ {
+ InitializeForContextCore((VirtualizingLayoutContext)context);
+ }
+
+ ///
+ public sealed override void UninitializeForContext(LayoutContext context)
+ {
+ UninitializeForContextCore((VirtualizingLayoutContext)context);
+ }
+
+ ///
+ public sealed override Size Measure(LayoutContext context, Size availableSize)
+ {
+ return MeasureOverride((VirtualizingLayoutContext)context, availableSize);
+ }
+
+ ///
+ public sealed override Size Arrange(LayoutContext context, Size finalSize)
+ {
+ return ArrangeOverride((VirtualizingLayoutContext)context, finalSize);
+ }
+
+ ///
+ /// When overridden in a derived class, initializes any per-container state the layout
+ /// requires when it is attached to an ILayoutable container.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ protected virtual void InitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ }
+
+ ///
+ /// When overridden in a derived class, removes any state the layout previously stored on
+ /// the ILayoutable container.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ }
+
+ ///
+ /// Provides the behavior for the "Measure" pass of the layout cycle. Classes can override
+ /// this method to define their own "Measure" pass behavior.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ ///
+ /// The available size that this object can give to child objects. Infinity can be
+ /// specified as a value to indicate that the object will size to whatever content is
+ /// available.
+ ///
+ ///
+ /// The size that this object determines it needs during layout, based on its calculations
+ /// of the allocated sizes for child objects or based on other considerations such as a
+ /// fixed container size.
+ ///
+ protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize);
+
+ ///
+ /// When implemented in a derived class, provides the behavior for the "Arrange" pass of
+ /// layout. Classes can override this method to define their own "Arrange" pass behavior.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ ///
+ /// The final area within the container that this object should use to arrange itself and
+ /// its children.
+ ///
+ /// The actual size that is used after the element is arranged in layout.
+ protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize;
+ }
+}
diff --git a/src/Avalonia.Controls/Orientation.cs b/src/Avalonia.Layout/Orientation.cs
similarity index 94%
rename from src/Avalonia.Controls/Orientation.cs
rename to src/Avalonia.Layout/Orientation.cs
index fe998c024a..f03b087adc 100644
--- a/src/Avalonia.Controls/Orientation.cs
+++ b/src/Avalonia.Layout/Orientation.cs
@@ -1,7 +1,7 @@
// 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.
-namespace Avalonia.Controls
+namespace Avalonia.Layout
{
///
/// Defines vertical or horizontal orientation.
diff --git a/src/Avalonia.Layout/OrientationBasedMeasures.cs b/src/Avalonia.Layout/OrientationBasedMeasures.cs
new file mode 100644
index 0000000000..23a8b0e168
--- /dev/null
+++ b/src/Avalonia.Layout/OrientationBasedMeasures.cs
@@ -0,0 +1,96 @@
+// 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.Layout
+{
+ internal enum ScrollOrientation
+ {
+ Vertical,
+ Horizontal,
+ }
+
+ internal class OrientationBasedMeasures
+ {
+ public ScrollOrientation ScrollOrientation { get; set; } = ScrollOrientation.Vertical;
+
+ public double Major(in Size size) => ScrollOrientation == ScrollOrientation.Vertical ? size.Height : size.Width;
+ public double Minor(in Size size) => ScrollOrientation == ScrollOrientation.Vertical ? size.Width : size.Height;
+ public double MajorSize(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.Height : rect.Width;
+ public double MinorSize(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.Width : rect.Height;
+ public double MajorStart(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.Y : rect.X;
+ public double MinorStart(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.X : rect.Y;
+ public double MajorEnd(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.Bottom : rect.Right;
+ public double MinorEnd(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.Right : rect.Bottom;
+
+ public void SetMajorSize(ref Rect rect, double value)
+ {
+ if (ScrollOrientation == ScrollOrientation.Vertical)
+ {
+ rect = rect.WithHeight(value);
+ }
+ else
+ {
+ rect = rect.WithWidth(value);
+ }
+ }
+
+ public void SetMinorSize(ref Rect rect, double value)
+ {
+ if (ScrollOrientation == ScrollOrientation.Vertical)
+ {
+ rect = rect.WithWidth(value);
+ }
+ else
+ {
+ rect = rect.WithHeight(value);
+ }
+ }
+
+ public void SetMajorStart(ref Rect rect, double value)
+ {
+ if (ScrollOrientation == ScrollOrientation.Vertical)
+ {
+ rect = rect.WithY(value);
+ }
+ else
+ {
+ rect = rect.WithX(value);
+ }
+ }
+
+ public void SetMinorStart(ref Rect rect, double value)
+ {
+ if (ScrollOrientation == ScrollOrientation.Vertical)
+ {
+ rect = rect.WithX(value);
+ }
+ else
+ {
+ rect = rect.WithY(value);
+ }
+ }
+
+ public Rect MinorMajorRect(double minor, double major, double minorSize, double majorSize)
+ {
+ return ScrollOrientation == ScrollOrientation.Vertical ?
+ new Rect(minor, major, minorSize, majorSize) :
+ new Rect(major, minor, majorSize, minorSize);
+ }
+
+ public Point MinorMajorPoint(double minor, double major)
+ {
+ return ScrollOrientation == ScrollOrientation.Vertical ?
+ new Point(minor, major) :
+ new Point(major, minor);
+ }
+
+ public Size MinorMajorSize(double minor, double major)
+ {
+ return ScrollOrientation == ScrollOrientation.Vertical ?
+ new Size(minor, major) :
+ new Size(major, minor);
+ }
+ }
+}
diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs
new file mode 100644
index 0000000000..e9735b9b31
--- /dev/null
+++ b/src/Avalonia.Layout/StackLayout.cs
@@ -0,0 +1,336 @@
+// 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.Specialized;
+
+namespace Avalonia.Layout
+{
+ ///
+ /// Arranges elements into a single line (with spacing) that can be oriented horizontally or vertically.
+ ///
+ public class StackLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty OrientationProperty =
+ AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty SpacingProperty =
+ AvaloniaProperty.Register(nameof(Spacing));
+
+ private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures();
+
+ ///
+ /// Initializes a new instance of the StackLayout class.
+ ///
+ public StackLayout()
+ {
+ LayoutId = "StackLayout";
+ }
+
+ ///
+ /// Gets or sets the axis along which items are laid out.
+ ///
+ ///
+ /// One of the enumeration values that specifies the axis along which items are laid out.
+ /// The default is Vertical.
+ ///
+ public Orientation Orientation
+ {
+ get => GetValue(OrientationProperty);
+ set => SetValue(OrientationProperty, value);
+ }
+
+ ///
+ /// Gets or sets a uniform distance (in pixels) between stacked items. It is applied in the
+ /// direction of the StackLayout's Orientation.
+ ///
+ public double Spacing
+ {
+ get => GetValue(SpacingProperty);
+ set => SetValue(SpacingProperty, value);
+ }
+
+ internal Rect GetExtent(
+ Size availableSize,
+ VirtualizingLayoutContext context,
+ ILayoutable firstRealized,
+ int firstRealizedItemIndex,
+ Rect firstRealizedLayoutBounds,
+ ILayoutable lastRealized,
+ int lastRealizedItemIndex,
+ Rect lastRealizedLayoutBounds)
+ {
+ var extent = new Rect();
+
+ // Constants
+ int itemsCount = context.ItemCount;
+ var stackState = (StackLayoutState)context.LayoutState;
+ double averageElementSize = GetAverageElementSize(availableSize, context, stackState) + Spacing;
+
+ _orientation.SetMinorSize(ref extent, stackState.MaxArrangeBounds);
+ _orientation.SetMajorSize(ref extent, Math.Max(0.0f, itemsCount * averageElementSize - Spacing));
+ if (itemsCount > 0)
+ {
+ if (firstRealized != null)
+ {
+ _orientation.SetMajorStart(
+ ref extent,
+ _orientation.MajorStart(firstRealizedLayoutBounds) - firstRealizedItemIndex * averageElementSize);
+ var remainingItems = itemsCount - lastRealizedItemIndex - 1;
+ _orientation.SetMajorSize(
+ ref extent,
+ _orientation.MajorEnd(lastRealizedLayoutBounds) -
+ _orientation.MajorStart(extent) +
+ (remainingItems * averageElementSize));
+ }
+ }
+
+ return extent;
+ }
+
+ internal void OnElementMeasured(
+ ILayoutable element,
+ int index,
+ Size availableSize,
+ Size measureSize,
+ Size desiredSize,
+ Size provisionalArrangeSize,
+ VirtualizingLayoutContext context)
+ {
+ if (context is VirtualizingLayoutContext virtualContext)
+ {
+ var stackState = (StackLayoutState)virtualContext.LayoutState;
+ var provisionalArrangeSizeWinRt = provisionalArrangeSize;
+ stackState.OnElementMeasured(
+ index,
+ _orientation.Major(provisionalArrangeSizeWinRt),
+ _orientation.Minor(provisionalArrangeSizeWinRt));
+ }
+ }
+
+ Size IFlowLayoutAlgorithmDelegates.Algorithm_GetMeasureSize(
+ int index,
+ Size availableSize,
+ VirtualizingLayoutContext context) => availableSize;
+
+ Size IFlowLayoutAlgorithmDelegates.Algorithm_GetProvisionalArrangeSize(
+ int index,
+ Size measureSize,
+ Size desiredSize,
+ VirtualizingLayoutContext context)
+ {
+ var measureSizeMinor = _orientation.Minor(measureSize);
+ return _orientation.MinorMajorSize(
+ !double.IsInfinity(measureSizeMinor) ?
+ Math.Max(measureSizeMinor, _orientation.Minor(desiredSize)) :
+ _orientation.Minor(desiredSize),
+ _orientation.Major(desiredSize));
+ }
+
+ bool IFlowLayoutAlgorithmDelegates.Algorithm_ShouldBreakLine(int index, double remainingSpace) => true;
+
+ FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForRealizationRect(
+ Size availableSize,
+ VirtualizingLayoutContext context) => GetAnchorForRealizationRect(availableSize, context);
+
+ FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForTargetElement(
+ int targetIndex,
+ Size availableSize,
+ VirtualizingLayoutContext context)
+ {
+ double offset = double.NaN;
+ int index = -1;
+ int itemsCount = context.ItemCount;
+
+ if (targetIndex >= 0 && targetIndex < itemsCount)
+ {
+ index = targetIndex;
+ var state = (StackLayoutState)context.LayoutState;
+ double averageElementSize = GetAverageElementSize(availableSize, context, state) + Spacing;
+ offset = index * averageElementSize + _orientation.MajorStart(state.FlowAlgorithm.LastExtent);
+ }
+
+ return new FlowLayoutAnchorInfo { Index = index, Offset = offset };
+ }
+
+ Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent(
+ Size availableSize,
+ VirtualizingLayoutContext context,
+ ILayoutable firstRealized,
+ int firstRealizedItemIndex,
+ Rect firstRealizedLayoutBounds,
+ ILayoutable lastRealized,
+ int lastRealizedItemIndex,
+ Rect lastRealizedLayoutBounds)
+ {
+ return GetExtent(
+ availableSize,
+ context,
+ firstRealized,
+ firstRealizedItemIndex,
+ firstRealizedLayoutBounds,
+ lastRealized,
+ lastRealizedItemIndex,
+ lastRealizedLayoutBounds);
+ }
+
+ void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(ILayoutable element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context)
+ {
+ OnElementMeasured(
+ element,
+ index,
+ availableSize,
+ measureSize,
+ desiredSize,
+ provisionalArrangeSize,
+ context);
+ }
+
+ void IFlowLayoutAlgorithmDelegates.Algorithm_OnLineArranged(int startIndex, int countInLine, double lineSize, VirtualizingLayoutContext context)
+ {
+ }
+
+ internal FlowLayoutAnchorInfo GetAnchorForRealizationRect(
+ Size availableSize,
+ VirtualizingLayoutContext context)
+ {
+ int anchorIndex = -1;
+ double offset = double.NaN;
+
+ // Constants
+ int itemsCount = context.ItemCount;
+ if (itemsCount > 0)
+ {
+ var realizationRect = context.RealizationRect;
+ var state = (StackLayoutState)context.LayoutState;
+ var lastExtent = state.FlowAlgorithm.LastExtent;
+
+ double averageElementSize = GetAverageElementSize(availableSize, context, state) + Spacing;
+ double realizationWindowOffsetInExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent);
+ double majorSize = _orientation.MajorSize(lastExtent) == 0 ? Math.Max(0.0, averageElementSize * itemsCount - Spacing) : _orientation.MajorSize(lastExtent);
+ if (itemsCount > 0 &&
+ _orientation.MajorSize(realizationRect) >= 0 &&
+ // MajorSize = 0 will account for when a nested repeater is outside the realization rect but still being measured. Also,
+ // note that if we are measuring this repeater, then we are already realizing an element to figure out the size, so we could
+ // just keep that element alive. It also helps in XYFocus scenarios to have an element realized for XYFocus to find a candidate
+ // in the navigating direction.
+ realizationWindowOffsetInExtent + _orientation.MajorSize(realizationRect) >= 0 && realizationWindowOffsetInExtent <= majorSize)
+ {
+ anchorIndex = (int) (realizationWindowOffsetInExtent / averageElementSize);
+ offset = anchorIndex* averageElementSize + _orientation.MajorStart(lastExtent);
+ anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorIndex));
+ }
+ }
+
+ return new FlowLayoutAnchorInfo { Index = anchorIndex, Offset = offset, };
+ }
+
+ protected override void InitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ var state = context.LayoutState;
+ var stackState = state as StackLayoutState;
+
+ if (stackState == null)
+ {
+ if (state != null)
+ {
+ throw new InvalidOperationException("LayoutState must derive from StackLayoutState.");
+ }
+
+ // Custom deriving layouts could potentially be stateful.
+ // If that is the case, we will just create the base state required by UniformGridLayout ourselves.
+ stackState = new StackLayoutState();
+ }
+
+ stackState.InitializeForContext(context, this);
+ }
+
+ protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ var stackState = (StackLayoutState)context.LayoutState;
+ stackState.UninitializeForContext(context);
+ }
+
+ protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
+ {
+ var desiredSize = GetFlowAlgorithm(context).Measure(
+ availableSize,
+ context,
+ false,
+ 0,
+ Spacing,
+ _orientation.ScrollOrientation,
+ LayoutId);
+
+ return new Size(desiredSize.Width, desiredSize.Height);
+ }
+
+ protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
+ {
+ var value = GetFlowAlgorithm(context).Arrange(
+ finalSize,
+ context,
+ FlowLayoutAlgorithm.LineAlignment.Start,
+ LayoutId);
+
+ ((StackLayoutState)context.LayoutState).OnArrangeLayoutEnd();
+
+ return new Size(value.Width, value.Height);
+ }
+
+ protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args)
+ {
+ GetFlowAlgorithm(context).OnItemsSourceChanged(source, args, context);
+ // Always invalidate layout to keep the view accurate.
+ InvalidateLayout();
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Property == OrientationProperty)
+ {
+ var orientation = (Orientation)e.NewValue;
+
+ //Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation.
+ //Horizontal Orientation means we have a Horizontal ScrollOrientation.
+ _orientation.ScrollOrientation = orientation == Orientation.Horizontal ? ScrollOrientation.Horizontal : ScrollOrientation.Vertical;
+ }
+
+ InvalidateLayout();
+ }
+
+ private double GetAverageElementSize(
+ Size availableSize,
+ VirtualizingLayoutContext context,
+ StackLayoutState stackLayoutState)
+ {
+ double averageElementSize = 0;
+
+ if (context.ItemCount > 0)
+ {
+ if (stackLayoutState.TotalElementsMeasured == 0)
+ {
+ var tmpElement = context.GetOrCreateElementAt(0, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
+ stackLayoutState.FlowAlgorithm.MeasureElement(tmpElement, 0, availableSize, context);
+ context.RecycleElement(tmpElement);
+ }
+
+ averageElementSize = Math.Round(stackLayoutState.TotalElementSize / stackLayoutState.TotalElementsMeasured);
+ }
+
+ return averageElementSize;
+ }
+
+ private void InvalidateLayout() => InvalidateMeasure();
+
+ private FlowLayoutAlgorithm GetFlowAlgorithm(VirtualizingLayoutContext context) => ((StackLayoutState)context.LayoutState).FlowAlgorithm;
+ }
+}
diff --git a/src/Avalonia.Layout/StackLayoutState.cs b/src/Avalonia.Layout/StackLayoutState.cs
new file mode 100644
index 0000000000..05ad9bca8e
--- /dev/null
+++ b/src/Avalonia.Layout/StackLayoutState.cs
@@ -0,0 +1,61 @@
+// 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.Generic;
+using System.Linq;
+
+namespace Avalonia.Layout
+{
+ ///
+ /// Represents the state of a StackLayout.
+ ///
+ public class StackLayoutState
+ {
+ private const int BufferSize = 100;
+ private readonly List _estimationBuffer = new List();
+
+ internal FlowLayoutAlgorithm FlowAlgorithm { get; } = new FlowLayoutAlgorithm();
+ internal double MaxArrangeBounds { get; private set; }
+ internal int TotalElementsMeasured { get; private set; }
+ internal double TotalElementSize { get; private set; }
+
+ internal void InitializeForContext(VirtualizingLayoutContext context, IFlowLayoutAlgorithmDelegates callbacks)
+ {
+ FlowAlgorithm.InitializeForContext(context, callbacks);
+
+ if (_estimationBuffer.Count == 0)
+ {
+ _estimationBuffer.AddRange(Enumerable.Repeat(0.0, BufferSize));
+ }
+
+ context.LayoutState = this;
+ }
+
+ internal void UninitializeForContext(VirtualizingLayoutContext context)
+ {
+ FlowAlgorithm.UninitializeForContext(context);
+ }
+
+ internal void OnElementMeasured(int elementIndex, double majorSize, double minorSize)
+ {
+ int estimationBufferIndex = elementIndex % _estimationBuffer.Count;
+ bool alreadyMeasured = _estimationBuffer[estimationBufferIndex] != 0;
+
+ if (!alreadyMeasured)
+ {
+ TotalElementsMeasured++;
+ }
+
+ TotalElementSize -= _estimationBuffer[estimationBufferIndex];
+ TotalElementSize += majorSize;
+ _estimationBuffer[estimationBufferIndex] = majorSize;
+
+ MaxArrangeBounds = Math.Max(MaxArrangeBounds, minorSize);
+ }
+
+ internal void OnArrangeLayoutEnd() => MaxArrangeBounds = 0;
+ }
+}
diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs
new file mode 100644
index 0000000000..edc2042922
--- /dev/null
+++ b/src/Avalonia.Layout/UniformGridLayout.cs
@@ -0,0 +1,520 @@
+// 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.Specialized;
+
+namespace Avalonia.Layout
+{
+ ///
+ /// Defines constants that specify how items are aligned on the non-scrolling or non-virtualizing axis.
+ ///
+ public enum UniformGridLayoutItemsJustification
+ {
+ ///
+ /// Items are aligned with the start of the row or column, with extra space at the end.
+ /// Spacing between items does not change.
+ ///
+ Start = 0,
+
+ ///
+ /// Items are aligned in the center of the row or column, with extra space at the start and
+ /// end. Spacing between items does not change.
+ ///
+ Center = 1,
+
+ ///
+ /// Items are aligned with the end of the row or column, with extra space at the start.
+ /// Spacing between items does not change.
+ ///
+ End = 2,
+
+ ///
+ /// Items are aligned so that extra space is added evenly before and after each item.
+ ///
+ SpaceAround = 3,
+
+ ///
+ /// Items are aligned so that extra space is added evenly between adjacent items. No space
+ /// is added at the start or end.
+ ///
+ SpaceBetween = 4,
+
+ SpaceEvenly = 5,
+ };
+
+ ///
+ /// Defines constants that specify how items are sized to fill the available space.
+ ///
+ public enum UniformGridLayoutItemsStretch
+ {
+ ///
+ /// The item retains its natural size. Use of extra space is determined by the
+ /// property.
+ ///
+ None = 0,
+
+ ///
+ /// The item is sized to fill the available space in the non-scrolling direction. Item size
+ /// in the scrolling direction is not changed.
+ ///
+ Fill = 1,
+
+ ///
+ /// The item is sized to both fill the available space in the non-scrolling direction and
+ /// maintain its aspect ratio.
+ ///
+ Uniform = 2,
+ };
+
+ ///
+ /// Positions elements sequentially from left to right or top to bottom in a wrapping layout.
+ ///
+ public class UniformGridLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ItemsJustificationProperty =
+ AvaloniaProperty.Register(nameof(ItemsJustification));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ItemsStretchProperty =
+ AvaloniaProperty.Register(nameof(ItemsStretch));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MinColumnSpacingProperty =
+ AvaloniaProperty.Register(nameof(MinColumnSpacing));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MinItemHeightProperty =
+ AvaloniaProperty.Register(nameof(MinItemHeight));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MinItemWidthProperty =
+ AvaloniaProperty.Register(nameof(MinItemWidth));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MinRowSpacingProperty =
+ AvaloniaProperty.Register(nameof(MinRowSpacing));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty OrientationProperty =
+ StackLayout.OrientationProperty.AddOwner();
+
+ private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures();
+ private double _minItemWidth = double.NaN;
+ private double _minItemHeight = double.NaN;
+ private double _minRowSpacing;
+ private double _minColumnSpacing;
+ private UniformGridLayoutItemsJustification _itemsJustification;
+ private UniformGridLayoutItemsStretch _itemsStretch;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public UniformGridLayout()
+ {
+ LayoutId = "UniformGridLayout";
+ }
+
+ static UniformGridLayout()
+ {
+ OrientationProperty.OverrideDefaultValue(Orientation.Horizontal);
+ }
+
+ ///
+ /// Gets or sets a value that indicates how items are aligned on the non-scrolling or non-
+ /// virtualizing axis.
+ ///
+ ///
+ /// An enumeration value that indicates how items are aligned. The default is Start.
+ ///
+ public UniformGridLayoutItemsJustification ItemsJustification
+ {
+ get => GetValue(ItemsJustificationProperty);
+ set => SetValue(ItemsJustificationProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value that indicates how items are sized to fill the available space.
+ ///
+ ///
+ /// An enumeration value that indicates how items are sized to fill the available space.
+ /// The default is None.
+ ///
+ ///
+ /// This property enables adaptive layout behavior where the items are sized to fill the
+ /// available space along the non-scrolling axis, and optionally maintain their aspect ratio.
+ ///
+ public UniformGridLayoutItemsStretch ItemsStretch
+ {
+ get => GetValue(ItemsStretchProperty);
+ set => SetValue(ItemsStretchProperty, value);
+ }
+
+ ///
+ /// Gets or sets the minimum space between items on the horizontal axis.
+ ///
+ ///
+ /// The spacing may exceed this minimum value when is set
+ /// to SpaceEvenly, SpaceAround, or SpaceBetween.
+ ///
+ public double MinColumnSpacing
+ {
+ get => GetValue(MinColumnSpacingProperty);
+ set => SetValue(MinColumnSpacingProperty, value);
+ }
+
+ ///
+ /// Gets or sets the minimum height of each item.
+ ///
+ ///
+ /// The minimum height (in pixels) of each item. The default is NaN, in which case the
+ /// height of the first item is used as the minimum.
+ ///
+ public double MinItemHeight
+ {
+ get => GetValue(MinItemHeightProperty);
+ set => SetValue(MinItemHeightProperty, value);
+ }
+
+ ///
+ /// Gets or sets the minimum width of each item.
+ ///
+ ///
+ /// The minimum width (in pixels) of each item. The default is NaN, in which case the width
+ /// of the first item is used as the minimum.
+ ///
+ public double MinItemWidth
+ {
+ get => GetValue(MinItemWidthProperty);
+ set => SetValue(MinItemWidthProperty, value);
+ }
+
+ ///
+ /// Gets or sets the minimum space between items on the vertical axis.
+ ///
+ ///
+ /// The spacing may exceed this minimum value when is set
+ /// to SpaceEvenly, SpaceAround, or SpaceBetween.
+ ///
+ public double MinRowSpacing
+ {
+ get => GetValue(MinRowSpacingProperty);
+ set => SetValue(MinRowSpacingProperty, value);
+ }
+
+ ///
+ /// Gets or sets the axis along which items are laid out.
+ ///
+ ///
+ /// One of the enumeration values that specifies the axis along which items are laid out.
+ /// The default is Vertical.
+ ///
+ public Orientation Orientation
+ {
+ get => GetValue(OrientationProperty);
+ set => SetValue(OrientationProperty, value);
+ }
+
+ internal double LineSpacing => Orientation == Orientation.Horizontal ? _minRowSpacing : _minColumnSpacing;
+ internal double MinItemSpacing => Orientation == Orientation.Horizontal ? _minColumnSpacing : _minRowSpacing;
+
+ Size IFlowLayoutAlgorithmDelegates.Algorithm_GetMeasureSize(
+ int index,
+ Size availableSize,
+ VirtualizingLayoutContext context)
+ {
+ var gridState = (UniformGridLayoutState)context.LayoutState;
+ return new Size(gridState.EffectiveItemWidth, gridState.EffectiveItemHeight);
+ }
+
+ Size IFlowLayoutAlgorithmDelegates.Algorithm_GetProvisionalArrangeSize(
+ int index,
+ Size measureSize,
+ Size desiredSize,
+ VirtualizingLayoutContext context)
+ {
+ var gridState = (UniformGridLayoutState)context.LayoutState;
+ return new Size(gridState.EffectiveItemWidth, gridState.EffectiveItemHeight);
+ }
+
+ bool IFlowLayoutAlgorithmDelegates.Algorithm_ShouldBreakLine(int index, double remainingSpace) => remainingSpace < 0;
+
+ FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForRealizationRect(
+ Size availableSize,
+ VirtualizingLayoutContext context)
+ {
+ Rect bounds = new Rect(double.NaN, double.NaN, double.NaN, double.NaN);
+ int anchorIndex = -1;
+
+ int itemsCount = context.ItemCount;
+ var realizationRect = context.RealizationRect;
+ if (itemsCount > 0 && _orientation.MajorSize(realizationRect) > 0)
+ {
+ var gridState = (UniformGridLayoutState)context.LayoutState;
+ var lastExtent = gridState.FlowAlgorithm.LastExtent;
+ int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context)));
+ double majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context);
+ double realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent);
+ if ((realizationWindowStartWithinExtent + _orientation.MajorSize(realizationRect)) >= 0 && realizationWindowStartWithinExtent <= majorSize)
+ {
+ double offset = Math.Max(0.0, _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent));
+ int anchorRowIndex = (int)(offset / GetMajorSizeWithSpacing(context));
+
+ anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine));
+ bounds = GetLayoutRectForDataIndex(availableSize, anchorIndex, lastExtent, context);
+ }
+ }
+
+ return new FlowLayoutAnchorInfo
+ {
+ Index = anchorIndex,
+ Offset = _orientation.MajorStart(bounds)
+ };
+ }
+
+ FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForTargetElement(
+ int targetIndex,
+ Size availableSize,
+ VirtualizingLayoutContext context)
+ {
+ int index = -1;
+ double offset = double.NaN;
+ int count = context.ItemCount;
+ if (targetIndex >= 0 && targetIndex < count)
+ {
+ int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context)));
+ int indexOfFirstInLine = (targetIndex / itemsPerLine) * itemsPerLine;
+ index = indexOfFirstInLine;
+ var state = context.LayoutState as UniformGridLayoutState;
+ offset = _orientation.MajorStart(GetLayoutRectForDataIndex(availableSize, indexOfFirstInLine, state.FlowAlgorithm.LastExtent, context));
+ }
+
+ return new FlowLayoutAnchorInfo
+ {
+ Index = index,
+ Offset = offset
+ };
+ }
+
+ Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent(
+ Size availableSize,
+ VirtualizingLayoutContext context,
+ ILayoutable firstRealized,
+ int firstRealizedItemIndex,
+ Rect firstRealizedLayoutBounds,
+ ILayoutable lastRealized,
+ int lastRealizedItemIndex,
+ Rect lastRealizedLayoutBounds)
+ {
+ var extent = new Rect();
+
+
+ // Constants
+ int itemsCount = context.ItemCount;
+ double availableSizeMinor = _orientation.Minor(availableSize);
+ int itemsPerLine = Math.Max(1, !double.IsInfinity(availableSizeMinor) ?
+ (int)(availableSizeMinor / GetMinorSizeWithSpacing(context)) : itemsCount);
+ double lineSize = GetMajorSizeWithSpacing(context);
+
+ if (itemsCount > 0)
+ {
+ _orientation.SetMinorSize(
+ ref extent,
+ !double.IsInfinity(availableSizeMinor) ?
+ availableSizeMinor :
+ Math.Max(0.0, itemsCount * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing));
+ _orientation.SetMajorSize(
+ ref extent,
+ Math.Max(0.0, (itemsCount / itemsPerLine) * lineSize - (double)LineSpacing));
+
+ if (firstRealized != null)
+ {
+ _orientation.SetMajorStart(
+ ref extent,
+ _orientation.MajorStart(firstRealizedLayoutBounds) - (firstRealizedItemIndex / itemsPerLine) * lineSize);
+ int remainingItems = itemsCount - lastRealizedItemIndex - 1;
+ _orientation.SetMajorSize(
+ ref extent,
+ _orientation.MajorEnd(lastRealizedLayoutBounds) - _orientation.MajorStart(extent) + (remainingItems / itemsPerLine) * lineSize);
+ }
+ }
+
+ return extent;
+ }
+
+ void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(ILayoutable element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context)
+ {
+ }
+
+ void IFlowLayoutAlgorithmDelegates.Algorithm_OnLineArranged(int startIndex, int countInLine, double lineSize, VirtualizingLayoutContext context)
+ {
+ }
+
+ protected override void InitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ var state = context.LayoutState;
+ var gridState = state as UniformGridLayoutState;
+
+ if (gridState == null)
+ {
+ if (state != null)
+ {
+ throw new InvalidOperationException("LayoutState must derive from UniformGridLayoutState.");
+ }
+
+ // Custom deriving layouts could potentially be stateful.
+ // If that is the case, we will just create the base state required by UniformGridLayout ourselves.
+ gridState = new UniformGridLayoutState();
+ }
+
+ gridState.InitializeForContext(context, this);
+ }
+
+ protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ var gridState = (UniformGridLayoutState)context.LayoutState;
+ gridState.UninitializeForContext(context);
+ }
+
+ protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
+ {
+ // Set the width and height on the grid state. If the user already set them then use the preset.
+ // If not, we have to measure the first element and get back a size which we're going to be using for the rest of the items.
+ var gridState = (UniformGridLayoutState)context.LayoutState;
+ gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing);
+
+ var desiredSize = GetFlowAlgorithm(context).Measure(
+ availableSize,
+ context,
+ true,
+ MinItemSpacing,
+ LineSpacing,
+ _orientation.ScrollOrientation,
+ LayoutId);
+
+ // If after Measure the first item is in the realization rect, then we revoke grid state's ownership,
+ // and only use the layout when to clear it when it's done.
+ gridState.EnsureFirstElementOwnership(context);
+
+ return new Size(desiredSize.Width, desiredSize.Height);
+ }
+
+ protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
+ {
+ var value = GetFlowAlgorithm(context).Arrange(
+ finalSize,
+ context,
+ (FlowLayoutAlgorithm.LineAlignment)_itemsJustification,
+ LayoutId);
+ return new Size(value.Width, value.Height);
+ }
+
+ protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args)
+ {
+ GetFlowAlgorithm(context).OnItemsSourceChanged(source, args, context);
+ // Always invalidate layout to keep the view accurate.
+ InvalidateLayout();
+
+ var gridState = (UniformGridLayoutState)context.LayoutState;
+ gridState.ClearElementOnDataSourceChange(context, args);
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args)
+ {
+ if (args.Property == OrientationProperty)
+ {
+ var orientation = (Orientation)args.NewValue;
+
+ //Note: For UniformGridLayout Vertical Orientation means we have a Horizontal ScrollOrientation. Horizontal Orientation means we have a Vertical ScrollOrientation.
+ //i.e. the properties are the inverse of each other.
+ var scrollOrientation = (orientation == Orientation.Horizontal) ? ScrollOrientation.Vertical : ScrollOrientation.Horizontal;
+ _orientation.ScrollOrientation = scrollOrientation;
+ }
+ else if (args.Property == MinColumnSpacingProperty)
+ {
+ _minColumnSpacing = (double)args.NewValue;
+ }
+ else if (args.Property == MinRowSpacingProperty)
+ {
+ _minRowSpacing = (double)args.NewValue;
+ }
+ else if (args.Property == ItemsJustificationProperty)
+ {
+ _itemsJustification = (UniformGridLayoutItemsJustification)args.NewValue;
+ }
+ else if (args.Property == ItemsStretchProperty)
+ {
+ _itemsStretch = (UniformGridLayoutItemsStretch)args.NewValue;
+ }
+ else if (args.Property == MinItemWidthProperty)
+ {
+ _minItemWidth = (double)args.NewValue;
+ }
+ else if (args.Property == MinItemHeightProperty)
+ {
+ _minItemHeight = (double)args.NewValue;
+ }
+
+ InvalidateLayout();
+ }
+
+ private double GetMinorSizeWithSpacing(VirtualizingLayoutContext context)
+ {
+ var minItemSpacing = MinItemSpacing;
+ var gridState = (UniformGridLayoutState)context.LayoutState;
+ return _orientation.ScrollOrientation == ScrollOrientation.Vertical?
+ gridState.EffectiveItemWidth + minItemSpacing :
+ gridState.EffectiveItemHeight + minItemSpacing;
+ }
+
+ private double GetMajorSizeWithSpacing(VirtualizingLayoutContext context)
+ {
+ var lineSpacing = LineSpacing;
+ var gridState = (UniformGridLayoutState)context.LayoutState;
+ return _orientation.ScrollOrientation == ScrollOrientation.Vertical ?
+ gridState.EffectiveItemHeight + lineSpacing :
+ gridState.EffectiveItemWidth + lineSpacing;
+ }
+
+ Rect GetLayoutRectForDataIndex(
+ Size availableSize,
+ int index,
+ Rect lastExtent,
+ VirtualizingLayoutContext context)
+ {
+ int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context)));
+ int rowIndex = (int)(index / itemsPerLine);
+ int indexInRow = index - (rowIndex * itemsPerLine);
+
+ var gridState = (UniformGridLayoutState)context.LayoutState;
+ Rect bounds = _orientation.MinorMajorRect(
+ indexInRow * GetMinorSizeWithSpacing(context) + _orientation.MinorStart(lastExtent),
+ rowIndex * GetMajorSizeWithSpacing(context) + _orientation.MajorStart(lastExtent),
+ _orientation.ScrollOrientation == ScrollOrientation.Vertical ? gridState.EffectiveItemWidth : gridState.EffectiveItemHeight,
+ _orientation.ScrollOrientation == ScrollOrientation.Vertical ? gridState.EffectiveItemHeight : gridState.EffectiveItemWidth);
+
+ return bounds;
+ }
+
+ private void InvalidateLayout() => InvalidateMeasure();
+
+ private FlowLayoutAlgorithm GetFlowAlgorithm(VirtualizingLayoutContext context) => ((UniformGridLayoutState)context.LayoutState).FlowAlgorithm;
+ }
+}
diff --git a/src/Avalonia.Layout/UniformGridLayoutState.cs b/src/Avalonia.Layout/UniformGridLayoutState.cs
new file mode 100644
index 0000000000..4557a78d37
--- /dev/null
+++ b/src/Avalonia.Layout/UniformGridLayoutState.cs
@@ -0,0 +1,192 @@
+// 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.Specialized;
+
+namespace Avalonia.Layout
+{
+ ///
+ /// Represents the state of a .
+ ///
+ public class UniformGridLayoutState
+ {
+ // We need to measure the element at index 0 to know what size to measure all other items.
+ // If FlowlayoutAlgorithm has already realized element 0 then we can use that.
+ // If it does not, then we need to do context.GetElement(0) at which point we have requested an element and are on point to clear it.
+ // If we are responsible for clearing element 0 we keep m_cachedFirstElement valid.
+ // If we are not (because FlowLayoutAlgorithm is holding it for us) then we just null out this field and use the one from FlowLayoutAlgorithm.
+ private ILayoutable _cachedFirstElement;
+
+ internal FlowLayoutAlgorithm FlowAlgorithm { get; } = new FlowLayoutAlgorithm();
+ internal double EffectiveItemWidth { get; private set; }
+ internal double EffectiveItemHeight { get; private set; }
+
+ internal void InitializeForContext(VirtualizingLayoutContext context, IFlowLayoutAlgorithmDelegates callbacks)
+ {
+ FlowAlgorithm.InitializeForContext(context, callbacks);
+ context.LayoutState = this;
+ }
+
+ internal void UninitializeForContext(VirtualizingLayoutContext context)
+ {
+ FlowAlgorithm.UninitializeForContext(context);
+
+ if (_cachedFirstElement != null)
+ {
+ context.RecycleElement(_cachedFirstElement);
+ }
+ }
+
+ internal void EnsureElementSize(
+ Size availableSize,
+ VirtualizingLayoutContext context,
+ double layoutItemWidth,
+ double LayoutItemHeight,
+ UniformGridLayoutItemsStretch stretch,
+ Orientation orientation,
+ double minRowSpacing,
+ double minColumnSpacing)
+ {
+ if (context.ItemCount > 0)
+ {
+ // If the first element is realized we don't need to cache it or to get it from the context
+ var realizedElement = FlowAlgorithm.GetElementIfRealized(0);
+ if (realizedElement != null)
+ {
+ realizedElement.Measure(availableSize);
+ SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing);
+ _cachedFirstElement = null;
+ }
+ else
+ {
+ if (_cachedFirstElement == null)
+ {
+ // we only cache if we aren't realizing it
+ _cachedFirstElement = context.GetOrCreateElementAt(
+ 0,
+ ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); // expensive
+ }
+
+ _cachedFirstElement.Measure(availableSize);
+
+ // This doesn't need to be done in the UWP version and I'm not sure why. If we
+ // don't do this here, and we receive a recycled element then it will be shown
+ // at its previous arrange point, but we don't want it shown at all until its
+ // arranged.
+ _cachedFirstElement.Arrange(new Rect(-10000.0, -10000.0, 0, 0));
+
+ SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing);
+
+ // See if we can move ownership to the flow algorithm. If we can, we do not need a local cache.
+ bool added = FlowAlgorithm.TryAddElement0(_cachedFirstElement);
+ if (added)
+ {
+ _cachedFirstElement = null;
+ }
+ }
+ }
+ }
+
+ private void SetSize(
+ ILayoutable element,
+ double layoutItemWidth,
+ double LayoutItemHeight,
+ Size availableSize,
+ UniformGridLayoutItemsStretch stretch,
+ Orientation orientation,
+ double minRowSpacing,
+ double minColumnSpacing)
+ {
+ EffectiveItemWidth = (double.IsNaN(layoutItemWidth) ? element.DesiredSize.Width : layoutItemWidth);
+ EffectiveItemHeight = (double.IsNaN(LayoutItemHeight) ? element.DesiredSize.Height : LayoutItemHeight);
+
+ var availableSizeMinor = orientation == Orientation.Horizontal ? availableSize.Width : availableSize.Height;
+ var minorItemSpacing = orientation == Orientation.Vertical ? minRowSpacing : minColumnSpacing;
+
+ var itemSizeMinor = orientation == Orientation.Horizontal ? EffectiveItemWidth : EffectiveItemHeight;
+ itemSizeMinor += minorItemSpacing;
+
+ var numItemsPerColumn = (int)(Math.Max(1.0, availableSizeMinor / itemSizeMinor));
+ var remainingSpace = ((int)availableSizeMinor) % ((int)itemSizeMinor);
+ var extraMinorPixelsForEachItem = remainingSpace / numItemsPerColumn;
+
+ if (stretch == UniformGridLayoutItemsStretch.Fill)
+ {
+ if (orientation == Orientation.Horizontal)
+ {
+ EffectiveItemWidth += extraMinorPixelsForEachItem;
+ }
+ else
+ {
+ EffectiveItemHeight += extraMinorPixelsForEachItem;
+ }
+ }
+ else if (stretch == UniformGridLayoutItemsStretch.Uniform)
+ {
+ var itemSizeMajor = orientation == Orientation.Horizontal ? EffectiveItemHeight : EffectiveItemWidth;
+ var extraMajorPixelsForEachItem = itemSizeMajor * (extraMinorPixelsForEachItem / itemSizeMinor);
+ if (orientation == Orientation.Horizontal)
+ {
+ EffectiveItemWidth += extraMinorPixelsForEachItem;
+ EffectiveItemHeight += extraMajorPixelsForEachItem;
+ }
+ else
+ {
+ EffectiveItemHeight += extraMinorPixelsForEachItem;
+ EffectiveItemWidth += extraMajorPixelsForEachItem;
+ }
+ }
+ }
+
+ internal void EnsureFirstElementOwnership(VirtualizingLayoutContext context)
+ {
+ if (_cachedFirstElement != null && FlowAlgorithm.GetElementIfRealized(0) != null)
+ {
+ // We created the element, but then flowlayout algorithm took ownership, so we can clear it and
+ // let flowlayout algorithm do its thing.
+ context.RecycleElement(_cachedFirstElement);
+ _cachedFirstElement = null;
+ }
+ }
+
+ internal void ClearElementOnDataSourceChange(
+ VirtualizingLayoutContext context,
+ NotifyCollectionChangedEventArgs args)
+ {
+ if (_cachedFirstElement != null)
+ {
+ bool shouldClear = false;
+ switch (args.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ shouldClear = args.NewStartingIndex == 0;
+ break;
+
+ case NotifyCollectionChangedAction.Replace:
+ shouldClear = args.NewStartingIndex == 0 || args.OldStartingIndex == 0;
+ break;
+
+ case NotifyCollectionChangedAction.Remove:
+ shouldClear = args.OldStartingIndex == 0;
+ break;
+
+ case NotifyCollectionChangedAction.Reset:
+ shouldClear = true;
+ break;
+
+ case NotifyCollectionChangedAction.Move:
+ throw new NotImplementedException();
+ }
+
+ if (shouldClear)
+ {
+ context.RecycleElement(_cachedFirstElement);
+ _cachedFirstElement = null;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Layout/Utils/ListUtils.cs b/src/Avalonia.Layout/Utils/ListUtils.cs
new file mode 100644
index 0000000000..eb2480acd3
--- /dev/null
+++ b/src/Avalonia.Layout/Utils/ListUtils.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Avalonia.Layout.Utils
+{
+ internal static class ListUtils
+ {
+ public static void Resize(this List list, int size, T value)
+ {
+ int cur = list.Count;
+
+ if (size < cur)
+ {
+ list.RemoveRange(size, cur - size);
+ }
+ else if (size > cur)
+ {
+ if (size > list.Capacity)
+ {
+ list.Capacity = size;
+ }
+
+ list.AddRange(Enumerable.Repeat(value, size - cur));
+ }
+ }
+
+ public static void Resize(this List list, int count)
+ {
+ Resize(list, count, default);
+ }
+ }
+}
diff --git a/src/Avalonia.Layout/VirtualizingLayout.cs b/src/Avalonia.Layout/VirtualizingLayout.cs
new file mode 100644
index 0000000000..4c601175f3
--- /dev/null
+++ b/src/Avalonia.Layout/VirtualizingLayout.cs
@@ -0,0 +1,139 @@
+// 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.Collections.Specialized;
+
+namespace Avalonia.Layout
+{
+ ///
+ /// Represents the base class for an object that sizes and arranges child elements for a host
+ /// and supports virtualization.
+ ///
+ ///
+ /// is the base class for layouts that support virtualization.
+ /// You can use one of the provided derived class, or inherit from it to create your own layout.
+ /// Provided concrete virtualizing layout classes are and
+ /// .
+ ///
+ public abstract class VirtualizingLayout : AttachedLayout
+ {
+ ///
+ public sealed override void InitializeForContext(LayoutContext context)
+ {
+ InitializeForContextCore((VirtualizingLayoutContext)context);
+ }
+
+ ///
+ public sealed override void UninitializeForContext(LayoutContext context)
+ {
+ UninitializeForContextCore((VirtualizingLayoutContext)context);
+ }
+
+ ///
+ public sealed override Size Measure(LayoutContext context, Size availableSize)
+ {
+ return MeasureOverride((VirtualizingLayoutContext)context, availableSize);
+ }
+
+ ///
+ public sealed override Size Arrange(LayoutContext context, Size finalSize)
+ {
+ return ArrangeOverride((VirtualizingLayoutContext)context, finalSize);
+ }
+
+ ///
+ /// Notifies the layout when the data collection assigned to the container element (Items)
+ /// has changed.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ /// The data source.
+ /// Data about the collection change.
+ ///
+ /// Override
+ /// to provide the behavior for this method in a derived class.
+ ///
+ public void OnItemsChanged(
+ VirtualizingLayoutContext context,
+ object source,
+ NotifyCollectionChangedEventArgs args) => OnItemsChangedCore(context, source, args);
+
+ ///
+ /// When overridden in a derived class, initializes any per-container state the layout
+ /// requires when it is attached to an ILayoutable container.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ protected virtual void InitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ }
+
+ ///
+ /// When overridden in a derived class, removes any state the layout previously stored on
+ /// the ILayoutable container.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ }
+
+ ///
+ /// Provides the behavior for the "Measure" pass of the layout cycle. Classes can override
+ /// this method to define their own "Measure" pass behavior.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ ///
+ /// The available size that this object can give to child objects. Infinity can be
+ /// specified as a value to indicate that the object will size to whatever content is
+ /// available.
+ ///
+ ///
+ /// The size that this object determines it needs during layout, based on its calculations
+ /// of the allocated sizes for child objects or based on other considerations such as a
+ /// fixed container size.
+ ///
+ protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize);
+
+ ///
+ /// When implemented in a derived class, provides the behavior for the "Arrange" pass of
+ /// layout. Classes can override this method to define their own "Arrange" pass behavior.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ ///
+ /// The final area within the container that this object should use to arrange itself and
+ /// its children.
+ ///
+ /// The actual size that is used after the element is arranged in layout.
+ protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize;
+
+ ///
+ /// Notifies the layout when the data collection assigned to the container element (Items)
+ /// has changed.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ /// The data source.
+ /// Data about the collection change.
+ protected internal virtual void OnItemsChangedCore(
+ VirtualizingLayoutContext context,
+ object source,
+ NotifyCollectionChangedEventArgs args) => InvalidateMeasure();
+ }
+}
diff --git a/src/Avalonia.Layout/VirtualizingLayoutContext.cs b/src/Avalonia.Layout/VirtualizingLayoutContext.cs
new file mode 100644
index 0000000000..980daec2eb
--- /dev/null
+++ b/src/Avalonia.Layout/VirtualizingLayoutContext.cs
@@ -0,0 +1,190 @@
+// 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.Layout
+{
+ ///
+ /// Defines constants that specify whether to suppress automatic recycling of the retrieved
+ /// element or force creation of a new element.
+ ///
+ ///
+ /// When you call ,
+ /// you can specify whether to suppress automatic recycling of the retrieved element or force
+ /// creation of a new element. Elements retrieved with automatic recycling suppressed
+ /// (SuppressAutoRecycle) are ignored by the automatic recycling logic that clears realized
+ /// elements that were not retrieved as part of the current layout pass. You must explicitly
+ /// recycle these elements by passing them to the RecycleElement method to avoid memory leaks.
+ ///
+ [Flags]
+ public enum ElementRealizationOptions
+ {
+ ///
+ /// No option is specified.
+ ///
+ None = 0x0,
+
+ ///
+ /// Creation of a new element is forced.
+ ///
+ ForceCreate = 0x1,
+
+ ///
+ /// The element is ignored by the automatic recycling logic.
+ ///
+ SuppressAutoRecycle = 0x2,
+ };
+
+ ///
+ /// Represents the base class for layout context types that support virtualization.
+ ///
+ public abstract class VirtualizingLayoutContext : LayoutContext
+ {
+ ///
+ /// Gets the number of items in the data.
+ ///
+ ///
+ /// This property gets the value returned by ItemCountCore, which must be implemented in
+ /// a derived class.
+ ///
+ public int ItemCount => ItemCountCore();
+
+ ///
+ /// Gets or sets the origin point for the estimated content size.
+ ///
+ ///
+ /// LayoutOrigin is used by virtualizing layouts that rely on estimations when determining
+ /// the size and position of content. It allows the layout to fix-up the estimated origin
+ /// of the content as it changes due to on-going estimation or potentially identifying the
+ /// actual size to use. For example, it’s possible that as a user is scrolling back to the
+ /// top of the content that the layout's estimates for the content size that it reports as
+ /// part of its MeasureOverride become increasingly accurate. If the predicted position of
+ /// the content does not already match the previously predicted position (for example, if
+ /// the size of the elements ends up being smaller than previously thought), then the
+ /// layout can indicate a new origin. The viewport provided to the layout on subsequent
+ /// passes will take into account the adjusted origin.
+ ///
+ public Point LayoutOrigin { get => LayoutOriginCore; set => LayoutOriginCore = value; }
+
+ ///
+ /// Gets an area that represents the viewport and buffer that the layout should fill with
+ /// realized elements.
+ ///
+ public Rect RealizationRect => RealizationRectCore();
+
+ ///
+ /// Gets the recommended index from which to start the generation and layout of elements.
+ ///
+ ///
+ /// The recommended index might be the result of programmatically realizing an element and
+ /// requesting that it be brought into view. Or, it may be that a user drags the scrollbar
+ /// thumb so quickly that the new viewport and the viewport and buffer previously given to
+ /// the layout do not intersect, so a new index is suggested as the anchor from which to
+ /// generate and layout other elements.
+ ///
+ public int RecommendedAnchorIndex => RecommendedAnchorIndexCore;
+
+ ///
+ /// Implements the behavior of LayoutOrigin in a derived or custom VirtualizingLayoutContext.
+ ///
+ protected abstract Point LayoutOriginCore { get; set; }
+
+ ///
+ /// Implements the behavior for getting the return value of RecommendedAnchorIndex in a
+ /// derived or custom .
+ ///
+ protected virtual int RecommendedAnchorIndexCore { get; }
+
+ ///
+ /// Retrieves the data item in the source found at the specified index.
+ ///
+ /// The index of the data item to retrieve.
+ public object GetItemAt(int index) => GetItemAtCore(index);
+
+ ///
+ /// Retrieves a UIElement that represents the data item in the source found at the
+ /// specified index. By default, if an element already exists, it is returned; otherwise,
+ /// a new element is created.
+ ///
+ /// The index of the data item to retrieve a UIElement for.
+ ///
+ /// This method calls
+ /// with options set to None. GetElementAtCore must be implemented in a derived class.
+ ///
+ public ILayoutable GetOrCreateElementAt(int index)
+ => GetOrCreateElementAtCore(index, ElementRealizationOptions.None);
+
+ ///
+ /// Retrieves a UIElement that represents the data item in the source found at the
+ /// specified index using the specified options.
+ ///
+ /// The index of the data item to retrieve a UIElement for.
+ ///
+ /// A value of that specifies whether to suppress
+ /// automatic recycling of the retrieved element or force creation of a new element.
+ ///
+ ///
+ /// This method calls ,
+ /// which must be implemented in a derived class. When you request an element for the
+ /// specified index, you can optionally specify whether to suppress automatic recycling of
+ /// the retrieved element or force creation of a new element.Elements retrieved with
+ /// automatic recycling suppressed(SuppressAutoRecycle) are ignored by the automatic
+ /// recycling logic that clears realized elements that were not retrieved as part of the
+ /// current layout pass.You must explicitly recycle these elements by passing them to the
+ /// RecycleElement method to avoid memory leaks. These options are intended for more
+ /// advanced layouts that choose to explicitly manage the realization and recycling of
+ /// elements as a performance optimization.
+ ///
+ public ILayoutable GetOrCreateElementAt(int index, ElementRealizationOptions options)
+ => GetOrCreateElementAtCore(index, options);
+
+ ///
+ /// Clears the specified UIElement and allows it to be either re-used or released.
+ ///
+ /// The element to clear.
+ ///
+ /// This method calls , which must be implemented
+ /// in a derived class.
+ ///
+ public void RecycleElement(ILayoutable element) => RecycleElementCore(element);
+
+ ///
+ /// When implemented in a derived class, retrieves the number of items in the data.
+ ///
+ protected abstract int ItemCountCore();
+
+ ///
+ /// When implemented in a derived class, retrieves the data item in the source found at the
+ /// specified index.
+ ///
+ /// The index of the data item to retrieve.
+ protected abstract object GetItemAtCore(int index);
+
+ ///
+ /// When implemented in a derived class, retrieves an area that represents the viewport and
+ /// buffer that the layout should fill with realized elements.
+ ///
+ protected abstract Rect RealizationRectCore();
+
+ ///
+ /// When implemented in a derived class, retrieves a UIElement that represents the data item
+ /// in the source found at the specified index using the specified options.
+ ///
+ /// The index of the data item to retrieve a UIElement for.
+ ///
+ /// A value of that specifies whether to suppress
+ /// automatic recycling of the retrieved element or force creation of a new element.
+ ///
+ protected abstract ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options);
+
+ ///
+ /// When implemented in a derived class, clears the specified UIElement and allows it to be
+ /// either re-used or released.
+ ///
+ /// The element to clear.
+ protected abstract void RecycleElementCore(ILayoutable element);
+ }
+}
diff --git a/src/Avalonia.OpenGL/EglContext.cs b/src/Avalonia.OpenGL/EglContext.cs
index 17caf84179..a39000f198 100644
--- a/src/Avalonia.OpenGL/EglContext.cs
+++ b/src/Avalonia.OpenGL/EglContext.cs
@@ -10,7 +10,7 @@ namespace Avalonia.OpenGL
private readonly EglInterface _egl;
private readonly object _lock = new object();
- public EglContext(EglDisplay display, EglInterface egl, IntPtr ctx, IntPtr offscreenSurface)
+ public EglContext(EglDisplay display, EglInterface egl, IntPtr ctx, EglSurface offscreenSurface)
{
_disp = display;
_egl = egl;
@@ -19,7 +19,7 @@ namespace Avalonia.OpenGL
}
public IntPtr Context { get; }
- public IntPtr OffscreenSurface { get; }
+ public EglSurface OffscreenSurface { get; }
public IGlDisplay Display => _disp;
public IDisposable Lock()
@@ -36,8 +36,8 @@ namespace Avalonia.OpenGL
public void MakeCurrent(EglSurface surface)
{
- var surf = surface?.DangerousGetHandle() ?? OffscreenSurface;
- if (!_egl.MakeCurrent(_disp.Handle, surf, surf, Context))
+ var surf = surface ?? OffscreenSurface;
+ if (!_egl.MakeCurrent(_disp.Handle, surf.DangerousGetHandle(), surf.DangerousGetHandle(), Context))
throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl);
}
}
diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs
index b2b5a1a646..66418c0e15 100644
--- a/src/Avalonia.OpenGL/EglDisplay.cs
+++ b/src/Avalonia.OpenGL/EglDisplay.cs
@@ -12,49 +12,62 @@ namespace Avalonia.OpenGL
private readonly IntPtr _display;
private readonly IntPtr _config;
private readonly int[] _contextAttributes;
+ private readonly int _surfaceType;
public IntPtr Handle => _display;
private AngleOptions.PlatformApi? _angleApi;
- public EglDisplay(EglInterface egl)
+
+ public EglDisplay(EglInterface egl) : this(egl, -1, IntPtr.Zero, null)
+ {
+
+ }
+ public EglDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs)
{
- _egl = egl;
+ _egl = egl;
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ if (platformType == -1 && platformDisplay == IntPtr.Zero)
{
- if (_egl.GetPlatformDisplayEXT == null)
- throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll");
-
- var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis
- ?? new List {AngleOptions.PlatformApi.DirectX9};
-
- foreach (var platformApi in allowedApis)
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
- int dapi;
- if (platformApi == AngleOptions.PlatformApi.DirectX9)
- dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE;
- else if (platformApi == AngleOptions.PlatformApi.DirectX11)
- dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE;
- else
- continue;
-
- _display = _egl.GetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, new[]
- {
- EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE
- });
- if (_display != IntPtr.Zero)
+ if (_egl.GetPlatformDisplayEXT == null)
+ throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll");
+
+ var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis
+ ?? new List {AngleOptions.PlatformApi.DirectX9};
+
+ foreach (var platformApi in allowedApis)
{
- _angleApi = platformApi;
- break;
+ int dapi;
+ if (platformApi == AngleOptions.PlatformApi.DirectX9)
+ dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE;
+ else if (platformApi == AngleOptions.PlatformApi.DirectX11)
+ dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE;
+ else
+ continue;
+
+ _display = _egl.GetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero,
+ new[] {EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE});
+ if (_display != IntPtr.Zero)
+ {
+ _angleApi = platformApi;
+ break;
+ }
}
+
+ if (_display == IntPtr.Zero)
+ throw new OpenGlException("Unable to create ANGLE display");
}
if (_display == IntPtr.Zero)
- throw new OpenGlException("Unable to create ANGLE display");
+ _display = _egl.GetDisplay(IntPtr.Zero);
+ }
+ else
+ {
+ if (_egl.GetPlatformDisplayEXT == null)
+ throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl");
+ _display = _egl.GetPlatformDisplayEXT(platformType, platformDisplay, attrs);
}
- if (_display == IntPtr.Zero)
- _display = _egl.GetDisplay(IntPtr.Zero);
-
if (_display == IntPtr.Zero)
throw OpenGlException.GetFormattedException("eglGetDisplay", _egl);
@@ -85,16 +98,14 @@ namespace Avalonia.OpenGL
{
if (!_egl.BindApi(cfg.Api))
continue;
-
+ foreach(var surfaceType in new[]{EGL_PBUFFER_BIT|EGL_WINDOW_BIT, EGL_WINDOW_BIT})
foreach(var stencilSize in new[]{8, 1, 0})
foreach (var depthSize in new []{8, 1, 0})
{
var attribs = new[]
{
- EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
-
+ EGL_SURFACE_TYPE, surfaceType,
EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit,
-
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
@@ -108,6 +119,7 @@ namespace Avalonia.OpenGL
if (numConfigs == 0)
continue;
_contextAttributes = cfg.Attributes;
+ _surfaceType = surfaceType;
Type = cfg.Type;
}
}
@@ -126,8 +138,10 @@ namespace Avalonia.OpenGL
public GlDisplayType Type { get; }
public GlInterface GlInterface { get; }
public EglInterface EglInterface => _egl;
- public IGlContext CreateContext(IGlContext share)
+ public EglContext CreateContext(IGlContext share)
{
+ if((_surfaceType|EGL_PBUFFER_BIT) == 0)
+ throw new InvalidOperationException("Platform doesn't support PBUFFER surfaces");
var shareCtx = (EglContext)share;
var ctx = _egl.CreateContext(_display, _config, shareCtx?.Context ?? IntPtr.Zero, _contextAttributes);
if (ctx == IntPtr.Zero)
@@ -140,7 +154,17 @@ namespace Avalonia.OpenGL
});
if (surf == IntPtr.Zero)
throw OpenGlException.GetFormattedException("eglCreatePBufferSurface", _egl);
- var rv = new EglContext(this, _egl, ctx, surf);
+ var rv = new EglContext(this, _egl, ctx, new EglSurface(this, _egl, surf));
+ rv.MakeCurrent(null);
+ return rv;
+ }
+
+ public EglContext CreateContext(EglContext share, EglSurface offscreenSurface)
+ {
+ var ctx = _egl.CreateContext(_display, _config, share?.Context ?? IntPtr.Zero, _contextAttributes);
+ if (ctx == IntPtr.Zero)
+ throw OpenGlException.GetFormattedException("eglCreateContext", _egl);
+ var rv = new EglContext(this, _egl, ctx, offscreenSurface);
rv.MakeCurrent(null);
return rv;
}
diff --git a/src/Avalonia.OpenGL/EglInterface.cs b/src/Avalonia.OpenGL/EglInterface.cs
index 0a99778ddf..47088972a4 100644
--- a/src/Avalonia.OpenGL/EglInterface.cs
+++ b/src/Avalonia.OpenGL/EglInterface.cs
@@ -10,6 +10,11 @@ namespace Avalonia.OpenGL
public EglInterface() : base(Load())
{
+ }
+
+ public EglInterface(Func getProcAddress) : base(getProcAddress)
+ {
+
}
public EglInterface(string library) : base(Load(library))
diff --git a/src/Avalonia.OpenGL/GlInterface.cs b/src/Avalonia.OpenGL/GlInterface.cs
index 718afc4a94..30f7d67152 100644
--- a/src/Avalonia.OpenGL/GlInterface.cs
+++ b/src/Avalonia.OpenGL/GlInterface.cs
@@ -9,12 +9,14 @@ namespace Avalonia.OpenGL
public class GlInterface : GlInterfaceBase
{
public string Version { get; }
+ public string Vendor { get; }
+ public string Renderer { get; }
public GlInterface(Func getProcAddress) : base(getProcAddress)
{
- var versionPtr = GetString(GlConsts.GL_VERSION);
- if (versionPtr != IntPtr.Zero)
- Version = Marshal.PtrToStringAnsi(versionPtr);
+ Version = GetString(GlConsts.GL_VERSION);
+ Renderer = GetString(GlConsts.GL_RENDERER);
+ Vendor = GetString(GlConsts.GL_VENDOR);
}
public GlInterface(Func n) : this(ConvertNative(n))
@@ -37,7 +39,7 @@ namespace Avalonia.OpenGL
[GlEntryPoint("glClearStencil")]
public GlClearStencil ClearStencil { get; }
- public delegate void GlClearColor(int r, int g, int b, int a);
+ public delegate void GlClearColor(float r, float g, float b, float a);
[GlEntryPoint("glClearColor")]
public GlClearColor ClearColor { get; }
@@ -54,7 +56,15 @@ namespace Avalonia.OpenGL
public delegate IntPtr GlGetString(int v);
[GlEntryPoint("glGetString")]
- public GlGetString GetString { get; }
+ public GlGetString GetStringNative { get; }
+
+ public string GetString(int v)
+ {
+ var ptr = GetStringNative(v);
+ if (ptr != IntPtr.Zero)
+ return Marshal.PtrToStringAnsi(ptr);
+ return null;
+ }
public delegate void GlGetIntegerv(int name, out int rv);
[GlEntryPoint("glGetIntegerv")]
diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs
index 3f41f54363..4881c77034 100644
--- a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs
+++ b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs
@@ -16,7 +16,7 @@ namespace Avalonia.ReactiveUI
///
public class AutoDataTemplateBindingHook : IPropertyBindingHook
{
- private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate(x =>
+ private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate((x, _) =>
{
var control = new ViewModelViewHost();
var context = control.GetObservable(Control.DataContextProperty);
@@ -52,4 +52,4 @@ namespace Avalonia.ReactiveUI
return true;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs b/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs
new file mode 100644
index 0000000000..b404ea57cb
--- /dev/null
+++ b/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs
@@ -0,0 +1,92 @@
+// 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;
+using Avalonia.VisualTree;
+using Avalonia.Controls;
+using System.Threading;
+using System.Reactive.Disposables;
+using System.Reactive.Subjects;
+using System.Reactive.Linq;
+using System.Reactive;
+using ReactiveUI;
+using System;
+using Avalonia.Controls.ApplicationLifetimes;
+using Splat;
+
+namespace Avalonia.ReactiveUI
+{
+ ///
+ /// A ReactiveUI AutoSuspendHelper which initializes suspension hooks for
+ /// Avalonia applications. Call its constructor in your app's composition root,
+ /// before calling the RxApp.SuspensionHost.SetupDefaultSuspendResume method.
+ ///
+ public sealed class AutoSuspendHelper : IEnableLogger, IDisposable
+ {
+ private readonly Subject _shouldPersistState = new Subject();
+ private readonly Subject _isLaunchingNew = new Subject();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Pass in the Application.ApplicationLifetime property.
+ public AutoSuspendHelper(IApplicationLifetime lifetime)
+ {
+ RxApp.SuspensionHost.IsResuming = Observable.Never();
+ RxApp.SuspensionHost.IsLaunchingNew = _isLaunchingNew;
+
+ if (Avalonia.Controls.Design.IsDesignMode)
+ {
+ this.Log().Debug("Design mode detected. AutoSuspendHelper won't persist app state.");
+ RxApp.SuspensionHost.ShouldPersistState = Observable.Never();
+ }
+ else if (lifetime is IControlledApplicationLifetime controlled)
+ {
+ this.Log().Debug("Using IControlledApplicationLifetime events to handle app exit.");
+ controlled.Exit += (sender, args) => OnControlledApplicationLifetimeExit();
+ RxApp.SuspensionHost.ShouldPersistState = _shouldPersistState;
+ }
+ else if (lifetime != null)
+ {
+ var type = lifetime.GetType().FullName;
+ var message = $"Don't know how to detect app exit event for {type}.";
+ throw new NotSupportedException(message);
+ }
+ else
+ {
+ var message = "ApplicationLifetime is null. "
+ + "Ensure you are initializing AutoSuspendHelper "
+ + "after Avalonia application initialization is completed.";
+ throw new ArgumentNullException(message);
+ }
+
+ var errored = new Subject();
+ AppDomain.CurrentDomain.UnhandledException += (o, e) => errored.OnNext(Unit.Default);
+ RxApp.SuspensionHost.ShouldInvalidateState = errored;
+ }
+
+ ///
+ /// Call this method in your App.OnFrameworkInitializationCompleted method.
+ ///
+ public void OnFrameworkInitializationCompleted() => _isLaunchingNew.OnNext(Unit.Default);
+
+ ///
+ /// Disposes internally stored observers.
+ ///
+ public void Dispose()
+ {
+ _shouldPersistState.Dispose();
+ _isLaunchingNew.Dispose();
+ }
+
+ private void OnControlledApplicationLifetimeExit()
+ {
+ this.Log().Debug("Received IControlledApplicationLifetime exit event.");
+ var manual = new ManualResetEvent(false);
+ _shouldPersistState.OnNext(Disposable.Create(() => manual.Set()));
+
+ manual.WaitOne();
+ this.Log().Debug("Completed actions on IControlledApplicationLifetime exit event.");
+ }
+ }
+}
diff --git a/src/Avalonia.Styling/Controls/ChildNameScope.cs b/src/Avalonia.Styling/Controls/ChildNameScope.cs
new file mode 100644
index 0000000000..e6707e71db
--- /dev/null
+++ b/src/Avalonia.Styling/Controls/ChildNameScope.cs
@@ -0,0 +1,73 @@
+using System.Threading.Tasks;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls
+{
+ public class ChildNameScope : INameScope
+ {
+ private readonly INameScope _parentScope;
+ private readonly NameScope _inner = new NameScope();
+
+ public ChildNameScope(INameScope parentScope)
+ {
+ _parentScope = parentScope;
+ }
+
+ public void Register(string name, object element) => _inner.Register(name, element);
+
+ public SynchronousCompletionAsyncResult FindAsync(string name)
+ {
+ var found = Find(name);
+ if (found != null)
+ return new SynchronousCompletionAsyncResult(found);
+ // Not found and both current and parent scope are in completed state
+ if(IsCompleted)
+ return new SynchronousCompletionAsyncResult(null);
+ return DoFindAsync(name);
+ }
+
+ public SynchronousCompletionAsyncResult DoFindAsync(string name)
+ {
+ var src = new SynchronousCompletionAsyncResultSource();
+
+ void ParentSearch()
+ {
+ var parentSearch = _parentScope.FindAsync(name);
+ if (parentSearch.IsCompleted)
+ src.SetResult(parentSearch.GetResult());
+ else
+ parentSearch.OnCompleted(() => src.SetResult(parentSearch.GetResult()));
+ }
+ if (!_inner.IsCompleted)
+ {
+ // Guaranteed to be incomplete at this point
+ var innerSearch = _inner.FindAsync(name);
+ innerSearch.OnCompleted(() =>
+ {
+ var value = innerSearch.GetResult();
+ if (value != null)
+ src.SetResult(value);
+ else ParentSearch();
+ });
+ }
+ else
+ ParentSearch();
+
+ return src.AsyncResult;
+ }
+
+ public object Find(string name)
+ {
+ var found = _inner.Find(name);
+ if (found != null)
+ return found;
+ if (_inner.IsCompleted)
+ return _parentScope.Find(name);
+ return null;
+ }
+
+ public void Complete() => _inner.Complete();
+
+ public bool IsCompleted => _inner.IsCompleted && _parentScope.IsCompleted;
+ }
+}
diff --git a/src/Avalonia.Styling/Controls/INameScope.cs b/src/Avalonia.Styling/Controls/INameScope.cs
index e4ff90c573..b4f4c8e57b 100644
--- a/src/Avalonia.Styling/Controls/INameScope.cs
+++ b/src/Avalonia.Styling/Controls/INameScope.cs
@@ -2,6 +2,8 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
+using System.Threading.Tasks;
+using Avalonia.Utilities;
namespace Avalonia.Controls
{
@@ -10,16 +12,6 @@ namespace Avalonia.Controls
///
public interface INameScope
{
- ///
- /// Raised when an element is registered with the name scope.
- ///
- event EventHandler Registered;
-
- ///
- /// Raised when an element is unregistered with the name scope.
- ///
- event EventHandler Unregistered;
-
///
/// Registers an element in the name scope.
///
@@ -28,16 +20,30 @@ namespace Avalonia.Controls
void Register(string name, object element);
///
- /// Finds a named element in the name scope.
+ /// Finds a named element in the name scope, waits for the scope to be completely populated before returning null
+ /// Returned task is configured to run any continuations synchronously.
+ ///
+ /// The name.
+ /// The element, or null if the name was not found.
+ SynchronousCompletionAsyncResult FindAsync(string name);
+
+ ///
+ /// Finds a named element in the name scope, returns immediately, doesn't traverse the name scope stack
///
/// The name.
/// The element, or null if the name was not found.
object Find(string name);
///
- /// Unregisters an element with the name scope.
+ /// Marks the name scope as completed, no further registrations will be allowed
///
- /// The name.
- void Unregister(string name);
+ void Complete();
+
+ ///
+ /// Returns whether further registrations are allowed on the scope
+ ///
+ bool IsCompleted { get; }
+
+
}
}
diff --git a/src/Avalonia.Styling/Controls/NameScope.cs b/src/Avalonia.Styling/Controls/NameScope.cs
index e3a29af541..ec01a53cfd 100644
--- a/src/Avalonia.Styling/Controls/NameScope.cs
+++ b/src/Avalonia.Styling/Controls/NameScope.cs
@@ -3,7 +3,9 @@
using System;
using System.Collections.Generic;
+using System.Threading.Tasks;
using Avalonia.LogicalTree;
+using Avalonia.Utilities;
namespace Avalonia.Controls
{
@@ -18,44 +20,14 @@ namespace Avalonia.Controls
public static readonly AttachedProperty NameScopeProperty =
AvaloniaProperty.RegisterAttached("NameScope");
+ ///
+ public bool IsCompleted { get; private set; }
+
private readonly Dictionary _inner = new Dictionary();
- ///
- /// Raised when an element is registered with the name scope.
- ///
- public event EventHandler Registered;
-
- ///
- /// Raised when an element is unregistered with the name scope.
- ///
- public event EventHandler Unregistered;
-
- ///
- /// Finds the containing name scope for a styled element.
- ///
- /// The styled element.
- /// The containing name scope.
- public static INameScope FindNameScope(StyledElement styled)
- {
- Contract.Requires(styled != null);
-
- INameScope result;
-
- while (styled != null)
- {
- result = styled as INameScope ?? GetNameScope(styled);
-
- if (result != null)
- {
- return result;
- }
-
- styled = (styled as ILogical)?.LogicalParent as StyledElement;
- }
-
- return null;
- }
-
+ private readonly Dictionary> _pendingSearches =
+ new Dictionary>();
+
///
/// Gets the value of the attached on a styled element.
///
@@ -80,13 +52,11 @@ namespace Avalonia.Controls
styled.SetValue(NameScopeProperty, value);
}
- ///
- /// Registers an element with the name scope.
- ///
- /// The element name.
- /// The element.
+ ///
public void Register(string name, object element)
{
+ if (IsCompleted)
+ throw new InvalidOperationException("NameScope is completed, no further registrations are allowed");
Contract.Requires(name != null);
Contract.Requires(element != null);
@@ -102,15 +72,29 @@ namespace Avalonia.Controls
else
{
_inner.Add(name, element);
- Registered?.Invoke(this, new NameScopeEventArgs(name, element));
+ if (_pendingSearches.TryGetValue(name, out var tcs))
+ {
+ _pendingSearches.Remove(name);
+ tcs.SetResult(element);
+ }
}
}
- ///
- /// Finds a named element in the name scope.
- ///
- /// The name.
- /// The element, or null if the name was not found.
+ public SynchronousCompletionAsyncResult FindAsync(string name)
+ {
+ var found = Find(name);
+ if (found != null)
+ return new SynchronousCompletionAsyncResult(found);
+ if (IsCompleted)
+ return new SynchronousCompletionAsyncResult((object)null);
+ if (!_pendingSearches.TryGetValue(name, out var tcs))
+ // We are intentionally running continuations synchronously here
+ _pendingSearches[name] = tcs = new SynchronousCompletionAsyncResultSource();
+
+ return tcs.AsyncResult;
+ }
+
+ ///
public object Find(string name)
{
Contract.Requires(name != null);
@@ -120,21 +104,14 @@ namespace Avalonia.Controls
return result;
}
- ///
- /// Unregisters an element with the name scope.
- ///
- /// The name.
- public void Unregister(string name)
+ public void Complete()
{
- Contract.Requires(name != null);
-
- object element;
-
- if (_inner.TryGetValue(name, out element))
- {
- _inner.Remove(name);
- Unregistered?.Invoke(this, new NameScopeEventArgs(name, element));
- }
+ IsCompleted = true;
+ foreach (var kp in _pendingSearches)
+ kp.Value.TrySetResult(null);
+ _pendingSearches.Clear();
}
+
+
}
}
diff --git a/src/Avalonia.Styling/Controls/NameScopeExtensions.cs b/src/Avalonia.Styling/Controls/NameScopeExtensions.cs
index 991a97a614..5921acd3b6 100644
--- a/src/Avalonia.Styling/Controls/NameScopeExtensions.cs
+++ b/src/Avalonia.Styling/Controls/NameScopeExtensions.cs
@@ -37,6 +37,25 @@ namespace Avalonia.Controls
return (T)result;
}
+ ///
+ /// Finds a named element in an .
+ ///
+ /// The element type.
+ /// The control to take the name scope from.
+ /// The name.
+ /// The named element or null if not found.
+ public static T Find(this ILogical anchor, string name)
+ where T : class
+ {
+ Contract.Requires(anchor != null);
+ Contract.Requires(name != null);
+ var styledAnchor = anchor as StyledElement;
+ if (styledAnchor == null)
+ return null;
+ var nameScope = (anchor as INameScope) ?? NameScope.GetNameScope(styledAnchor);
+ return nameScope?.Find(name);
+ }
+
///
/// Gets a named element from an or throws if no element of the
/// requested name was found.
@@ -67,6 +86,28 @@ namespace Avalonia.Controls
return (T)result;
}
+ ///
+ /// Gets a named element from an or throws if no element of the
+ /// requested name was found.
+ ///
+ /// The element type.
+ /// The control to take the name scope from.
+ /// The name.
+ /// The named element.
+ public static T Get(this ILogical anchor, string name)
+ where T : class
+ {
+ Contract.Requires(anchor != null);
+ Contract.Requires(name != null);
+
+ var nameScope = (anchor as INameScope) ?? NameScope.GetNameScope((StyledElement)anchor);
+ if (nameScope == null)
+ throw new InvalidOperationException(
+ "The control doesn't have an associated name scope, probably no registrations has been done yet");
+
+ return nameScope.Get(name);
+ }
+
public static INameScope FindNameScope(this ILogical control)
{
Contract.Requires(control != null);
diff --git a/src/Avalonia.Styling/Controls/NameScopeLocator.cs b/src/Avalonia.Styling/Controls/NameScopeLocator.cs
new file mode 100644
index 0000000000..354ed33657
--- /dev/null
+++ b/src/Avalonia.Styling/Controls/NameScopeLocator.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Linq;
+using System.Reactive.Disposables;
+using System.Reactive.Threading.Tasks;
+using System.Reflection;
+using System.Threading.Tasks;
+using Avalonia.LogicalTree;
+using Avalonia.Reactive;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls
+{
+ public class NameScopeLocator
+ {
+ ///
+ /// Tracks a named control relative to another control.
+ ///
+ /// The scope relative from which the object should be resolved.
+ /// The name of the object to find.
+ public static IObservable Track(INameScope scope, string name)
+ {
+ return new NeverEndingSynchronousCompletionAsyncResultObservable(scope.FindAsync(name));
+ }
+
+ // This class is implemented in such weird way because for some reason
+ // our binding system doesn't expect OnCompleted to be ever called and
+ // seems to treat it as binding cancellation or something
+
+ private class NeverEndingSynchronousCompletionAsyncResultObservable : IObservable
+ {
+ private T _value;
+ private SynchronousCompletionAsyncResult? _asyncResult;
+
+ public NeverEndingSynchronousCompletionAsyncResultObservable(SynchronousCompletionAsyncResult task)
+ {
+ if (task.IsCompleted)
+ _value = task.GetResult();
+ else
+ _asyncResult = task;
+ }
+
+ public IDisposable Subscribe(IObserver observer)
+ {
+ if (_asyncResult?.IsCompleted == true)
+ {
+ _value = _asyncResult.Value.GetResult();
+ _asyncResult = null;
+ }
+
+ if (_asyncResult != null)
+ _asyncResult.Value.OnCompleted(() =>
+ {
+ observer.OnNext(_asyncResult.Value.GetResult());
+ });
+ else
+ observer.OnNext(_value);
+
+ return Disposable.Empty;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs
index 7186143bf9..491f38c153 100644
--- a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs
+++ b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs
@@ -15,18 +15,6 @@ namespace Avalonia.LogicalTree
///
public static class ControlLocator
{
- ///
- /// Tracks a named control relative to another control.
- ///
- ///
- /// The control relative from which the other control should be found.
- ///
- /// The name of the control to find.
- public static IObservable Track(ILogical relativeTo, string name)
- {
- return new ControlTracker(relativeTo, name);
- }
-
public static IObservable Track(ILogical relativeTo, int ancestorLevel, Type ancestorType = null)
{
return new ControlTracker(relativeTo, ancestorLevel, ancestorType);
@@ -35,18 +23,10 @@ namespace Avalonia.LogicalTree
private class ControlTracker : LightweightObservableBase
{
private readonly ILogical _relativeTo;
- private readonly string _name;
private readonly int _ancestorLevel;
private readonly Type _ancestorType;
- INameScope _nameScope;
ILogical _value;
- public ControlTracker(ILogical relativeTo, string name)
- {
- _relativeTo = relativeTo;
- _name = name;
- }
-
public ControlTracker(ILogical relativeTo, int ancestorLevel, Type ancestorType)
{
_relativeTo = relativeTo;
@@ -66,12 +46,6 @@ namespace Avalonia.LogicalTree
_relativeTo.AttachedToLogicalTree -= Attached;
_relativeTo.DetachedFromLogicalTree -= Detached;
- if (_nameScope != null)
- {
- _nameScope.Registered -= Registered;
- _nameScope.Unregistered -= Unregistered;
- }
-
_value = null;
}
@@ -88,57 +62,15 @@ namespace Avalonia.LogicalTree
private void Detached(object sender, LogicalTreeAttachmentEventArgs e)
{
- if (_nameScope != null)
- {
- _nameScope.Registered -= Registered;
- _nameScope.Unregistered -= Unregistered;
- }
-
_value = null;
PublishNext(null);
}
- private void Registered(object sender, NameScopeEventArgs e)
- {
- if (e.Name == _name && e.Element is ILogical logical)
- {
- _value = logical;
- PublishNext(logical);
- }
- }
-
- private void Unregistered(object sender, NameScopeEventArgs e)
- {
- if (e.Name == _name)
- {
- _value = null;
- PublishNext(null);
- }
- }
-
private void Update()
{
- if (_name != null)
- {
- _nameScope = _relativeTo.FindNameScope();
-
- if (_nameScope != null)
- {
- _nameScope.Registered += Registered;
- _nameScope.Unregistered += Unregistered;
- _value = _nameScope.Find(_name);
- }
- else
- {
- _value = null;
- }
- }
- else
- {
- _value = _relativeTo.GetLogicalAncestors()
- .Where(x => _ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true)
- .ElementAtOrDefault(_ancestorLevel);
- }
+ _value = _relativeTo.GetLogicalAncestors()
+ .Where(x => _ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true)
+ .ElementAtOrDefault(_ancestorLevel);
}
}
}
diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs
index 146a4c75e7..38c29289b6 100644
--- a/src/Avalonia.Styling/StyledElement.cs
+++ b/src/Avalonia.Styling/StyledElement.cs
@@ -61,7 +61,6 @@ namespace Avalonia
private readonly Classes _classes = new Classes();
private bool _isAttachedToLogicalTree;
private IAvaloniaList _logicalChildren;
- private INameScope _nameScope;
private IResourceDictionary _resources;
private Styles _styles;
private bool _styled;
@@ -82,7 +81,6 @@ namespace Avalonia
///
public StyledElement()
{
- _nameScope = this as INameScope;
_isAttachedToLogicalTree = this is IStyleRoot;
}
@@ -381,7 +379,6 @@ namespace Avalonia
{
if (_initCount == 0 && (!_styled || force))
{
- RegisterWithNameScope();
ApplyStyling();
_styled = true;
}
@@ -571,6 +568,28 @@ namespace Avalonia
});
}
+ protected virtual void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ SetLogicalParent(e.NewItems.Cast());
+ break;
+
+ case NotifyCollectionChangedAction.Remove:
+ ClearLogicalParent(e.OldItems.Cast());
+ break;
+
+ case NotifyCollectionChangedAction.Replace:
+ ClearLogicalParent(e.OldItems.Cast());
+ SetLogicalParent(e.NewItems.Cast());
+ break;
+
+ case NotifyCollectionChangedAction.Reset:
+ throw new NotSupportedException("Reset should not be signaled on LogicalChildren collection");
+ }
+ }
+
///
/// Called when the styled element is added to a rooted logical tree.
///
@@ -675,19 +694,6 @@ namespace Avalonia
AvaloniaLocator.Current.GetService()?.ApplyStyles(this);
}
- private void RegisterWithNameScope()
- {
- if (_nameScope == null)
- {
- _nameScope = NameScope.GetNameScope(this) ?? ((StyledElement)Parent)?._nameScope;
- }
-
- if (Name != null)
- {
- _nameScope?.Register(Name, this);
- }
- }
-
private static void ValidateLogicalChild(ILogical c)
{
if (c == null)
@@ -724,11 +730,6 @@ namespace Avalonia
{
if (_isAttachedToLogicalTree)
{
- if (Name != null)
- {
- _nameScope?.Unregister(Name);
- }
-
_isAttachedToLogicalTree = false;
_styleDetach.OnNext(this);
OnDetachedFromLogicalTree(e);
@@ -757,28 +758,6 @@ namespace Avalonia
OnDataContextChanged(EventArgs.Empty);
}
- private void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
- {
- switch (e.Action)
- {
- case NotifyCollectionChangedAction.Add:
- SetLogicalParent(e.NewItems.Cast());
- break;
-
- case NotifyCollectionChangedAction.Remove:
- ClearLogicalParent(e.OldItems.Cast());
- break;
-
- case NotifyCollectionChangedAction.Replace:
- ClearLogicalParent(e.OldItems.Cast());
- SetLogicalParent(e.NewItems.Cast());
- break;
-
- case NotifyCollectionChangedAction.Reset:
- throw new NotSupportedException("Reset should not be signaled on LogicalChildren collection");
- }
- }
-
private void SetLogicalParent(IEnumerable children)
{
foreach (var i in children)
diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs
index 3702259f35..9312d38c51 100644
--- a/src/Avalonia.Styling/Styling/Setter.cs
+++ b/src/Avalonia.Styling/Styling/Setter.cs
@@ -99,7 +99,6 @@ namespace Avalonia.Styling
if (template != null && !isPropertyOfTypeITemplate)
{
var materialized = template.Build();
- NameScope.SetNameScope((StyledElement)materialized, new NameScope());
value = materialized;
}
diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs
index 789bb6ffd3..a4563110a9 100644
--- a/src/Avalonia.Styling/Styling/Styles.cs
+++ b/src/Avalonia.Styling/Styling/Styles.cs
@@ -180,7 +180,7 @@ namespace Avalonia.Styling
///
public bool TryGetResource(object key, out object value)
{
- if (_resources != null && _resources.TryGetValue(key, out value))
+ if (_resources != null && _resources.TryGetResource(key, out value))
{
return true;
}
diff --git a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml
index 11d8a344d9..788b60892b 100644
--- a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml
+++ b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml
@@ -27,7 +27,6 @@
Background="{TemplateBinding Background}"
Foreground="{TemplateBinding Foreground}"
ItemTemplate="{TemplateBinding ItemTemplate}"
- MemberSelector="{TemplateBinding ValueMemberSelector}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto" />
diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj
index eb2b6789e1..3d525955b4 100644
--- a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj
+++ b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj
@@ -18,6 +18,5 @@
-
diff --git a/src/Avalonia.Themes.Default/Button.xaml b/src/Avalonia.Themes.Default/Button.xaml
index 6ed1f6d8fc..698ddec2a8 100644
--- a/src/Avalonia.Themes.Default/Button.xaml
+++ b/src/Avalonia.Themes.Default/Button.xaml
@@ -22,13 +22,13 @@
-
-
-
+
\ No newline at end of file
diff --git a/src/Avalonia.Themes.Default/ButtonSpinner.xaml b/src/Avalonia.Themes.Default/ButtonSpinner.xaml
index 5417d5fb0b..89fbb9d64d 100644
--- a/src/Avalonia.Themes.Default/ButtonSpinner.xaml
+++ b/src/Avalonia.Themes.Default/ButtonSpinner.xaml
@@ -1,5 +1,6 @@
\ No newline at end of file
+
diff --git a/src/Avalonia.Themes.Default/ComboBox.xaml b/src/Avalonia.Themes.Default/ComboBox.xaml
index ca6c2e372e..0c2d33bc7b 100644
--- a/src/Avalonia.Themes.Default/ComboBox.xaml
+++ b/src/Avalonia.Themes.Default/ComboBox.xaml
@@ -1,5 +1,6 @@
-
+
diff --git a/src/Avalonia.Themes.Default/ItemsControl.xaml b/src/Avalonia.Themes.Default/ItemsControl.xaml
index 7b6671b42c..f3def542fc 100644
--- a/src/Avalonia.Themes.Default/ItemsControl.xaml
+++ b/src/Avalonia.Themes.Default/ItemsControl.xaml
@@ -4,8 +4,7 @@
+ ItemTemplate="{TemplateBinding ItemTemplate}"/>
-
\ No newline at end of file
+
diff --git a/src/Avalonia.Themes.Default/ListBox.xaml b/src/Avalonia.Themes.Default/ListBox.xaml
index 57b0c541b8..59c596bcaa 100644
--- a/src/Avalonia.Themes.Default/ListBox.xaml
+++ b/src/Avalonia.Themes.Default/ListBox.xaml
@@ -18,10 +18,9 @@
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="{TemplateBinding Padding}"
- MemberSelector="{TemplateBinding MemberSelector}"
VirtualizationMode="{TemplateBinding VirtualizationMode}"/>
-
\ No newline at end of file
+
diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml
index be86e8b14c..a794d15577 100644
--- a/src/Avalonia.Themes.Default/MenuItem.xaml
+++ b/src/Avalonia.Themes.Default/MenuItem.xaml
@@ -56,8 +56,7 @@
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
- Margin="2"
- MemberSelector="{TemplateBinding MemberSelector}"/>
+ Margin="2"/>
+ Margin="2"/>
+ ItemTemplate="{TemplateBinding ItemTemplate}">
@@ -18,4 +17,4 @@
-
\ No newline at end of file
+
diff --git a/src/Avalonia.Themes.Default/ToggleButton.xaml b/src/Avalonia.Themes.Default/ToggleButton.xaml
index 41f366fdf9..9e05c38eef 100644
--- a/src/Avalonia.Themes.Default/ToggleButton.xaml
+++ b/src/Avalonia.Themes.Default/ToggleButton.xaml
@@ -22,17 +22,17 @@
-
-
-
-
+
\ No newline at end of file
diff --git a/src/Avalonia.Themes.Default/TreeView.xaml b/src/Avalonia.Themes.Default/TreeView.xaml
index 4e38c6db3a..6ed2fd17b8 100644
--- a/src/Avalonia.Themes.Default/TreeView.xaml
+++ b/src/Avalonia.Themes.Default/TreeView.xaml
@@ -15,8 +15,7 @@
+ Margin="{TemplateBinding Padding}"/>
diff --git a/src/Avalonia.Themes.Default/TreeViewItem.xaml b/src/Avalonia.Themes.Default/TreeViewItem.xaml
index b5e0e7a005..5dd082cf7a 100644
--- a/src/Avalonia.Themes.Default/TreeViewItem.xaml
+++ b/src/Avalonia.Themes.Default/TreeViewItem.xaml
@@ -32,8 +32,7 @@
+ ItemsPanel="{TemplateBinding ItemsPanel}"/>
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/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/RenderLayers.cs b/src/Avalonia.Visuals/Rendering/RenderLayers.cs
index 0ff7862ab6..e82934fbad 100644
--- a/src/Avalonia.Visuals/Rendering/RenderLayers.cs
+++ b/src/Avalonia.Visuals/Rendering/RenderLayers.cs
@@ -8,8 +8,8 @@ namespace Avalonia.Rendering
{
public class RenderLayers : IEnumerable
{
- private List _inner = new List();
- private Dictionary _index = new Dictionary();
+ private readonly List _inner = new List();
+ private readonly Dictionary _index = new Dictionary();
public int Count => _inner.Count;
public RenderLayer this[IVisual layerRoot] => _index[layerRoot];
@@ -56,6 +56,7 @@ namespace Avalonia.Rendering
}
_index.Clear();
+ _inner.Clear();
}
public bool TryGetValue(IVisual layerRoot, out RenderLayer value)
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/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs
index 5602b33280..04f2a7137c 100644
--- a/src/Avalonia.X11/Glx/GlxDisplay.cs
+++ b/src/Avalonia.X11/Glx/GlxDisplay.cs
@@ -90,6 +90,19 @@ namespace Avalonia.X11.Glx
GlInterface = new GlInterface(GlxInterface.GlxGetProcAddress);
if (GlInterface.Version == null)
throw new OpenGlException("GL version string is null, aborting");
+ if (GlInterface.Renderer == null)
+ throw new OpenGlException("GL renderer string is null, aborting");
+
+ if (Environment.GetEnvironmentVariable("AVALONIA_GLX_IGNORE_RENDERER_BLACKLIST") != "1")
+ {
+ var blacklist = AvaloniaLocator.Current.GetService()
+ ?.GlxRendererBlacklist;
+ if (blacklist != null)
+ foreach(var item in blacklist)
+ if (GlInterface.Renderer.Contains(item))
+ throw new OpenGlException($"Renderer '{GlInterface.Renderer}' is blacklisted by '{item}'");
+ }
+
}
public void ClearContext() => Glx.MakeContextCurrent(_x11.Display,
diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs
index cf5902eff7..7bdc61eb28 100644
--- a/src/Avalonia.X11/X11Platform.cs
+++ b/src/Avalonia.X11/X11Platform.cs
@@ -96,6 +96,14 @@ namespace Avalonia
{
public bool UseEGL { get; set; }
public bool UseGpu { get; set; } = true;
+
+ public List GlxRendererBlacklist { get; set; } = new List
+ {
+ // llvmpipe is a software GL rasterizer. If it's returned by glGetString,
+ // that usually means that something in the system is horribly misconfigured
+ // and sometimes attempts to use GLX might cause a segfault
+ "llvmpipe"
+ };
public string WmClass { get; set; } = Assembly.GetEntryAssembly()?.GetName()?.Name ?? "AvaloniaApplication";
public bool? EnableMultiTouch { get; set; }
}
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..5e2ba51caf 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,10 @@ namespace Avalonia.LinuxFramebuffer
{
}
- public Size ClientSize => _fb.PixelSize;
- public IMouseDevice MouseDevice => LinuxFramebufferPlatform.MouseDevice;
+ public Size ClientSize => ScaledSize;
+ public IMouseDevice MouseDevice => new MouseDevice();
public double Scaling => 1;
- public IEnumerable Surfaces => new object[] {_fb};
+ public IEnumerable Surfaces => new object[] {_outputBackend};
public Action Input { get; set; }
public Action Paint { get; set; }
public Action Resized { get; set; }
@@ -73,5 +71,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..2cc1f65202 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,16 @@ 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)
+ where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new FbdevOutput(fbdev));
+
+ public static int StartLinuxDrm(this T builder, string[] args, string card = null)
+ where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card));
+
+ public static int 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..6a76977352
--- /dev/null
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
@@ -0,0 +1,250 @@
+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 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 => 1;
+ }
+
+ 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 92%
rename from src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs
rename to src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs
index 1e25bd4a8a..3021c29015 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs
@@ -2,11 +2,12 @@
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;
@@ -15,7 +16,7 @@ namespace Avalonia.LinuxFramebuffer
private IntPtr _mappedLength;
private IntPtr _mappedAddress;
- public LinuxFramebuffer(string fileName = null, Vector? dpi = null)
+ public FbdevOutput(string fileName = null, Vector? dpi = null)
{
_dpi = dpi ?? new Vector(96, 96);
fileName = fileName ?? Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0";
@@ -85,14 +86,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);
}
}
@@ -123,7 +124,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..01690f07ac
--- /dev/null
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs
@@ -0,0 +1,7 @@
+namespace Avalonia.LinuxFramebuffer.Output
+{
+ public interface IOutputBackend
+ {
+ PixelSize PixelSize { get; }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
index 6f3dabd568..06c5375520 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
+++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
@@ -12,7 +12,6 @@
-
@@ -33,7 +32,6 @@
-
diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/MemberSelectorTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/MemberSelectorTypeConverter.cs
deleted file mode 100644
index 8dc052fe63..0000000000
--- a/src/Markup/Avalonia.Markup.Xaml/Converters/MemberSelectorTypeConverter.cs
+++ /dev/null
@@ -1,24 +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;
-using System.Globalization;
-using Avalonia.Markup.Xaml.Templates;
-
-namespace Avalonia.Markup.Xaml.Converters
-{
- using System.ComponentModel;
-
- public class MemberSelectorTypeConverter : TypeConverter
- {
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- return sourceType == typeof(string);
- }
-
- public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
- {
- return MemberSelector.Parse((string)value);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
index 726f4221f8..a466714136 100644
--- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
@@ -40,7 +40,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
Source = Source,
StringFormat = StringFormat,
RelativeSource = RelativeSource,
- DefaultAnchor = new WeakReference(GetDefaultAnchor(descriptorContext))
+ DefaultAnchor = new WeakReference(GetDefaultAnchor(descriptorContext)),
+ NameScope = new WeakReference(serviceProvider.GetService())
};
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs
index e28828625f..f46ee8787b 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs
@@ -17,6 +17,6 @@ namespace Avalonia.Markup.Xaml.Templates
public Type TargetType { get; set; }
- public IControl Build(ITemplatedControl control) => TemplateContent.Load(Content);
+ public ControlTemplateResult Build(ITemplatedControl control) => TemplateContent.Load(Content);
}
-}
\ No newline at end of file
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
index ec5de8a059..c81a3718a7 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
@@ -32,6 +32,6 @@ namespace Avalonia.Markup.Xaml.Templates
}
}
- public IControl Build(object data) => TemplateContent.Load(Content);
+ public IControl Build(object data) => TemplateContent.Load(Content).Control;
}
-}
\ No newline at end of file
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs
index 6e4d2a3980..0dd4e57a7f 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs
@@ -14,8 +14,8 @@ namespace Avalonia.Markup.Xaml.Templates
public object Content { get; set; }
public IPanel Build()
- => (IPanel)TemplateContent.Load(Content);
+ => (IPanel)TemplateContent.Load(Content).Control;
object ITemplate.Build() => Build();
}
-}
\ No newline at end of file
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs
deleted file mode 100644
index fa91ab60ff..0000000000
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs
+++ /dev/null
@@ -1,48 +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.Controls.Templates;
-using Avalonia.Data;
-using Avalonia.Data.Core;
-using Avalonia.Markup.Parsers;
-using System;
-using System.Reactive.Linq;
-
-namespace Avalonia.Markup.Xaml.Templates
-{
- public class MemberSelector : IMemberSelector
- {
- private string _memberName;
-
- public string MemberName
- {
- get { return _memberName; }
- set
- {
- if (_memberName != value)
- {
- _memberName = value;
- }
- }
- }
-
- public static MemberSelector Parse(string s)
- {
- return new MemberSelector { MemberName = s };
- }
-
- public object Select(object o)
- {
- if (string.IsNullOrEmpty(MemberName))
- {
- return o;
- }
-
- var expression = ExpressionObserverBuilder.Build(o, MemberName);
- object result = AvaloniaProperty.UnsetValue;
-
- expression.Subscribe(x => result = x);
- return (result == AvaloniaProperty.UnsetValue || result is BindingNotification) ? null : result;
- }
- }
-}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs
index b8fec4e8d0..5f36cfe0ac 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs
@@ -13,8 +13,8 @@ namespace Avalonia.Markup.Xaml.Templates
[TemplateContent]
public object Content { get; set; }
- public IControl Build() => TemplateContent.Load(Content);
+ public IControl Build() => TemplateContent.Load(Content).Control;
object ITemplate.Build() => Build();
}
-}
\ No newline at end of file
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs
index ead373d380..6a30c3861a 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs
@@ -4,17 +4,18 @@
using System;
using Avalonia.Controls;
using System.Collections.Generic;
+using Avalonia.Controls.Templates;
namespace Avalonia.Markup.Xaml.Templates
{
public static class TemplateContent
{
- public static IControl Load(object templateContent)
+ public static ControlTemplateResult Load(object templateContent)
{
if (templateContent is Func direct)
{
- return (IControl)direct(null);
+ return (ControlTemplateResult)direct(null);
}
throw new ArgumentException(nameof(templateContent));
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
index bd2b9d2efd..0e2a131afd 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
@@ -51,9 +51,9 @@ namespace Avalonia.Markup.Xaml.Templates
public IControl Build(object data)
{
- var visualTreeForItem = TemplateContent.Load(Content);
+ var visualTreeForItem = TemplateContent.Load(Content).Control;
visualTreeForItem.DataContext = data;
return visualTreeForItem;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs
index 167b75603f..b91d679fba 100644
--- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs
@@ -156,7 +156,7 @@ namespace Avalonia.Markup.Xaml.XamlIl
{
overrideField.SetValue(null,
new Action(
- target => { populateCb(XamlIlRuntimeHelpers.RootServiceProviderV1, target); }));
+ target => { populateCb(XamlIlRuntimeHelpers.CreateRootServiceProviderV2(), target); }));
try
{
return Activator.CreateInstance(targetType);
@@ -170,11 +170,11 @@ namespace Avalonia.Markup.Xaml.XamlIl
var createCb = Expression.Lambda>(
Expression.Convert(Expression.Call(
created.GetMethod(AvaloniaXamlIlCompiler.BuildName), isp), typeof(object)), isp).Compile();
- return createCb(XamlIlRuntimeHelpers.RootServiceProviderV1);
+ return createCb(XamlIlRuntimeHelpers.CreateRootServiceProviderV2());
}
else
{
- populateCb(XamlIlRuntimeHelpers.RootServiceProviderV1, rootInstance);
+ populateCb(XamlIlRuntimeHelpers.CreateRootServiceProviderV2(), rootInstance);
return rootInstance;
}
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs
index c25e1186d0..63c8b1c074 100644
--- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs
@@ -35,6 +35,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
},
ProvideValueTarget = typeSystem.GetType("Avalonia.Markup.Xaml.IProvideValueTarget"),
RootObjectProvider = typeSystem.GetType("Avalonia.Markup.Xaml.IRootObjectProvider"),
+ RootObjectProviderIntermediateRootPropertyName = "IntermediateRootObject",
UriContextProvider = typeSystem.GetType("Avalonia.Markup.Xaml.IUriContext"),
ParentStackProvider =
typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.IAvaloniaXamlIlParentStackProvider"),
@@ -53,9 +54,30 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
ProvideValueTargetPropertyEmitter = XamlIlAvaloniaPropertyHelper.Emit,
};
rv.CustomAttributeResolver = new AttributeResolver(typeSystem, rv);
+ rv.ContextTypeBuilderCallback = (b, c) => EmitNameScopeField(rv, typeSystem, b, c);
return rv;
}
+ public const string ContextNameScopeFieldName = "AvaloniaNameScope";
+
+ private static void EmitNameScopeField(XamlIlLanguageTypeMappings mappings,
+ IXamlIlTypeSystem typeSystem,
+ IXamlIlTypeBuilder typebuilder, IXamlIlEmitter constructor)
+ {
+
+ var nameScopeType = typeSystem.FindType("Avalonia.Controls.INameScope");
+ var field = typebuilder.DefineField(nameScopeType,
+ ContextNameScopeFieldName, true, false);
+ constructor
+ .Ldarg_0()
+ .Ldarg(1)
+ .Ldtype(nameScopeType)
+ .EmitCall(mappings.ServiceProvider.GetMethod(new FindMethodMethodSignature("GetService",
+ typeSystem.FindType("System.Object"), typeSystem.FindType("System.Type"))))
+ .Stfld(field);
+ }
+
+
class AttributeResolver : IXamlIlCustomAttributeResolver
{
private readonly IXamlIlType _typeConverterAttribute;
@@ -81,8 +103,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
var ilist = typeSystem.GetType("System.Collections.Generic.IList`1");
AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")),
typeSystem.GetType("Avalonia.Markup.Xaml.Converters.PointsListTypeConverter"));
- Add("Avalonia.Controls.Templates.IMemberSelector",
- "Avalonia.Markup.Xaml.Converters.MemberSelectorTypeConverter");
Add("Avalonia.Controls.WindowIcon","Avalonia.Markup.Xaml.Converters.IconTypeConverter");
Add("System.Globalization.CultureInfo", "System.ComponentModel.CultureInfoConverter");
Add("System.Uri", "Avalonia.Markup.Xaml.Converters.AvaloniaUriTypeConverter");
diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs
index 33056fa3e8..805b733feb 100644
--- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs
@@ -15,7 +15,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
&& pa.Property.DeclaringType.FullName == "Avalonia.StyledElement")
{
if (context.ParentNodes().FirstOrDefault() is XamlIlManipulationGroupNode mg
- && mg.Children.OfType().Any())
+ && mg.Children.OfType().Any())
return node;
IXamlIlAstValueNode value = null;
@@ -41,54 +41,99 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
Children =
{
pa,
- new ScopeRegistrationNode(value)
+ new AvaloniaNameScopeRegistrationXamlIlNode(value, context.GetAvaloniaTypes())
}
};
}
+ if (!context.ParentNodes().Any()
+ && node is XamlIlValueWithManipulationNode mnode)
+ {
+ mnode.Manipulation = new XamlIlManipulationGroupNode(mnode,
+ new[]
+ {
+ mnode.Manipulation,
+ new HandleRootObjectScopeNode(mnode, context.GetAvaloniaTypes())
+ });
+ }
return node;
}
- class ScopeRegistrationNode : XamlIlAstNode, IXamlIlAstManipulationNode, IXamlIlAstEmitableNode
+ class HandleRootObjectScopeNode : XamlIlAstNode, IXamlIlAstManipulationNode, IXamlIlAstEmitableNode
{
- public IXamlIlAstValueNode Value { get; set; }
- public ScopeRegistrationNode(IXamlIlAstValueNode value) : base(value)
+ private readonly AvaloniaXamlIlWellKnownTypes _types;
+
+ public HandleRootObjectScopeNode(IXamlIlLineInfo lineInfo,
+ AvaloniaXamlIlWellKnownTypes types) : base(lineInfo)
{
- Value = value;
+ _types = types;
}
- public override void VisitChildren(IXamlIlAstVisitor visitor)
- => Value = (IXamlIlAstValueNode)Value.Visit(visitor);
-
public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen)
{
- var exts = context.Configuration.TypeSystem.GetType("Avalonia.Controls.NameScopeExtensions");
- var findNameScope = exts.FindMethod(m => m.Name == "FindNameScope");
- var registerMethod = findNameScope.ReturnType.FindMethod(m => m.Name == "Register");
- using (var targetLoc = context.GetLocal(context.Configuration.WellKnownTypes.Object))
- using (var nameScopeLoc = context.GetLocal(findNameScope.ReturnType))
+ var next = codeGen.DefineLabel();
+ var scopeField = context.RuntimeContext.ContextType.Fields.First(f =>
+ f.Name == AvaloniaXamlIlLanguage.ContextNameScopeFieldName);
+ using (var local = codeGen.LocalsPool.GetLocal(_types.StyledElement))
{
- var exit = codeGen.DefineLabel();
- codeGen
- // var target = {pop}
- .Stloc(targetLoc.Local)
- // var scope = target.FindNameScope()
- .Ldloc(targetLoc.Local)
- .Castclass(findNameScope.Parameters[0])
- .EmitCall(findNameScope)
- .Stloc(nameScopeLoc.Local)
- // if({scope} != null) goto call;
- .Ldloc(nameScopeLoc.Local)
- .Brfalse(exit)
- .Ldloc(nameScopeLoc.Local);
- context.Emit(Value, codeGen, Value.Type.GetClrType());
codeGen
- .Ldloc(targetLoc.Local)
- .EmitCall(registerMethod)
- .MarkLabel(exit);
+ .Isinst(_types.StyledElement)
+ .Dup()
+ .Stloc(local.Local)
+ .Brfalse(next)
+ .Ldloc(local.Local)
+ .Ldloc(context.ContextLocal)
+ .Ldfld(scopeField)
+ .EmitCall(_types.NameScopeSetNameScope, true)
+ .MarkLabel(next)
+ .Ldloc(context.ContextLocal)
+ .Ldfld(scopeField)
+ .EmitCall(_types.INameScopeComplete, true);
}
+
return XamlIlNodeEmitResult.Void(1);
+
}
}
}
+
+ class AvaloniaNameScopeRegistrationXamlIlNode : XamlIlAstNode, IXamlIlAstManipulationNode, IXamlIlAstEmitableNode
+ {
+ private readonly AvaloniaXamlIlWellKnownTypes _types;
+ public IXamlIlAstValueNode Name { get; set; }
+
+ public AvaloniaNameScopeRegistrationXamlIlNode(IXamlIlAstValueNode name, AvaloniaXamlIlWellKnownTypes types) : base(name)
+ {
+ _types = types;
+ Name = name;
+ }
+
+ public override void VisitChildren(IXamlIlAstVisitor visitor)
+ => Name = (IXamlIlAstValueNode)Name.Visit(visitor);
+
+ public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen)
+ {
+ var scopeField = context.RuntimeContext.ContextType.Fields.First(f =>
+ f.Name == AvaloniaXamlIlLanguage.ContextNameScopeFieldName);
+
+ using (var targetLoc = context.GetLocal(context.Configuration.WellKnownTypes.Object))
+ {
+
+ codeGen
+ // var target = {pop}
+ .Stloc(targetLoc.Local)
+ // _context.NameScope.Register(Name, target)
+ .Ldloc(context.ContextLocal)
+ .Ldfld(scopeField);
+
+ context.Emit(Name, codeGen, Name.Type.GetClrType());
+
+ codeGen
+ .Ldloc(targetLoc.Local)
+ .EmitCall(_types.INameScopeRegister, true);
+ }
+
+ return XamlIlNodeEmitResult.Void(1);
+ }
+ }
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
index c054e57380..1efae902c6 100644
--- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
@@ -16,13 +16,21 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlIlMethod AvaloniaObjectSetValueMethod { get; }
public IXamlIlType IDisposable { get; }
public XamlIlTypeWellKnownTypes XamlIlTypes { get; }
+ public XamlIlLanguageTypeMappings XamlIlMappings { get; }
public IXamlIlType Transitions { get; }
public IXamlIlType AssignBindingAttribute { get; }
public IXamlIlType UnsetValueType { get; }
+ public IXamlIlType StyledElement { get; }
+ public IXamlIlType NameScope { get; }
+ public IXamlIlMethod NameScopeSetNameScope { get; }
+ public IXamlIlType INameScope { get; }
+ public IXamlIlMethod INameScopeRegister { get; }
+ public IXamlIlMethod INameScopeComplete { get; }
public AvaloniaXamlIlWellKnownTypes(XamlIlAstTransformationContext ctx)
{
XamlIlTypes = ctx.Configuration.WellKnownTypes;
+ XamlIlMappings = ctx.Configuration.TypeMappings;
AvaloniaObject = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaObject");
IAvaloniaObject = ctx.Configuration.TypeSystem.GetType("Avalonia.IAvaloniaObject");
AvaloniaObjectExtensions = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaObjectExtensions");
@@ -37,8 +45,26 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
AvaloniaProperty,
IBinding, ctx.Configuration.WellKnownTypes.Object);
UnsetValueType = ctx.Configuration.TypeSystem.GetType("Avalonia.UnsetValueType");
+ StyledElement = ctx.Configuration.TypeSystem.GetType("Avalonia.StyledElement");
+ INameScope = ctx.Configuration.TypeSystem.GetType("Avalonia.Controls.INameScope");
+ INameScopeRegister = INameScope.GetMethod(
+ new FindMethodMethodSignature("Register", XamlIlTypes.Void,
+ XamlIlTypes.String, XamlIlTypes.Object)
+ {
+ IsStatic = false, DeclaringOnly = true, IsExactMatch = true
+ });
+ INameScopeComplete = INameScope.GetMethod(
+ new FindMethodMethodSignature("Complete", XamlIlTypes.Void)
+ {
+ IsStatic = false, DeclaringOnly = true, IsExactMatch = true
+ });
+ NameScope = ctx.Configuration.TypeSystem.GetType("Avalonia.Controls.NameScope");
+ NameScopeSetNameScope = NameScope.GetMethod(new FindMethodMethodSignature("SetNameScope",
+ XamlIlTypes.Void, StyledElement, INameScope) {IsStatic = true});
+
AvaloniaObjectSetValueMethod = AvaloniaObject.FindMethod("SetValue", XamlIlTypes.Void,
false, AvaloniaProperty, XamlIlTypes.Object, BindingPriority);
+
}
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs
index 2d8ea643ac..83d70122b3 100644
--- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Avalonia.Controls;
+using Avalonia.Controls.Templates;
using Avalonia.Data;
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedParameter.Global
@@ -17,7 +19,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
var resourceNodes = provider.GetService().Parents
.OfType().ToList();
var rootObject = provider.GetService().RootObject;
- return sp => builder(new DeferredParentServiceProvider(sp, resourceNodes, rootObject));
+ var parentScope = provider.GetService();
+ return sp =>
+ {
+ var scope = parentScope != null ? new ChildNameScope(parentScope) : (INameScope)new NameScope();
+ var obj = builder(new DeferredParentServiceProvider(sp, resourceNodes, rootObject, scope));
+ scope.Complete();
+ return new ControlTemplateResult((IControl)obj, scope);
+ };
}
class DeferredParentServiceProvider :
@@ -27,12 +36,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
{
private readonly IServiceProvider _parentProvider;
private readonly List _parentResourceNodes;
+ private readonly INameScope _nameScope;
public DeferredParentServiceProvider(IServiceProvider parentProvider, List parentResourceNodes,
- object rootObject)
+ object rootObject, INameScope nameScope)
{
_parentProvider = parentProvider;
_parentResourceNodes = parentResourceNodes;
+ _nameScope = nameScope;
RootObject = rootObject;
}
@@ -48,6 +59,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
public object GetService(Type serviceType)
{
+ if (serviceType == typeof(INameScope))
+ return _nameScope;
if (serviceType == typeof(IAvaloniaXamlIlParentStackProvider))
return this;
if (serviceType == typeof(IRootObjectProvider))
@@ -56,6 +69,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
}
public object RootObject { get; }
+ public object IntermediateRootObject => RootObject;
}
@@ -132,12 +146,30 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
}
}
- public static readonly IServiceProvider RootServiceProviderV1 = new RootServiceProvider();
+ [Obsolete("Don't use", true)]
+ public static readonly IServiceProvider RootServiceProviderV1 = new RootServiceProvider(null);
+ // 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
{
+ private readonly INameScope _nameScope;
+
+ public RootServiceProvider(INameScope nameScope)
+ {
+ _nameScope = nameScope;
+ }
+
public object GetService(Type serviceType)
{
+ if (serviceType == typeof(INameScope))
+ return _nameScope;
if (serviceType == typeof(IAvaloniaXamlIlParentStackProvider))
return this;
return null;
diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github
index 894b2c0282..c2ec091f79 160000
--- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github
+++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github
@@ -1 +1 @@
-Subproject commit 894b2c02827fd5eb16a338de5d5b6c9fbc60fef5
+Subproject commit c2ec091f79fb4e1eea629bc823c9c24da7050022
diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs
index 06cc85101a..5ef3dc9753 100644
--- a/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs
@@ -10,7 +10,15 @@ namespace Avalonia.Markup.Xaml
public interface IRootObjectProvider
{
+ ///
+ /// The root object of the xaml file
+ ///
object RootObject { get; }
+ ///
+ /// The "current" root object, contains either the root of the xaml file
+ /// or the root object of the control/data template
+ ///
+ object IntermediateRootObject { get; }
}
public interface IUriContext
diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs
index 0b85e3224d..dbe5800e55 100644
--- a/src/Markup/Avalonia.Markup/Data/Binding.cs
+++ b/src/Markup/Avalonia.Markup/Data/Binding.cs
@@ -5,6 +5,7 @@ using System;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
+using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Data.Core;
using Avalonia.LogicalTree;
@@ -90,6 +91,8 @@ namespace Avalonia.Data
public string StringFormat { get; set; }
public WeakReference DefaultAnchor { get; set; }
+
+ public WeakReference NameScope { get; set; }
///
/// Gets or sets a function used to resolve types from names in the binding path.
@@ -110,7 +113,9 @@ namespace Avalonia.Data
ExpressionObserver observer;
- var (node, mode) = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver);
+ INameScope nameScope = null;
+ NameScope?.TryGetTarget(out nameScope);
+ var (node, mode) = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver, nameScope);
if (ElementName != null)
{
@@ -254,9 +259,12 @@ namespace Avalonia.Data
ExpressionNode node)
{
Contract.Requires(target != null);
-
+
+ NameScope.TryGetTarget(out var scope);
+ if (scope == null)
+ throw new InvalidOperationException("Name scope is null or was already collected");
var result = new ExpressionObserver(
- ControlLocator.Track(target, elementName),
+ NameScopeLocator.Track(scope, elementName),
node,
null);
return result;
diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs
index 33d12e8eb5..19f92149ec 100644
--- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs
+++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs
@@ -97,7 +97,12 @@ namespace Avalonia.Data
var culture = CultureInfo.CurrentCulture;
var converted = converter.Convert(values, targetType, ConverterParameter, culture);
- if (converted == AvaloniaProperty.UnsetValue && FallbackValue != null)
+ if (converted == BindingOperations.DoNothing)
+ {
+ return converted;
+ }
+
+ if (converted == AvaloniaProperty.UnsetValue)
{
converted = FallbackValue;
}
diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs
index 8cb14277ff..f957bcab1e 100644
--- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs
+++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs
@@ -1,5 +1,6 @@
using System;
using System.Reactive;
+using Avalonia.Controls;
using Avalonia.Data.Core;
using Avalonia.Utilities;
@@ -7,7 +8,8 @@ namespace Avalonia.Markup.Parsers
{
public static class ExpressionObserverBuilder
{
- internal static (ExpressionNode Node, SourceMode Mode) Parse(string expression, bool enableValidation = false, Func typeResolver = null)
+ internal static (ExpressionNode Node, SourceMode Mode) Parse(string expression, bool enableValidation = false, Func typeResolver = null,
+ INameScope nameScope = null)
{
if (string.IsNullOrWhiteSpace(expression))
{
@@ -15,7 +17,7 @@ namespace Avalonia.Markup.Parsers
}
var reader = new CharacterReader(expression.AsSpan());
- var parser = new ExpressionParser(enableValidation, typeResolver);
+ var parser = new ExpressionParser(enableValidation, typeResolver, nameScope);
var node = parser.Parse(ref reader);
if (!reader.End)
diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs
index a1350a8393..ed3e65feda 100644
--- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs
+++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs
@@ -7,6 +7,7 @@ using Avalonia.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
+using Avalonia.Controls;
namespace Avalonia.Markup.Parsers
{
@@ -20,10 +21,12 @@ namespace Avalonia.Markup.Parsers
{
private readonly bool _enableValidation;
private readonly Func _typeResolver;
+ private readonly INameScope _nameScope;
- public ExpressionParser(bool enableValidation, Func typeResolver)
+ public ExpressionParser(bool enableValidation, Func typeResolver, INameScope nameScope)
{
_typeResolver = typeResolver;
+ _nameScope = nameScope;
_enableValidation = enableValidation;
}
@@ -213,7 +216,7 @@ namespace Avalonia.Markup.Parsers
throw new ExpressionParseException(r.Position, "Element name expected after '#'.");
}
- nodes.Add(new ElementNameNode(name.ToString()));
+ nodes.Add(new ElementNameNode(_nameScope, name.ToString()));
return State.AfterMember;
}
diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs
index f09efca7d0..981e93c534 100644
--- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs
+++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs
@@ -1,4 +1,5 @@
using System;
+using Avalonia.Controls;
using Avalonia.Data.Core;
using Avalonia.LogicalTree;
@@ -6,11 +7,13 @@ namespace Avalonia.Markup.Parsers.Nodes
{
internal class ElementNameNode : ExpressionNode
{
+ private readonly WeakReference _nameScope;
private readonly string _name;
private IDisposable _subscription;
- public ElementNameNode(string name)
+ public ElementNameNode(INameScope nameScope, string name)
{
+ _nameScope = new WeakReference(nameScope);
_name = name;
}
@@ -18,14 +21,10 @@ namespace Avalonia.Markup.Parsers.Nodes
protected override void StartListeningCore(WeakReference reference)
{
- if (reference.Target is ILogical logical)
- {
- _subscription = ControlLocator.Track(logical, _name).Subscribe(ValueChanged);
- }
+ if (_nameScope.TryGetTarget(out var scope))
+ _subscription = NameScopeLocator.Track(scope, _name).Subscribe(ValueChanged);
else
- {
_subscription = null;
- }
}
protected override void StopListeningCore()
diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
index ee4564ff35..47e651ce91 100644
--- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
+++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
@@ -101,6 +101,7 @@ namespace Avalonia.Skia
public SKCanvas Canvas { get; }
SKCanvas ISkiaDrawingContextImpl.SkCanvas => Canvas;
+ GRContext ISkiaDrawingContextImpl.GrContext => _grContext;
///
public void Clear(Color color)
@@ -153,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))))
{
@@ -162,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;
@@ -183,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))
{
@@ -560,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/ISkiaDrawingContextImpl.cs b/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs
index ff82990c47..a9b91384d7 100644
--- a/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs
+++ b/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs
@@ -6,5 +6,6 @@ namespace Avalonia.Skia
public interface ISkiaDrawingContextImpl : IDrawingContextImpl
{
SKCanvas SkCanvas { get; }
+ GRContext GrContext { get; }
}
}
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/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
index bc7fc1c9fa..097aba6dc9 100644
--- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
+++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
@@ -608,6 +608,14 @@ namespace Avalonia.Win32.Interop
GWL_USERDATA = -21
}
+ public enum MenuCharParam
+ {
+ MNC_IGNORE = 0,
+ MNC_CLOSE = 1,
+ MNC_EXECUTE = 2,
+ MNC_SELECT = 3
+ }
+
[StructLayout(LayoutKind.Sequential)]
public struct RGBQUAD
{
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index 5cc148fa0d..2f7805884d 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.cs
@@ -514,6 +514,10 @@ namespace Avalonia.Win32
KeyInterop.KeyFromVirtualKey(ToInt32(wParam)), WindowsKeyboardDevice.Instance.Modifiers);
break;
+ case UnmanagedMethods.WindowsMessage.WM_MENUCHAR:
+ // mute the system beep
+ return (IntPtr)((Int32)UnmanagedMethods.MenuCharParam.MNC_CLOSE << 16);
+
case UnmanagedMethods.WindowsMessage.WM_KEYUP:
case UnmanagedMethods.WindowsMessage.WM_SYSKEYUP:
e = new RawKeyEventArgs(
diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs
index b12b2e3c31..428f878945 100644
--- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs
+++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Data;
+using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Base.UnitTests
@@ -34,10 +35,10 @@ namespace Avalonia.Base.UnitTests
{
var target = new Class1();
- target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(6));
- target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(new Exception(), BindingErrorType.Error));
- target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(new Exception(), BindingErrorType.DataValidationError));
- target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(7));
+ target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(6));
+ target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(new Exception(), BindingErrorType.Error));
+ target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(new Exception(), BindingErrorType.DataValidationError));
+ target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(7));
Assert.Equal(
new[]
@@ -73,7 +74,7 @@ namespace Avalonia.Base.UnitTests
var source = new Subject();
var target = new Class1
{
- [!Class1.ValidatedDirectProperty] = source.ToBinding(),
+ [!Class1.ValidatedDirectIntProperty] = source.ToBinding(),
};
source.OnNext(new BindingNotification(6));
@@ -92,6 +93,30 @@ namespace Avalonia.Base.UnitTests
target.Notifications.AsEnumerable());
}
+ [Fact]
+ public void Bound_Validated_Direct_String_Property_Can_Be_Set_To_Null()
+ {
+ var source = new ViewModel
+ {
+ StringValue = "foo",
+ };
+
+ var target = new Class1
+ {
+ [!Class1.ValidatedDirectStringProperty] = new Binding
+ {
+ Path = nameof(ViewModel.StringValue),
+ Source = source,
+ },
+ };
+
+ Assert.Equal("foo", target.ValidatedDirectString);
+
+ source.StringValue = null;
+
+ Assert.Null(target.ValidatedDirectString);
+ }
+
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty NonValidatedProperty =
@@ -104,15 +129,23 @@ namespace Avalonia.Base.UnitTests
o => o.NonValidatedDirect,
(o, v) => o.NonValidatedDirect = v);
- public static readonly DirectProperty ValidatedDirectProperty =
+ public static readonly DirectProperty ValidatedDirectIntProperty =
AvaloniaProperty.RegisterDirect(
- nameof(ValidatedDirect),
- o => o.ValidatedDirect,
- (o, v) => o.ValidatedDirect = v,
+ nameof(ValidatedDirectInt),
+ o => o.ValidatedDirectInt,
+ (o, v) => o.ValidatedDirectInt = v,
+ enableDataValidation: true);
+
+ public static readonly DirectProperty ValidatedDirectStringProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(ValidatedDirectString),
+ o => o.ValidatedDirectString,
+ (o, v) => o.ValidatedDirectString = v,
enableDataValidation: true);
private int _nonValidatedDirect;
- private int _direct;
+ private int _directInt;
+ private string _directString;
public int NonValidated
{
@@ -122,14 +155,20 @@ namespace Avalonia.Base.UnitTests
public int NonValidatedDirect
{
- get { return _direct; }
+ get { return _directInt; }
set { SetAndRaise(NonValidatedDirectProperty, ref _nonValidatedDirect, value); }
}
- public int ValidatedDirect
+ public int ValidatedDirectInt
+ {
+ get { return _directInt; }
+ set { SetAndRaise(ValidatedDirectIntProperty, ref _directInt, value); }
+ }
+
+ public string ValidatedDirectString
{
- get { return _direct; }
- set { SetAndRaise(ValidatedDirectProperty, ref _direct, value); }
+ get { return _directString; }
+ set { SetAndRaise(ValidatedDirectStringProperty, ref _directString, value); }
}
public IList Notifications { get; } = new List();
@@ -139,5 +178,16 @@ namespace Avalonia.Base.UnitTests
Notifications.Add(notification);
}
}
+
+ public class ViewModel : NotifyingBase
+ {
+ private string _stringValue;
+
+ public string StringValue
+ {
+ get { return _stringValue; }
+ set { _stringValue = value; RaisePropertyChanged(); }
+ }
+ }
}
}
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 b10929cbdc..015a122677 100644
--- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
@@ -1012,23 +1012,23 @@ namespace Avalonia.Controls.UnitTests
}
private IControlTemplate CreateTemplate()
{
- return new FuncControlTemplate(control =>
+ return new FuncControlTemplate((control, scope) =>
{
var textBox =
new TextBox
{
Name = "PART_TextBox"
- };
+ }.RegisterInNameScope(scope);
var listbox =
new ListBox
{
Name = "PART_SelectingItemsControl"
- };
+ }.RegisterInNameScope(scope);
var popup =
new Popup
{
Name = "PART_Popup"
- };
+ }.RegisterInNameScope(scope);
var panel = new Panel();
panel.Children.Add(textBox);
diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs
index 515e18434d..b16ac6bb8e 100644
--- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs
@@ -302,7 +302,7 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal("FooBar", target.SelectedItem);
}
- private Control CreateTemplate(Carousel control)
+ private Control CreateTemplate(Carousel control, INameScope scope)
{
return new CarouselPresenter
{
@@ -312,7 +312,7 @@ namespace Avalonia.Controls.UnitTests
[~CarouselPresenter.ItemsPanelProperty] = control[~Carousel.ItemsPanelProperty],
[~CarouselPresenter.SelectedIndexProperty] = control[~Carousel.SelectedIndexProperty],
[~CarouselPresenter.PageTransitionProperty] = control[~Carousel.PageTransitionProperty],
- };
+ }.RegisterInNameScope(scope);
}
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
index 70ec6c1408..599e214b31 100644
--- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
@@ -80,7 +80,7 @@ namespace Avalonia.Controls.UnitTests
private FuncControlTemplate GetTemplate()
{
- return new FuncControlTemplate(parent =>
+ return new FuncControlTemplate((parent, scope) =>
{
return new Panel
{
@@ -94,7 +94,7 @@ namespace Avalonia.Controls.UnitTests
new ToggleButton
{
Name = "toggle",
- },
+ }.RegisterInNameScope(scope),
new Popup
{
Name = "PART_Popup",
@@ -102,8 +102,8 @@ namespace Avalonia.Controls.UnitTests
{
Name = "PART_ItemsPresenter",
[!ItemsPresenter.ItemsProperty] = parent[!ComboBox.ItemsProperty],
- }
- }
+ }.RegisterInNameScope(scope)
+ }.RegisterInNameScope(scope)
}
};
});
diff --git a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs
index c17893604c..93355a22f2 100644
--- a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs
@@ -126,7 +126,7 @@ namespace Avalonia.Controls.UnitTests
var target = new ContentControl
{
Template = GetTemplate(),
- ContentTemplate = new FuncDataTemplate(_ => new Canvas()),
+ ContentTemplate = new FuncDataTemplate((_, __) => new Canvas()),
};
target.Content = "Foo";
@@ -302,8 +302,8 @@ namespace Avalonia.Controls.UnitTests
var target = new ContentControl
{
- Template = new FuncControlTemplate(_ => presenter),
- ContentTemplate = new FuncDataTemplate(x => new Canvas()),
+ Template = new FuncControlTemplate((_, __) => presenter),
+ ContentTemplate = new FuncDataTemplate((_, __) => new Canvas()),
Content = "foo",
};
@@ -333,7 +333,7 @@ namespace Avalonia.Controls.UnitTests
private FuncControlTemplate GetTemplate()
{
- return new FuncControlTemplate(parent =>
+ return new FuncControlTemplate((parent, scope) =>
{
return new Border
{
@@ -343,7 +343,7 @@ namespace Avalonia.Controls.UnitTests
Name = "PART_ContentPresenter",
[~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty],
[~ContentPresenter.ContentTemplateProperty] = parent[~ContentControl.ContentTemplateProperty],
- }
+ }.RegisterInNameScope(scope)
};
});
}
diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
index 6482fcb4da..58d205deaa 100644
--- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
@@ -16,6 +16,60 @@ namespace Avalonia.Controls.UnitTests
private Mock popupImpl;
private MouseTestHelper _mouse = new MouseTestHelper();
+ [Fact]
+ public void Opening_Raises_Single_Opened_Event()
+ {
+ using (Application())
+ {
+ var sut = new ContextMenu();
+ var target = new Panel
+ {
+ ContextMenu = sut
+ };
+
+ new Window { Content = target };
+
+ int openedCount = 0;
+
+ sut.MenuOpened += (sender, args) =>
+ {
+ openedCount++;
+ };
+
+ sut.Open(null);
+
+ Assert.Equal(1, openedCount);
+ }
+ }
+
+ [Fact]
+ public void Closing_Raises_Single_Closed_Event()
+ {
+ using (Application())
+ {
+ var sut = new ContextMenu();
+ var target = new Panel
+ {
+ ContextMenu = sut
+ };
+
+ new Window { Content = target };
+
+ sut.Open(null);
+
+ int closedCount = 0;
+
+ sut.MenuClosed += (sender, args) =>
+ {
+ closedCount++;
+ };
+
+ sut.Close();
+
+ Assert.Equal(1, closedCount);
+ }
+ }
+
[Fact]
public void Clicking_On_Control_Toggles_ContextMenu()
{
diff --git a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs
index 936d700ad0..7de80f72cc 100644
--- a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs
@@ -93,28 +93,28 @@ namespace Avalonia.Controls.UnitTests
private IControlTemplate CreateTemplate()
{
- return new FuncControlTemplate(control =>
+ return new FuncControlTemplate((control, scope) =>
{
var textBox =
new TextBox
{
Name = "PART_TextBox"
- };
+ }.RegisterInNameScope(scope);
var button =
new Button
{
Name = "PART_Button"
- };
+ }.RegisterInNameScope(scope);
var calendar =
new Calendar
{
Name = "PART_Calendar"
- };
+ }.RegisterInNameScope(scope);
var popup =
new Popup
{
Name = "PART_Popup"
- };
+ }.RegisterInNameScope(scope);
var panel = new Panel();
panel.Children.Add(textBox);
diff --git a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs
index 9b4be59647..70410dff0d 100644
--- a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs
@@ -118,7 +118,7 @@ namespace Avalonia.Controls.UnitTests.Generators
{
var owner = new Decorator();
var target = new ItemContainerGenerator(owner);
- var container = (ContentPresenter)target.Materialize(0, "foo", null).ContainerControl;
+ var container = (ContentPresenter)target.Materialize(0, "foo").ContainerControl;
Assert.Equal("foo", container.Content);
@@ -135,7 +135,7 @@ namespace Avalonia.Controls.UnitTests.Generators
{
var owner = new Decorator();
var target = new ItemContainerGenerator(owner, ListBoxItem.ContentProperty, null);
- var container = (ListBoxItem)target.Materialize(0, "foo", null).ContainerControl;
+ var container = (ListBoxItem)target.Materialize(0, "foo").ContainerControl;
Assert.Equal("foo", container.Content);
@@ -156,7 +156,7 @@ namespace Avalonia.Controls.UnitTests.Generators
foreach (var item in items)
{
- var container = generator.Materialize(index++, item, null);
+ var container = generator.Materialize(index++, item);
result.Add(container);
}
diff --git a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs
index f63c0efbf9..05954cbcd2 100644
--- a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs
@@ -35,7 +35,7 @@ namespace Avalonia.Controls.UnitTests.Generators
foreach (var item in items)
{
- var container = generator.Materialize(index++, item, null);
+ var container = generator.Materialize(index++, item);
result.Add(container);
}
diff --git a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs
index a1ba608ec6..a790d2fca1 100644
--- a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs
@@ -20,6 +20,7 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Detects_Horizontal_Orientation()
{
+ GridSplitter splitter;
var grid = new Grid()
{
RowDefinitions = new RowDefinitions("*,Auto,*"),
@@ -27,7 +28,7 @@ namespace Avalonia.Controls.UnitTests
Children =
{
new Border { [Grid.RowProperty] = 0 },
- new GridSplitter { [Grid.RowProperty] = 1, Name = "splitter" },
+ (splitter = new GridSplitter { [Grid.RowProperty] = 1 }),
new Border { [Grid.RowProperty] = 2 }
}
};
@@ -35,12 +36,13 @@ namespace Avalonia.Controls.UnitTests
var root = new TestRoot { Child = grid };
root.Measure(new Size(100, 300));
root.Arrange(new Rect(0, 0, 100, 300));
- Assert.Contains(grid.FindControl("splitter").Classes, ":horizontal".Equals);
+ Assert.Contains(splitter.Classes, ":horizontal".Equals);
}
[Fact]
public void Detects_Vertical_Orientation()
{
+ GridSplitter splitter;
var grid = new Grid()
{
ColumnDefinitions = new ColumnDefinitions("*,Auto,*"),
@@ -48,7 +50,7 @@ namespace Avalonia.Controls.UnitTests
Children =
{
new Border { [Grid.ColumnProperty] = 0 },
- new GridSplitter { [Grid.ColumnProperty] = 1, Name = "splitter" },
+ (splitter = new GridSplitter { [Grid.ColumnProperty] = 1}),
new Border { [Grid.ColumnProperty] = 2 },
}
};
@@ -56,12 +58,13 @@ namespace Avalonia.Controls.UnitTests
var root = new TestRoot { Child = grid };
root.Measure(new Size(100, 300));
root.Arrange(new Rect(0, 0, 100, 300));
- Assert.Contains(grid.FindControl("splitter").Classes, ":vertical".Equals);
+ Assert.Contains(splitter.Classes, ":vertical".Equals);
}
[Fact]
public void Detects_With_Both_Auto()
{
+ GridSplitter splitter;
var grid = new Grid()
{
ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"),
@@ -69,7 +72,7 @@ namespace Avalonia.Controls.UnitTests
Children =
{
new Border { [Grid.ColumnProperty] = 0 },
- new GridSplitter { [Grid.ColumnProperty] = 1, Name = "splitter" },
+ (splitter = new GridSplitter { [Grid.ColumnProperty] = 1}),
new Border { [Grid.ColumnProperty] = 2 },
}
};
@@ -77,7 +80,7 @@ namespace Avalonia.Controls.UnitTests
var root = new TestRoot { Child = grid };
root.Measure(new Size(100, 300));
root.Arrange(new Rect(0, 0, 100, 300));
- Assert.Contains(grid.FindControl("splitter").Classes, ":vertical".Equals);
+ Assert.Contains(splitter.Classes, ":vertical".Equals);
}
[Fact]
@@ -129,13 +132,14 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void In_First_Position_Doesnt_Throw_Exception()
{
+ GridSplitter splitter;
var grid = new Grid()
{
ColumnDefinitions = new ColumnDefinitions("Auto,*,*"),
RowDefinitions = new RowDefinitions("*,*"),
Children =
{
- new GridSplitter { [Grid.ColumnProperty] = 0, Name = "splitter" },
+ (splitter = new GridSplitter { [Grid.ColumnProperty] = 0} ),
new Border { [Grid.ColumnProperty] = 1 },
new Border { [Grid.ColumnProperty] = 2 },
}
@@ -144,7 +148,6 @@ namespace Avalonia.Controls.UnitTests
var root = new TestRoot { Child = grid };
root.Measure(new Size(100, 300));
root.Arrange(new Rect(0, 0, 100, 300));
- var splitter = grid.FindControl