Browse Source

Merge branch 'master' into fixes/windowsIME

pull/11809/head
Benedikt Stebner 3 years ago
committed by GitHub
parent
commit
454fd84b10
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      samples/AppWithoutLifetime/MainWindow.axaml.cs
  2. 6
      samples/AppWithoutLifetime/Sub.axaml.cs
  3. 9
      samples/ControlCatalog.Android/ControlCatalog.Android.csproj
  4. 12
      samples/ControlCatalog.Android/MainActivity.cs
  5. 66
      samples/ControlCatalog.Android/Resources/drawable-night-v31/avalonia_anim.xml
  6. 71
      samples/ControlCatalog.Android/Resources/drawable-v31/avalonia_anim.xml
  7. 21
      samples/ControlCatalog.Android/Resources/values-v31/styles.xml
  8. 11
      samples/ControlCatalog.Android/Resources/values/styles.xml
  9. 35
      samples/ControlCatalog.Android/SplashActivity.cs
  10. 10
      samples/ControlCatalog/Pages/CompositionPage.axaml.cs
  11. 16
      samples/ControlCatalog/Pages/GesturePage.cs
  12. 4
      samples/GpuInterop/DrawingSurfaceDemoBase.cs
  13. 5
      samples/MobileSandbox.Android/MainActivity.cs
  14. 10
      samples/MobileSandbox.Android/MobileSandbox.Android.csproj
  15. 66
      samples/MobileSandbox.Android/Resources/drawable-night-v31/avalonia_anim.xml
  16. 71
      samples/MobileSandbox.Android/Resources/drawable-v31/avalonia_anim.xml
  17. 4
      samples/MobileSandbox.Android/Resources/values-night/colors.xml
  18. 16
      samples/MobileSandbox.Android/Resources/values-v31/styles.xml
  19. 7
      samples/MobileSandbox.Android/Resources/values/styles.xml
  20. 17
      samples/MobileSandbox.Android/SplashActivity.cs
  21. 4
      samples/SafeAreaDemo.Android/MainActivity.cs
  22. 66
      samples/SafeAreaDemo.Android/Resources/drawable-night-v31/avalonia_anim.xml
  23. 71
      samples/SafeAreaDemo.Android/Resources/drawable-v31/avalonia_anim.xml
  24. 4
      samples/SafeAreaDemo.Android/Resources/values-night/colors.xml
  25. 16
      samples/SafeAreaDemo.Android/Resources/values-v31/styles.xml
  26. 7
      samples/SafeAreaDemo.Android/Resources/values/styles.xml
  27. 13
      samples/SafeAreaDemo.Android/SafeAreaDemo.Android.csproj
  28. 30
      samples/SafeAreaDemo.Android/SplashActivity.cs
  29. 6
      samples/SafeAreaDemo/Views/MainView.xaml.cs
  30. 7
      samples/Sandbox/MainWindow.axaml
  31. 60
      src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs
  32. 88
      src/Android/Avalonia.Android/AvaloniaMainActivity.cs
  33. 40
      src/Android/Avalonia.Android/AvaloniaSplashActivity.cs
  34. 119
      src/Avalonia.Base/Animation/AnimationInstance`1.cs
  35. 1
      src/Avalonia.Base/Input/Pointer.cs
  36. 24
      src/Avalonia.Base/Media/BoxShadow.cs
  37. 10
      src/Avalonia.Base/Media/BoxShadows.cs
  38. 4
      src/Avalonia.Base/Media/MediaContext.cs
  39. 2
      src/Avalonia.Base/Platform/IDrawingContextImpl.cs
  40. 4
      src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
  41. 25
      src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs
  42. 6
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  43. 2
      src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs
  44. 2
      src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
  45. 59
      src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs
  46. 43
      src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs
  47. 178
      src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs
  48. 33
      src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs
  49. 7
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs
  50. 20
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  51. 2
      src/Avalonia.Base/Rendering/Composition/Visual.cs
  52. 1
      src/Avalonia.Base/Rendering/RenderLoop.cs
  53. 30
      src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
  54. 11
      src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs
  55. 5
      src/Avalonia.Base/Threading/Dispatcher.Invoke.cs
  56. 48
      src/Avalonia.Base/Vector.cs
  57. 145
      src/Avalonia.Base/Vector3D.cs
  58. 15
      src/Avalonia.Base/composition-schema.xml
  59. 14
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  60. 7
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  61. 37
      src/Avalonia.Controls/Control.cs
  62. 86
      src/Avalonia.Controls/ItemsControl.cs
  63. 15
      src/Avalonia.Controls/LayoutTransformControl.cs
  64. 8
      src/Avalonia.Controls/MenuItem.cs
  65. 120
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  66. 83
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  67. 36
      src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs
  68. 6
      src/Avalonia.Controls/ToggleSwitch.cs
  69. 156
      src/Avalonia.Controls/Utils/VirtualizingSnapPointsList.cs
  70. 103
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  71. 104
      src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs
  72. 145
      src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml
  73. 46
      src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
  74. 6
      src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs
  75. 4
      src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs
  76. 2
      src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml
  77. 2
      src/Avalonia.Themes.Fluent/Controls/ListBox.xaml
  78. 47
      src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml
  79. 2
      src/Avalonia.Themes.Fluent/FluentTheme.xaml
  80. 35
      src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs
  81. 2
      src/Avalonia.Themes.Simple/Controls/ItemsControl.xaml
  82. 2
      src/Avalonia.Themes.Simple/Controls/ListBox.xaml
  83. 3
      src/Avalonia.Themes.Simple/Controls/TransitioningContentControl.xaml
  84. 2
      src/Avalonia.X11/X11Clipboard.cs
  85. 1
      src/tools/Avalonia.Generators/Avalonia.Generators.csproj
  86. 84
      tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs
  87. 13
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs
  88. 8
      tests/Avalonia.Base.UnitTests/Media/BoxShadowTests.cs
  89. 68
      tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs
  90. 19
      tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs
  91. 51
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  92. 12
      tests/Avalonia.Controls.UnitTests/MenuItemTests.cs
  93. 31
      tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
  94. 1
      tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj

5
samples/AppWithoutLifetime/MainWindow.axaml.cs

@ -17,10 +17,11 @@ public partial class MainWindow : Window
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
protected override void OnLoaded() /// <inheritdoc/>
protected override void OnLoaded(RoutedEventArgs e)
{ {
this.AttachDevTools(); this.AttachDevTools();
base.OnLoaded(); base.OnLoaded(e);
} }
public void Open(object sender, RoutedEventArgs e) public void Open(object sender, RoutedEventArgs e)

6
samples/AppWithoutLifetime/Sub.axaml.cs

@ -1,5 +1,6 @@
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
namespace AppWithoutLifetime; namespace AppWithoutLifetime;
@ -16,9 +17,10 @@ public partial class Sub : Window
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
protected override void OnLoaded() /// <inheritdoc/>
protected override void OnLoaded(RoutedEventArgs e)
{ {
this.AttachDevTools(); this.AttachDevTools();
base.OnLoaded(); base.OnLoaded(e);
} }
} }

9
samples/ControlCatalog.Android/ControlCatalog.Android.csproj

@ -10,6 +10,11 @@
<AndroidPackageFormat>apk</AndroidPackageFormat> <AndroidPackageFormat>apk</AndroidPackageFormat>
<MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver> <MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<None Remove="Resources\drawable-night-v31\avalonia_anim.xml" />
<None Remove="Resources\drawable-v31\avalonia_anim.xml" />
<None Remove="Resources\values-v31\styles.xml" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<AndroidResource Include="..\..\build\Assets\Icon.png"> <AndroidResource Include="..\..\build\Assets\Icon.png">
<Link>Resources\drawable\Icon.png</Link> <Link>Resources\drawable\Icon.png</Link>
@ -37,6 +42,10 @@
<AndroidEnvironment Condition="'$(IsEmulator)'!='True'" Include="environment.device.txt" /> <AndroidEnvironment Condition="'$(IsEmulator)'!='True'" Include="environment.device.txt" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.0.1" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Android\Avalonia.Android\Avalonia.Android.csproj" /> <ProjectReference Include="..\..\src\Android\Avalonia.Android\Avalonia.Android.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" /> <ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />

12
samples/ControlCatalog.Android/MainActivity.cs

@ -5,8 +5,16 @@ using Avalonia.Android;
namespace ControlCatalog.Android namespace ControlCatalog.Android
{ {
[Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.Main", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
public class MainActivity : AvaloniaMainActivity public class MainActivity : AvaloniaMainActivity<App>
{ {
protected override Avalonia.AppBuilder CustomizeAppBuilder(Avalonia.AppBuilder builder)
{
return base.CustomizeAppBuilder(builder)
.AfterSetup(_ =>
{
Pages.EmbedSample.Implementation = new EmbedSampleAndroid();
});
}
} }
} }

66
samples/ControlCatalog.Android/Resources/drawable-night-v31/avalonia_anim.xml

@ -0,0 +1,66 @@
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<group
android:name="wrapper"
android:translateX="21"
android:translateY="21">
<group android:name="group">
<path
android:name="path"
android:pathData="M 74.853 85.823 L 75.368 85.823 C 80.735 85.823 85.144 81.803 85.761 76.602 L 85.836 41.76 C 85.225 18.593 66.254 0 42.939 0 C 19.24 0 0.028 19.212 0.028 42.912 C 0.028 66.357 18.831 85.418 42.18 85.823 L 74.853 85.823 Z"
android:strokeWidth="1"/>
<path
android:name="path_1"
android:pathData="M 43.059 14.614 C 29.551 14.614 18.256 24.082 15.445 36.743 C 18.136 37.498 20.109 39.968 20.109 42.899 C 20.109 45.831 18.136 48.301 15.445 49.055 C 18.256 61.716 29.551 71.184 43.059 71.184 C 47.975 71.184 52.599 69.93 56.628 67.723 L 56.628 70.993 L 71.344 70.993 L 71.344 44.072 C 71.357 43.714 71.344 43.26 71.344 42.899 C 71.344 27.278 58.68 14.614 43.059 14.614 Z M 29.51 42.899 C 29.51 35.416 35.576 29.35 43.059 29.35 C 50.541 29.35 56.607 35.416 56.607 42.899 C 56.607 50.382 50.541 56.448 43.059 56.448 C 35.576 56.448 29.51 50.382 29.51 42.899 Z"
android:strokeWidth="1"
android:fillType="evenOdd"/>
<path
android:name="path_2"
android:pathData="M 18.105 42.88 C 18.105 45.38 16.078 47.407 13.579 47.407 C 11.079 47.407 9.052 45.38 9.052 42.88 C 9.052 40.381 11.079 38.354 13.579 38.354 C 16.078 38.354 18.105 40.381 18.105 42.88 Z"
android:strokeWidth="1"/>
</group>
</group>
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:duration="1000"
android:valueFrom="#00ffffff"
android:valueTo="#161c2d"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="path_1">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:duration="1000"
android:valueFrom="#00ffffff"
android:valueTo="#f9f9fb"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="path_2">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:duration="1000"
android:valueFrom="#00ffffff"
android:valueTo="#f9f9fb"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
</animated-vector>

71
samples/ControlCatalog.Android/Resources/drawable-v31/avalonia_anim.xml

@ -0,0 +1,71 @@
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<group
android:name="wrapper"
android:translateX="21"
android:translateY="21">
<group android:name="group">
<path
android:name="path"
android:pathData="M 74.853 85.823 L 75.368 85.823 C 80.735 85.823 85.144 81.803 85.761 76.602 L 85.836 41.76 C 85.225 18.593 66.254 0 42.939 0 C 19.24 0 0.028 19.212 0.028 42.912 C 0.028 66.357 18.831 85.418 42.18 85.823 L 74.853 85.823 Z"
android:fillColor="#00ffffff"
android:strokeWidth="1"/>
<path
android:name="path_1"
android:pathData="M 43.059 14.614 C 29.551 14.614 18.256 24.082 15.445 36.743 C 18.136 37.498 20.109 39.968 20.109 42.899 C 20.109 45.831 18.136 48.301 15.445 49.055 C 18.256 61.716 29.551 71.184 43.059 71.184 C 47.975 71.184 52.599 69.93 56.628 67.723 L 56.628 70.993 L 71.344 70.993 L 71.344 44.072 C 71.357 43.714 71.344 43.26 71.344 42.899 C 71.344 27.278 58.68 14.614 43.059 14.614 Z M 29.51 42.899 C 29.51 35.416 35.576 29.35 43.059 29.35 C 50.541 29.35 56.607 35.416 56.607 42.899 C 56.607 50.382 50.541 56.448 43.059 56.448 C 35.576 56.448 29.51 50.382 29.51 42.899 Z"
android:fillColor="#00ffffff"
android:strokeWidth="1"
android:fillType="evenOdd"/>
<path
android:name="path_2"
android:pathData="M 18.105 42.88 C 18.105 45.38 16.078 47.407 13.579 47.407 C 11.079 47.407 9.052 45.38 9.052 42.88 C 9.052 40.381 11.079 38.354 13.579 38.354 C 16.078 38.354 18.105 40.381 18.105 42.88 Z"
android:fillColor="#00ffffff"
android:strokeWidth="1"/>
</group>
</group>
</vector>
</aapt:attr>
<target android:name="path_2">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:startOffset="100"
android:duration="900"
android:valueFrom="#00ffffff"
android:valueTo="#161c2d"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:duration="500"
android:valueFrom="#00ffffff"
android:valueTo="#f9f9fb"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="path_1">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:startOffset="100"
android:duration="900"
android:valueFrom="#00ffffff"
android:valueTo="#161c2d"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
</animated-vector>

21
samples/ControlCatalog.Android/Resources/values-v31/styles.xml

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<style name="MyTheme">
</style>
<style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.NoActionBar">
<item name="android:windowActionBar">false</item>
<item name="android:windowBackground">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowSplashScreenBackground">@color/splash_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/avalonia_anim</item>
<item name="android:windowSplashScreenAnimationDuration">1000</item>
<item name="postSplashScreenTheme">@style/MyTheme.Main</item>
</style>
<style name="MyTheme.Main"
parent ="MyTheme.NoActionBar">
<item name="android:windowIsTranslucent">true</item>
</style>
</resources>

11
samples/ControlCatalog.Android/Resources/values/styles.xml

@ -6,16 +6,7 @@
<style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.DayNight.NoActionBar"> <style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.DayNight.NoActionBar">
<item name="android:windowActionBar">false</item> <item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
</style>
<style name="MyTheme.Splash" parent ="MyTheme.NoActionBar">
<item name="android:windowBackground">@drawable/splash_screen</item> <item name="android:windowBackground">@drawable/splash_screen</item>
<item name="android:windowContentOverlay">@null</item> <item name="android:windowNoTitle">true</item>
</style>
<style name="MyTheme.Main" parent ="MyTheme.NoActionBar">
<item name="android:windowIsTranslucent">true</item>
</style> </style>
</resources> </resources>

35
samples/ControlCatalog.Android/SplashActivity.cs

@ -1,35 +0,0 @@
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using Avalonia.Android;
namespace ControlCatalog.Android
{
[Activity(Theme = "@style/MyTheme.Splash", MainLauncher = true, NoHistory = true)]
public class SplashActivity : AvaloniaSplashActivity<App>
{
protected override Avalonia.AppBuilder CustomizeAppBuilder(Avalonia.AppBuilder builder)
{
return base.CustomizeAppBuilder(builder)
.AfterSetup(_ =>
{
Pages.EmbedSample.Implementation = new EmbedSampleAndroid();
});
}
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
}
protected override void OnResume()
{
base.OnResume();
StartActivity(new Intent(Application.Context, typeof(MainActivity)));
Finish();
}
}
}

10
samples/ControlCatalog/Pages/CompositionPage.axaml.cs

@ -144,8 +144,8 @@ public partial class CompositionPage : UserControl
{ {
if(_solidVisual == null) if(_solidVisual == null)
return; return;
_solidVisual.Size = new Vector2((float)v.Bounds.Width / 3, (float)v.Bounds.Height / 3); _solidVisual.Size = new (v.Bounds.Width / 3, v.Bounds.Height / 3);
_solidVisual.Offset = new Vector3((float)v.Bounds.Width / 3, (float)v.Bounds.Height / 3, 0); _solidVisual.Offset = new (v.Bounds.Width / 3, v.Bounds.Height / 3, 0);
} }
v.AttachedToVisualTree += delegate v.AttachedToVisualTree += delegate
{ {
@ -164,7 +164,7 @@ public partial class CompositionPage : UserControl
animation.Direction = PlaybackDirection.Alternate; animation.Direction = PlaybackDirection.Alternate;
_solidVisual.StartAnimation("Color", animation); _solidVisual.StartAnimation("Color", animation);
_solidVisual.AnchorPoint = new Vector2(0, 0); _solidVisual.AnchorPoint = new (0, 0);
var scale = _solidVisual.Compositor.CreateVector3KeyFrameAnimation(); var scale = _solidVisual.Compositor.CreateVector3KeyFrameAnimation();
scale.Duration = TimeSpan.FromSeconds(5); scale.Duration = TimeSpan.FromSeconds(5);
@ -195,8 +195,8 @@ public partial class CompositionPage : UserControl
if (_customVisual == null) if (_customVisual == null)
return; return;
var h = (float)Math.Min(v.Bounds.Height, v.Bounds.Width / 3); var h = (float)Math.Min(v.Bounds.Height, v.Bounds.Width / 3);
_customVisual.Size = new Vector2((float)v.Bounds.Width, h); _customVisual.Size = new (v.Bounds.Width, h);
_customVisual.Offset = new Vector3(0, (float)(v.Bounds.Height - h) / 2, 0); _customVisual.Offset = new (0, (v.Bounds.Height - h) / 2, 0);
} }
v.AttachedToVisualTree += delegate v.AttachedToVisualTree += delegate
{ {

16
samples/ControlCatalog/Pages/GesturePage.cs

@ -13,7 +13,7 @@ namespace ControlCatalog.Pages
public class GesturePage : UserControl public class GesturePage : UserControl
{ {
private bool _isInit; private bool _isInit;
private float _currentScale; private double _currentScale;
public GesturePage() public GesturePage()
{ {
@ -53,7 +53,7 @@ namespace ControlCatalog.Pages
if(compositionVisual!= null) if(compositionVisual!= null)
{ {
_currentScale = 1; _currentScale = 1;
compositionVisual.Scale = new Vector3(1,1,1); compositionVisual.Scale = new (1,1,1);
compositionVisual.Offset = default; compositionVisual.Offset = default;
image.InvalidateMeasure(); image.InvalidateMeasure();
} }
@ -69,7 +69,7 @@ namespace ControlCatalog.Pages
} }
_currentScale = 1; _currentScale = 1;
Vector3 currentOffset = default; Vector3D currentOffset = default;
CompositionVisual? compositionVisual = null; CompositionVisual? compositionVisual = null;
@ -133,11 +133,11 @@ namespace ControlCatalog.Pages
if (compositionVisual != null && _currentScale != 1) if (compositionVisual != null && _currentScale != 1)
{ {
currentOffset += new Vector3((float)e.Delta.X, (float)e.Delta.Y, 0); currentOffset += new Vector3D(e.Delta.X, e.Delta.Y, 0);
var currentSize = control.Bounds.Size * _currentScale; var currentSize = control.Bounds.Size * _currentScale;
currentOffset = new Vector3((float)MathUtilities.Clamp(currentOffset.X, 0, currentSize.Width - control.Bounds.Width), currentOffset = new Vector3D(MathUtilities.Clamp(currentOffset.X, 0, currentSize.Width - control.Bounds.Width),
(float)MathUtilities.Clamp(currentOffset.Y, 0, currentSize.Height - control.Bounds.Height), (float)MathUtilities.Clamp(currentOffset.Y, 0, currentSize.Height - control.Bounds.Height),
0); 0);
@ -157,7 +157,7 @@ namespace ControlCatalog.Pages
var ball = control.FindLogicalDescendantOfType<Border>(); var ball = control.FindLogicalDescendantOfType<Border>();
Vector3 defaultOffset = default; Vector3D defaultOffset = default;
CompositionVisual? ballCompositionVisual = null; CompositionVisual? ballCompositionVisual = null;
@ -181,11 +181,11 @@ namespace ControlCatalog.Pages
control.AddHandler(Gestures.PullGestureEvent, (s, e) => control.AddHandler(Gestures.PullGestureEvent, (s, e) =>
{ {
Vector3 center = new((float)control.Bounds.Center.X, (float)control.Bounds.Center.Y, 0); Vector3D center = new((float)control.Bounds.Center.X, (float)control.Bounds.Center.Y, 0);
InitComposition(ball!); InitComposition(ball!);
if (ballCompositionVisual != null) if (ballCompositionVisual != null)
{ {
ballCompositionVisual.Offset = defaultOffset + new System.Numerics.Vector3((float)e.Delta.X * 0.4f, (float)e.Delta.Y * 0.4f, 0) * (inverse ? -1 : 1); ballCompositionVisual.Offset = defaultOffset + new Vector3D(e.Delta.X * 0.4f, e.Delta.Y * 0.4f, 0) * (inverse ? -1 : 1);
e.Handled = true; e.Handled = true;
} }

4
samples/GpuInterop/DrawingSurfaceDemoBase.cs

@ -48,7 +48,7 @@ public abstract class DrawingSurfaceDemoBase : Control, IGpuDemo
Surface = _compositor.CreateDrawingSurface(); Surface = _compositor.CreateDrawingSurface();
_visual = _compositor.CreateSurfaceVisual(); _visual = _compositor.CreateSurfaceVisual();
_visual.Size = new Vector2((float)Bounds.Width, (float)Bounds.Height); _visual.Size = new (Bounds.Width, Bounds.Height);
_visual.Surface = Surface; _visual.Surface = Surface;
ElementComposition.SetElementChildVisual(this, _visual); ElementComposition.SetElementChildVisual(this, _visual);
var (res, info) = await DoInitialize(_compositor, Surface); var (res, info) = await DoInitialize(_compositor, Surface);
@ -72,7 +72,7 @@ public abstract class DrawingSurfaceDemoBase : Control, IGpuDemo
if (root == null) if (root == null)
return; return;
_visual!.Size = new Vector2((float)Bounds.Width, (float)Bounds.Height); _visual!.Size = new (Bounds.Width, Bounds.Height);
var size = PixelSize.FromSize(Bounds.Size, root.RenderScaling); var size = PixelSize.FromSize(Bounds.Size, root.RenderScaling);
RenderFrame(size); RenderFrame(size);
if (SupportsDisco && Disco > 0) if (SupportsDisco && Disco > 0)

5
samples/MobileSandbox.Android/MainActivity.cs

@ -1,12 +1,11 @@
using Android.App; using Android.App;
using Android.Content.PM; using Android.Content.PM;
using Avalonia;
using Avalonia.Android; using Avalonia.Android;
namespace MobileSandbox.Android namespace MobileSandbox.Android
{ {
[Activity(Label = "MobileSandbox.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] [Activity(Label = "MobileSandbox.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)]
public class MainActivity : AvaloniaMainActivity public class MainActivity : AvaloniaMainActivity<App>
{ {
} }
} }

10
samples/MobileSandbox.Android/MobileSandbox.Android.csproj

@ -10,6 +10,12 @@
<AndroidPackageFormat>apk</AndroidPackageFormat> <AndroidPackageFormat>apk</AndroidPackageFormat>
<MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver> <MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<None Remove="Resources\drawable-night-v31\avalonia_anim.xml" />
<None Remove="Resources\drawable-v31\avalonia_anim.xml" />
<None Remove="Resources\values-v31\styles.xml" />
<None Remove="Resources\values-night\colors.xml" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<AndroidResource Include="..\..\build\Assets\Icon.png"> <AndroidResource Include="..\..\build\Assets\Icon.png">
<Link>Resources\drawable\Icon.png</Link> <Link>Resources\drawable\Icon.png</Link>
@ -37,6 +43,10 @@
<AndroidEnvironment Condition="'$(IsEmulator)'!='True'" Include="environment.device.txt" /> <AndroidEnvironment Condition="'$(IsEmulator)'!='True'" Include="environment.device.txt" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.0.1" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Android\Avalonia.Android\Avalonia.Android.csproj" /> <ProjectReference Include="..\..\src\Android\Avalonia.Android\Avalonia.Android.csproj" />
<ProjectReference Include="..\MobileSandbox\MobileSandbox.csproj" /> <ProjectReference Include="..\MobileSandbox\MobileSandbox.csproj" />

66
samples/MobileSandbox.Android/Resources/drawable-night-v31/avalonia_anim.xml

@ -0,0 +1,66 @@
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<group
android:name="wrapper"
android:translateX="21"
android:translateY="21">
<group android:name="group">
<path
android:name="path"
android:pathData="M 74.853 85.823 L 75.368 85.823 C 80.735 85.823 85.144 81.803 85.761 76.602 L 85.836 41.76 C 85.225 18.593 66.254 0 42.939 0 C 19.24 0 0.028 19.212 0.028 42.912 C 0.028 66.357 18.831 85.418 42.18 85.823 L 74.853 85.823 Z"
android:strokeWidth="1"/>
<path
android:name="path_1"
android:pathData="M 43.059 14.614 C 29.551 14.614 18.256 24.082 15.445 36.743 C 18.136 37.498 20.109 39.968 20.109 42.899 C 20.109 45.831 18.136 48.301 15.445 49.055 C 18.256 61.716 29.551 71.184 43.059 71.184 C 47.975 71.184 52.599 69.93 56.628 67.723 L 56.628 70.993 L 71.344 70.993 L 71.344 44.072 C 71.357 43.714 71.344 43.26 71.344 42.899 C 71.344 27.278 58.68 14.614 43.059 14.614 Z M 29.51 42.899 C 29.51 35.416 35.576 29.35 43.059 29.35 C 50.541 29.35 56.607 35.416 56.607 42.899 C 56.607 50.382 50.541 56.448 43.059 56.448 C 35.576 56.448 29.51 50.382 29.51 42.899 Z"
android:strokeWidth="1"
android:fillType="evenOdd"/>
<path
android:name="path_2"
android:pathData="M 18.105 42.88 C 18.105 45.38 16.078 47.407 13.579 47.407 C 11.079 47.407 9.052 45.38 9.052 42.88 C 9.052 40.381 11.079 38.354 13.579 38.354 C 16.078 38.354 18.105 40.381 18.105 42.88 Z"
android:strokeWidth="1"/>
</group>
</group>
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:duration="1000"
android:valueFrom="#00ffffff"
android:valueTo="#161c2d"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="path_1">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:duration="1000"
android:valueFrom="#00ffffff"
android:valueTo="#f9f9fb"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="path_2">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:duration="1000"
android:valueFrom="#00ffffff"
android:valueTo="#f9f9fb"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
</animated-vector>

71
samples/MobileSandbox.Android/Resources/drawable-v31/avalonia_anim.xml

@ -0,0 +1,71 @@
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<group
android:name="wrapper"
android:translateX="21"
android:translateY="21">
<group android:name="group">
<path
android:name="path"
android:pathData="M 74.853 85.823 L 75.368 85.823 C 80.735 85.823 85.144 81.803 85.761 76.602 L 85.836 41.76 C 85.225 18.593 66.254 0 42.939 0 C 19.24 0 0.028 19.212 0.028 42.912 C 0.028 66.357 18.831 85.418 42.18 85.823 L 74.853 85.823 Z"
android:fillColor="#00ffffff"
android:strokeWidth="1"/>
<path
android:name="path_1"
android:pathData="M 43.059 14.614 C 29.551 14.614 18.256 24.082 15.445 36.743 C 18.136 37.498 20.109 39.968 20.109 42.899 C 20.109 45.831 18.136 48.301 15.445 49.055 C 18.256 61.716 29.551 71.184 43.059 71.184 C 47.975 71.184 52.599 69.93 56.628 67.723 L 56.628 70.993 L 71.344 70.993 L 71.344 44.072 C 71.357 43.714 71.344 43.26 71.344 42.899 C 71.344 27.278 58.68 14.614 43.059 14.614 Z M 29.51 42.899 C 29.51 35.416 35.576 29.35 43.059 29.35 C 50.541 29.35 56.607 35.416 56.607 42.899 C 56.607 50.382 50.541 56.448 43.059 56.448 C 35.576 56.448 29.51 50.382 29.51 42.899 Z"
android:fillColor="#00ffffff"
android:strokeWidth="1"
android:fillType="evenOdd"/>
<path
android:name="path_2"
android:pathData="M 18.105 42.88 C 18.105 45.38 16.078 47.407 13.579 47.407 C 11.079 47.407 9.052 45.38 9.052 42.88 C 9.052 40.381 11.079 38.354 13.579 38.354 C 16.078 38.354 18.105 40.381 18.105 42.88 Z"
android:fillColor="#00ffffff"
android:strokeWidth="1"/>
</group>
</group>
</vector>
</aapt:attr>
<target android:name="path_2">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:startOffset="100"
android:duration="900"
android:valueFrom="#00ffffff"
android:valueTo="#161c2d"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:duration="500"
android:valueFrom="#00ffffff"
android:valueTo="#f9f9fb"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="path_1">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:startOffset="100"
android:duration="900"
android:valueFrom="#00ffffff"
android:valueTo="#161c2d"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
</animated-vector>

4
samples/MobileSandbox.Android/Resources/values-night/colors.xml

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="splash_background">#212121</color>
</resources>

16
samples/MobileSandbox.Android/Resources/values-v31/styles.xml

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<style name="MyTheme">
</style>
<style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.NoActionBar">
<item name="android:windowActionBar">false</item>
<item name="android:windowBackground">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowSplashScreenBackground">@color/splash_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/avalonia_anim</item>
<item name="android:windowSplashScreenAnimationDuration">1000</item>
</style>
</resources>

7
samples/MobileSandbox.Android/Resources/values/styles.xml

@ -6,12 +6,7 @@
<style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.NoActionBar"> <style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.NoActionBar">
<item name="android:windowActionBar">false</item> <item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
</style>
<style name="MyTheme.Splash" parent ="MyTheme.NoActionBar">
<item name="android:windowBackground">@drawable/splash_screen</item> <item name="android:windowBackground">@drawable/splash_screen</item>
<item name="android:windowContentOverlay">@null</item> <item name="android:windowNoTitle">true</item>
</style> </style>
</resources> </resources>

17
samples/MobileSandbox.Android/SplashActivity.cs

@ -1,17 +0,0 @@
using Android.App;
using Android.Content;
using Avalonia.Android;
namespace MobileSandbox.Android
{
[Activity(Theme = "@style/MyTheme.Splash", MainLauncher = true, NoHistory = true)]
public class SplashActivity : AvaloniaSplashActivity<App>
{
protected override void OnResume()
{
base.OnResume();
StartActivity(new Intent(Application.Context, typeof(MainActivity)));
}
}
}

4
samples/SafeAreaDemo.Android/MainActivity.cs

@ -4,8 +4,8 @@ using Avalonia.Android;
namespace SafeAreaDemo.Android namespace SafeAreaDemo.Android
{ {
[Activity(Label = "SafeAreaDemo.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] [Activity(Label = "SafeAreaDemo.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
public class MainActivity : AvaloniaMainActivity public class MainActivity : AvaloniaMainActivity<App>
{ {
} }
} }

66
samples/SafeAreaDemo.Android/Resources/drawable-night-v31/avalonia_anim.xml

@ -0,0 +1,66 @@
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<group
android:name="wrapper"
android:translateX="21"
android:translateY="21">
<group android:name="group">
<path
android:name="path"
android:pathData="M 74.853 85.823 L 75.368 85.823 C 80.735 85.823 85.144 81.803 85.761 76.602 L 85.836 41.76 C 85.225 18.593 66.254 0 42.939 0 C 19.24 0 0.028 19.212 0.028 42.912 C 0.028 66.357 18.831 85.418 42.18 85.823 L 74.853 85.823 Z"
android:strokeWidth="1"/>
<path
android:name="path_1"
android:pathData="M 43.059 14.614 C 29.551 14.614 18.256 24.082 15.445 36.743 C 18.136 37.498 20.109 39.968 20.109 42.899 C 20.109 45.831 18.136 48.301 15.445 49.055 C 18.256 61.716 29.551 71.184 43.059 71.184 C 47.975 71.184 52.599 69.93 56.628 67.723 L 56.628 70.993 L 71.344 70.993 L 71.344 44.072 C 71.357 43.714 71.344 43.26 71.344 42.899 C 71.344 27.278 58.68 14.614 43.059 14.614 Z M 29.51 42.899 C 29.51 35.416 35.576 29.35 43.059 29.35 C 50.541 29.35 56.607 35.416 56.607 42.899 C 56.607 50.382 50.541 56.448 43.059 56.448 C 35.576 56.448 29.51 50.382 29.51 42.899 Z"
android:strokeWidth="1"
android:fillType="evenOdd"/>
<path
android:name="path_2"
android:pathData="M 18.105 42.88 C 18.105 45.38 16.078 47.407 13.579 47.407 C 11.079 47.407 9.052 45.38 9.052 42.88 C 9.052 40.381 11.079 38.354 13.579 38.354 C 16.078 38.354 18.105 40.381 18.105 42.88 Z"
android:strokeWidth="1"/>
</group>
</group>
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:duration="1000"
android:valueFrom="#00ffffff"
android:valueTo="#161c2d"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="path_1">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:duration="1000"
android:valueFrom="#00ffffff"
android:valueTo="#f9f9fb"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="path_2">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:duration="1000"
android:valueFrom="#00ffffff"
android:valueTo="#f9f9fb"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
</animated-vector>

71
samples/SafeAreaDemo.Android/Resources/drawable-v31/avalonia_anim.xml

@ -0,0 +1,71 @@
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<group
android:name="wrapper"
android:translateX="21"
android:translateY="21">
<group android:name="group">
<path
android:name="path"
android:pathData="M 74.853 85.823 L 75.368 85.823 C 80.735 85.823 85.144 81.803 85.761 76.602 L 85.836 41.76 C 85.225 18.593 66.254 0 42.939 0 C 19.24 0 0.028 19.212 0.028 42.912 C 0.028 66.357 18.831 85.418 42.18 85.823 L 74.853 85.823 Z"
android:fillColor="#00ffffff"
android:strokeWidth="1"/>
<path
android:name="path_1"
android:pathData="M 43.059 14.614 C 29.551 14.614 18.256 24.082 15.445 36.743 C 18.136 37.498 20.109 39.968 20.109 42.899 C 20.109 45.831 18.136 48.301 15.445 49.055 C 18.256 61.716 29.551 71.184 43.059 71.184 C 47.975 71.184 52.599 69.93 56.628 67.723 L 56.628 70.993 L 71.344 70.993 L 71.344 44.072 C 71.357 43.714 71.344 43.26 71.344 42.899 C 71.344 27.278 58.68 14.614 43.059 14.614 Z M 29.51 42.899 C 29.51 35.416 35.576 29.35 43.059 29.35 C 50.541 29.35 56.607 35.416 56.607 42.899 C 56.607 50.382 50.541 56.448 43.059 56.448 C 35.576 56.448 29.51 50.382 29.51 42.899 Z"
android:fillColor="#00ffffff"
android:strokeWidth="1"
android:fillType="evenOdd"/>
<path
android:name="path_2"
android:pathData="M 18.105 42.88 C 18.105 45.38 16.078 47.407 13.579 47.407 C 11.079 47.407 9.052 45.38 9.052 42.88 C 9.052 40.381 11.079 38.354 13.579 38.354 C 16.078 38.354 18.105 40.381 18.105 42.88 Z"
android:fillColor="#00ffffff"
android:strokeWidth="1"/>
</group>
</group>
</vector>
</aapt:attr>
<target android:name="path_2">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:startOffset="100"
android:duration="900"
android:valueFrom="#00ffffff"
android:valueTo="#161c2d"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:duration="500"
android:valueFrom="#00ffffff"
android:valueTo="#f9f9fb"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="path_1">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:startOffset="100"
android:duration="900"
android:valueFrom="#00ffffff"
android:valueTo="#161c2d"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
</animated-vector>

4
samples/SafeAreaDemo.Android/Resources/values-night/colors.xml

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="splash_background">#212121</color>
</resources>

16
samples/SafeAreaDemo.Android/Resources/values-v31/styles.xml

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<style name="MyTheme">
</style>
<style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.NoActionBar">
<item name="android:windowActionBar">false</item>
<item name="android:windowBackground">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowSplashScreenBackground">@color/splash_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/avalonia_anim</item>
<item name="android:windowSplashScreenAnimationDuration">1000</item>
</style>
</resources>

7
samples/SafeAreaDemo.Android/Resources/values/styles.xml

@ -6,12 +6,7 @@
<style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.NoActionBar"> <style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.NoActionBar">
<item name="android:windowActionBar">false</item> <item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
</style>
<style name="MyTheme.Splash" parent ="MyTheme.NoActionBar">
<item name="android:windowBackground">@drawable/splash_screen</item> <item name="android:windowBackground">@drawable/splash_screen</item>
<item name="android:windowContentOverlay">@null</item> <item name="android:windowNoTitle">true</item>
</style> </style>
</resources> </resources>

13
samples/SafeAreaDemo.Android/SafeAreaDemo.Android.csproj

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net7.0-android</TargetFramework> <TargetFramework>net7.0-android</TargetFramework>
@ -12,13 +12,14 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<AndroidResource Include="Icon.png"> <ProjectReference Include="..\SafeAreaDemo\SafeAreaDemo.csproj" />
<Link>Resources\drawable\Icon.png</Link> <ProjectReference Include="..\..\src\Android\Avalonia.Android\Avalonia.Android.csproj" />
</AndroidResource>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\SafeAreaDemo\SafeAreaDemo.csproj" /> <AndroidResource Include="..\..\build\Assets\Icon.png">
<ProjectReference Include="..\..\src\Android\Avalonia.Android\Avalonia.Android.csproj" /> <Link>Resources\drawable\Icon.png</Link>
</AndroidResource>
</ItemGroup> </ItemGroup>
</Project> </Project>

30
samples/SafeAreaDemo.Android/SplashActivity.cs

@ -1,30 +0,0 @@
using Android.App;
using Android.Content;
using Android.OS;
using Avalonia;
using Avalonia.Android;
using Application = Android.App.Application;
namespace SafeAreaDemo.Android
{
[Activity(Theme = "@style/MyTheme.Splash", MainLauncher = true, NoHistory = true)]
public class SplashActivity : AvaloniaSplashActivity<App>
{
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
{
return base.CustomizeAppBuilder(builder);
}
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
}
protected override void OnResume()
{
base.OnResume();
StartActivity(new Intent(Application.Context, typeof(MainActivity)));
}
}
}

6
samples/SafeAreaDemo/Views/MainView.xaml.cs

@ -1,4 +1,5 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using SafeAreaDemo.ViewModels; using SafeAreaDemo.ViewModels;
@ -11,9 +12,10 @@ namespace SafeAreaDemo.Views
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
protected override void OnLoaded() /// <inheritdoc/>
protected override void OnLoaded(RoutedEventArgs e)
{ {
base.OnLoaded(); base.OnLoaded(e);
var insetsManager = TopLevel.GetTopLevel(this)?.InsetsManager; var insetsManager = TopLevel.GetTopLevel(this)?.InsetsManager;
if (insetsManager != null && DataContext is MainViewModel viewModel) if (insetsManager != null && DataContext is MainViewModel viewModel)

7
samples/Sandbox/MainWindow.axaml

@ -1,4 +1,11 @@
<Window xmlns="https://github.com/avaloniaui" <Window xmlns="https://github.com/avaloniaui"
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
x:Class="Sandbox.MainWindow"> x:Class="Sandbox.MainWindow">
<ScrollViewer>
<StackPanel>
<Button Margin="0 100000000000000000 0 0">0</Button>
<Button>1</Button>
</StackPanel>
</ScrollViewer>
</Window> </Window>

60
src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Avalonia.Android
{
partial class AvaloniaMainActivity<TApp> where TApp : Application, new()
{
protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder.UseAndroid();
private static AppBuilder? s_appBuilder;
internal static object ViewContent;
public object Content
{
get
{
return ViewContent;
}
set
{
ViewContent = value;
if (View != null)
View.Content = value;
}
}
protected AppBuilder CreateAppBuilder()
{
var builder = AppBuilder.Configure<TApp>();
return CustomizeAppBuilder(builder);
}
private void InitializeApp()
{
if (s_appBuilder == null)
{
var builder = CreateAppBuilder();
builder.SetupWithLifetime(new SingleViewLifetime());
s_appBuilder = builder;
}
View = new AvaloniaView(this);
if (ViewContent != null)
{
View.Content = ViewContent;
}
if (Avalonia.Application.Current.ApplicationLifetime is SingleViewLifetime lifetime)
{
lifetime.View = View;
}
}
}
}

88
src/Android/Avalonia.Android/AvaloniaMainActivity.cs

@ -11,27 +11,49 @@ using AndroidX.AppCompat.App;
namespace Avalonia.Android namespace Avalonia.Android
{ {
public abstract class AvaloniaMainActivity : AppCompatActivity, IActivityResultHandler, IActivityNavigationService public class AvaloniaMainActivity : AppCompatActivity, IActivityResultHandler, IActivityNavigationService
{ {
internal static object ViewContent;
public Action<int, Result, Intent> ActivityResult { get; set; } public Action<int, Result, Intent> ActivityResult { get; set; }
public Action<int, string[], Permission[]> RequestPermissionsResult { get; set; } public Action<int, string[], Permission[]> RequestPermissionsResult { get; set; }
public event EventHandler<AndroidBackRequestedEventArgs> BackRequested;
public override void OnBackPressed()
{
var eventArgs = new AndroidBackRequestedEventArgs();
BackRequested?.Invoke(this, eventArgs);
if (!eventArgs.Handled)
{
base.OnBackPressed();
}
}
protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
ActivityResult?.Invoke(requestCode, resultCode, data);
}
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
{
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
RequestPermissionsResult?.Invoke(requestCode, permissions, grantResults);
}
}
public abstract partial class AvaloniaMainActivity<TApp> : AvaloniaMainActivity where TApp : Application, new()
{
internal AvaloniaView View; internal AvaloniaView View;
private GlobalLayoutListener _listener; private GlobalLayoutListener _listener;
protected override void OnCreate(Bundle savedInstanceState) protected override void OnCreate(Bundle savedInstanceState)
{ {
View = new AvaloniaView(this); InitializeApp();
if (ViewContent != null)
{
View.Content = ViewContent;
}
if (Avalonia.Application.Current.ApplicationLifetime is SingleViewLifetime lifetime)
{
lifetime.View = View;
}
base.OnCreate(savedInstanceState); base.OnCreate(savedInstanceState);
SetContentView(View); SetContentView(View);
@ -41,20 +63,6 @@ namespace Avalonia.Android
View.ViewTreeObserver?.AddOnGlobalLayoutListener(_listener); View.ViewTreeObserver?.AddOnGlobalLayoutListener(_listener);
} }
public object Content
{
get
{
return ViewContent;
}
set
{
ViewContent = value;
if (View != null)
View.Content = value;
}
}
protected override void OnResume() protected override void OnResume()
{ {
base.OnResume(); base.OnResume();
@ -66,20 +74,6 @@ namespace Avalonia.Android
} }
} }
public event EventHandler<AndroidBackRequestedEventArgs> BackRequested;
public override void OnBackPressed()
{
var eventArgs = new AndroidBackRequestedEventArgs();
BackRequested?.Invoke(this, eventArgs);
if (!eventArgs.Handled)
{
base.OnBackPressed();
}
}
protected override void OnDestroy() protected override void OnDestroy()
{ {
View.Content = null; View.Content = null;
@ -89,20 +83,6 @@ namespace Avalonia.Android
base.OnDestroy(); base.OnDestroy();
} }
protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
ActivityResult?.Invoke(requestCode, resultCode, data);
}
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
{
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
RequestPermissionsResult?.Invoke(requestCode, permissions, grantResults);
}
class GlobalLayoutListener : Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener class GlobalLayoutListener : Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener
{ {
private AvaloniaView _view; private AvaloniaView _view;

40
src/Android/Avalonia.Android/AvaloniaSplashActivity.cs

@ -1,40 +0,0 @@
using Android.OS;
using AndroidX.AppCompat.App;
namespace Avalonia.Android
{
public abstract class AvaloniaSplashActivity : AppCompatActivity
{
protected abstract AppBuilder CreateAppBuilder();
private static AppBuilder s_appBuilder;
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
if (s_appBuilder == null)
{
var builder = CreateAppBuilder();
var lifetime = new SingleViewLifetime();
builder.SetupWithLifetime(lifetime);
s_appBuilder = builder;
}
}
}
public abstract class AvaloniaSplashActivity<TApp> : AvaloniaSplashActivity where TApp : Application, new()
{
protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder.UseAndroid();
protected override AppBuilder CreateAppBuilder()
{
var builder = AppBuilder.Configure<TApp>();
return CustomizeAppBuilder(builder);
}
}
}

119
src/Avalonia.Base/Animation/AnimationInstance`1.cs

@ -58,6 +58,9 @@ namespace Avalonia.Animation
if (_animation.Duration < TimeSpan.Zero) if (_animation.Duration < TimeSpan.Zero)
throw new InvalidOperationException("Duration value cannot be negative."); throw new InvalidOperationException("Duration value cannot be negative.");
if (_animation.Delay < TimeSpan.Zero)
throw new InvalidOperationException("Delay value cannot be negative.");
_easeFunc = _animation.Easing; _easeFunc = _animation.Easing;
_speedRatioConv = 1d / _animation.SpeedRatio; _speedRatioConv = 1d / _animation.SpeedRatio;
@ -151,72 +154,74 @@ namespace Avalonia.Animation
var iterDelay = _iterationDelay.Ticks * _speedRatioConv; var iterDelay = _iterationDelay.Ticks * _speedRatioConv;
var initDelay = _initialDelay.Ticks * _speedRatioConv; var initDelay = _initialDelay.Ticks * _speedRatioConv;
if (indexTime > 0 & indexTime <= initDelay) // This conditional checks if the time given is the very start/zero
// and when we have an active delay time.
if (initDelay > 0 && indexTime <= initDelay)
{ {
DoDelay(); DoDelay();
return;
} }
else
// Calculate timebases.
var iterationTime = iterDuration + iterDelay;
var opsTime = indexTime - initDelay;
var playbackTime = opsTime % iterationTime;
_currentIteration = (ulong)(opsTime / iterationTime);
// Stop animation when the current iteration is beyond the iteration count or
// when the duration is set to zero while animating and snap to the last iterated value.
if (_currentIteration + 1 > _iterationCount || _duration == TimeSpan.Zero)
{ {
// Calculate timebases. var easedTime = _easeFunc!.Ease(_playbackReversed ? 0.0 : 1.0);
var iterationTime = iterDuration + iterDelay; _lastInterpValue = _interpolator(easedTime, _neutralValue);
var opsTime = indexTime - initDelay; DoComplete();
var playbackTime = opsTime % iterationTime; }
_currentIteration = (ulong)(opsTime / iterationTime); if (playbackTime <= iterDuration)
{
// Normalize time for interpolation.
var normalizedTime = playbackTime / iterDuration;
// Stop animation when the current iteration is beyond the iteration count or // Check if normalized time needs to be reversed according to PlaybackDirection
// when the duration is set to zero while animating and snap to the last iterated value.
if (_currentIteration + 1 > _iterationCount || _duration == TimeSpan.Zero)
{
var easedTime = _easeFunc!.Ease(_playbackReversed ? 0.0 : 1.0);
_lastInterpValue = _interpolator(easedTime, _neutralValue);
DoComplete();
}
if (playbackTime <= iterDuration) switch (_playbackDirection)
{
// Normalize time for interpolation.
var normalizedTime = playbackTime / iterDuration;
// Check if normalized time needs to be reversed according to PlaybackDirection
switch (_playbackDirection)
{
case PlaybackDirection.Normal:
_playbackReversed = false;
break;
case PlaybackDirection.Reverse:
_playbackReversed = true;
break;
case PlaybackDirection.Alternate:
_playbackReversed = _currentIteration % 2 != 0;
break;
case PlaybackDirection.AlternateReverse:
_playbackReversed = _currentIteration % 2 == 0;
break;
default:
throw new InvalidOperationException($"Animation direction value is unknown: {_playbackDirection}");
}
if (_playbackReversed)
normalizedTime = 1 - normalizedTime;
// Ease and interpolate
var easedTime = _easeFunc!.Ease(normalizedTime);
_lastInterpValue = _interpolator(easedTime, _neutralValue);
PublishNext(_lastInterpValue);
}
else if (playbackTime > iterDuration &
playbackTime <= iterationTime &
iterDelay > 0)
{ {
// The last iteration's trailing delay should be skipped. case PlaybackDirection.Normal:
if (_currentIteration + 1 < _iterationCount) _playbackReversed = false;
DoDelay(); break;
else case PlaybackDirection.Reverse:
DoComplete(); _playbackReversed = true;
break;
case PlaybackDirection.Alternate:
_playbackReversed = _currentIteration % 2 != 0;
break;
case PlaybackDirection.AlternateReverse:
_playbackReversed = _currentIteration % 2 == 0;
break;
default:
throw new InvalidOperationException(
$"Animation direction value is unknown: {_playbackDirection}");
} }
if (_playbackReversed)
normalizedTime = 1 - normalizedTime;
// Ease and interpolate
var easedTime = _easeFunc!.Ease(normalizedTime);
_lastInterpValue = _interpolator(easedTime, _neutralValue);
PublishNext(_lastInterpValue);
}
else if (playbackTime > iterDuration &&
playbackTime <= iterationTime &&
iterDelay > 0)
{
// The last iteration's trailing delay should be skipped.
if (_currentIteration + 1 < _iterationCount)
DoDelay();
else
DoComplete();
} }
} }

1
src/Avalonia.Base/Input/Pointer.cs

@ -88,7 +88,6 @@ namespace Avalonia.Input
/// Captures pointer input to the specified gesture recognizer. /// Captures pointer input to the specified gesture recognizer.
/// </summary> /// </summary>
/// <param name="gestureRecognizer">The gesture recognizer.</param> /// <param name="gestureRecognizer">The gesture recognizer.</param>
/// </remarks>
internal void CaptureGestureRecognizer(GestureRecognizer? gestureRecognizer) internal void CaptureGestureRecognizer(GestureRecognizer? gestureRecognizer)
{ {
if (CapturedGestureRecognizer != gestureRecognizer) if (CapturedGestureRecognizer != gestureRecognizer)

24
src/Avalonia.Base/Media/BoxShadow.cs

@ -73,40 +73,44 @@ namespace Avalonia.Media
public override string ToString() public override string ToString()
{ {
var sb = StringBuilderCache.Acquire(); var sb = StringBuilderCache.Acquire();
ToString(sb);
return StringBuilderCache.GetStringAndRelease(sb);
}
internal void ToString(StringBuilder sb)
{
if (this == default) if (this == default)
{ {
return "none"; sb.Append("none");
return;
} }
if (IsInset) if (IsInset)
{ {
sb.Append("inset"); sb.Append("inset ");
} }
if (OffsetX != 0.0) if (OffsetX != 0.0)
{ {
sb.AppendFormat(" {0}", OffsetX.ToString(CultureInfo.InvariantCulture)); sb.AppendFormat("{0} ", OffsetX.ToString(CultureInfo.InvariantCulture));
} }
if (OffsetY != 0.0) if (OffsetY != 0.0)
{ {
sb.AppendFormat(" {0}", OffsetY.ToString(CultureInfo.InvariantCulture)); sb.AppendFormat("{0} ", OffsetY.ToString(CultureInfo.InvariantCulture));
} }
if (Blur != 0.0) if (Blur != 0.0)
{ {
sb.AppendFormat(" {0}", Blur.ToString(CultureInfo.InvariantCulture)); sb.AppendFormat("{0} ", Blur.ToString(CultureInfo.InvariantCulture));
} }
if (Spread != 0.0) if (Spread != 0.0)
{ {
sb.AppendFormat(" {0}", Spread.ToString(CultureInfo.InvariantCulture)); sb.AppendFormat("{0} ", Spread.ToString(CultureInfo.InvariantCulture));
} }
sb.AppendFormat(" {0}", Color.ToString()); sb.AppendFormat("{0}", Color.ToString());
return StringBuilderCache.GetStringAndRelease(sb);
} }
public static unsafe BoxShadow Parse(string s) public static unsafe BoxShadow Parse(string s)

10
src/Avalonia.Base/Media/BoxShadows.cs

@ -39,20 +39,20 @@ namespace Avalonia.Media
public override string ToString() public override string ToString()
{ {
var sb = StringBuilderCache.Acquire();
if (Count == 0) if (Count == 0)
{ {
return "none"; return "none";
} }
var sb = StringBuilderCache.Acquire();
foreach (var boxShadow in this) foreach (var boxShadow in this)
{ {
sb.AppendFormat("{0} ", boxShadow.ToString()); boxShadow.ToString(sb);
sb.Append(',');
sb.Append(' ');
} }
sb.Remove(sb.Length - 2, 2);
return StringBuilderCache.GetStringAndRelease(sb); return StringBuilderCache.GetStringAndRelease(sb);
} }
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]

4
src/Avalonia.Base/Media/MediaContext.cs

@ -213,10 +213,10 @@ internal partial class MediaContext : ICompositorScheduler
} }
/// <summary> /// <summary>
/// Executes the <param name="callback">callback</param> in the next iteration of the current UI-thread /// Executes the <paramref name="callback">callback</paramref> in the next iteration of the current UI-thread
/// render loop / layout pass that. /// render loop / layout pass that.
/// </summary> /// </summary>
/// <param name="callback">Code to execute.</param> /// <param name="callback"></param>
public void BeginInvokeOnRender(Action callback) public void BeginInvokeOnRender(Action callback)
{ {
if (_invokeOnRenderCallbacks == null) if (_invokeOnRenderCallbacks == null)

2
src/Avalonia.Base/Platform/IDrawingContextImpl.cs

@ -161,7 +161,7 @@ namespace Avalonia.Platform
void PopGeometryClip(); void PopGeometryClip();
/// <summary> /// <summary>
/// Get Fetaure from type
/// <summary> /// <summary>
/// Attempts to get an optional feature from the drawing context implementation /// Attempts to get an optional feature from the drawing context implementation
/// </summary> /// </summary>

4
src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs

@ -216,7 +216,7 @@ namespace Avalonia.PropertyStore
valueChanged = !EqualityComparer<T>.Default.Equals(Value, v); valueChanged = !EqualityComparer<T>.Default.Equals(Value, v);
Value = v; Value = v;
Priority = priority; Priority = priority;
if (_uncommon is not null) if (!isCoercedDefaultValue && _uncommon is not null)
_uncommon._uncoercedValue = value; _uncommon._uncoercedValue = value;
} }
@ -225,7 +225,7 @@ namespace Avalonia.PropertyStore
baseValueChanged = !EqualityComparer<T>.Default.Equals(_baseValue, v); baseValueChanged = !EqualityComparer<T>.Default.Equals(_baseValue, v);
_baseValue = v; _baseValue = v;
BasePriority = priority; BasePriority = priority;
if (_uncommon is not null) if (!isCoercedDefaultValue && _uncommon is not null)
_uncommon._uncoercedBaseValue = value; _uncommon._uncoercedBaseValue = value;
} }

25
src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs

@ -19,6 +19,12 @@ namespace Avalonia.Rendering.Composition.Animations
public static ScalarInterpolator Instance { get; } = new ScalarInterpolator(); public static ScalarInterpolator Instance { get; } = new ScalarInterpolator();
} }
class DoubleInterpolator : IInterpolator<double>
{
public double Interpolate(double @from, double to, float progress) => @from + (to - @from) * progress;
public static DoubleInterpolator Instance { get; } = new ();
}
class Vector2Interpolator : IInterpolator<Vector2> class Vector2Interpolator : IInterpolator<Vector2>
{ {
@ -28,6 +34,15 @@ namespace Avalonia.Rendering.Composition.Animations
public static Vector2Interpolator Instance { get; } = new Vector2Interpolator(); public static Vector2Interpolator Instance { get; } = new Vector2Interpolator();
} }
class VectorInterpolator : IInterpolator<Vector>
{
public Vector Interpolate(Vector @from, Vector to, float progress)
=> new(DoubleInterpolator.Instance.Interpolate(from.X, to.X, progress),
DoubleInterpolator.Instance.Interpolate(from.Y, to.Y, progress));
public static VectorInterpolator Instance { get; } = new ();
}
class Vector3Interpolator : IInterpolator<Vector3> class Vector3Interpolator : IInterpolator<Vector3>
{ {
public Vector3 Interpolate(Vector3 @from, Vector3 to, float progress) public Vector3 Interpolate(Vector3 @from, Vector3 to, float progress)
@ -36,6 +51,16 @@ namespace Avalonia.Rendering.Composition.Animations
public static Vector3Interpolator Instance { get; } = new Vector3Interpolator(); public static Vector3Interpolator Instance { get; } = new Vector3Interpolator();
} }
class Vector3DInterpolator : IInterpolator<Vector3D>
{
public Vector3D Interpolate(Vector3D @from, Vector3D to, float progress)
=> new(DoubleInterpolator.Instance.Interpolate(from.X, to.X, progress),
DoubleInterpolator.Instance.Interpolate(from.Y, to.Y, progress),
DoubleInterpolator.Instance.Interpolate(from.Z, to.Z, progress));
public static Vector3DInterpolator Instance { get; } = new ();
}
class Vector4Interpolator : IInterpolator<Vector4> class Vector4Interpolator : IInterpolator<Vector4>
{ {
public Vector4 Interpolate(Vector4 @from, Vector4 to, float progress) public Vector4 Interpolate(Vector4 @from, Vector4 to, float progress)

6
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@ -241,8 +241,8 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester
continue; continue;
// TODO: Optimize all of that by moving to the Visual itself, so we won't have to recalculate every time // TODO: Optimize all of that by moving to the Visual itself, so we won't have to recalculate every time
comp.Offset = new Vector3((float)visual.Bounds.Left, (float)visual.Bounds.Top, 0); comp.Offset = new (visual.Bounds.Left, visual.Bounds.Top, 0);
comp.Size = new Vector2((float)visual.Bounds.Width, (float)visual.Bounds.Height); comp.Size = new (visual.Bounds.Width, visual.Bounds.Height);
comp.Visible = visual.IsVisible; comp.Visible = visual.IsVisible;
comp.Opacity = (float)visual.Opacity; comp.Opacity = (float)visual.Opacity;
comp.ClipToBounds = visual.ClipToBounds; comp.ClipToBounds = visual.ClipToBounds;
@ -269,7 +269,7 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester
renderTransform *= (-offset) * visual.RenderTransform.Value * (offset); renderTransform *= (-offset) * visual.RenderTransform.Value * (offset);
} }
comp.TransformMatrix = MatrixUtils.ToMatrix4x4(renderTransform); comp.TransformMatrix = renderTransform;
try try
{ {

2
src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs

@ -28,7 +28,7 @@ public abstract class CompositionCustomVisualHandler
_host.Compositor.VerifyAccess(); _host.Compositor.VerifyAccess();
} }
protected Vector2 EffectiveSize protected Vector EffectiveSize
{ {
get get
{ {

2
src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs

@ -70,7 +70,7 @@ namespace Avalonia.Rendering.Composition
return false; return false;
} }
var m33 = MatrixUtils.ToMatrix(m.Value); var m33 = m.Value;
return m33.TryInvert(out matrix); return m33.TryInvert(out matrix);
} }

59
src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs

@ -16,6 +16,7 @@ namespace Avalonia.Rendering.Composition.Expressions
private readonly DelegateExpressionFfi _registry; private readonly DelegateExpressionFfi _registry;
static float Lerp(float a, float b, float p) => p * (b - a) + a; static float Lerp(float a, float b, float p) => p * (b - a) + a;
static double Lerp(double a, double b, double p) => p * (b - a) + a;
static Matrix3x2 Inverse(Matrix3x2 m) static Matrix3x2 Inverse(Matrix3x2 m)
{ {
@ -34,6 +35,13 @@ namespace Avalonia.Rendering.Composition.Expressions
var t = MathUtilities.Clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); var t = MathUtilities.Clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f);
return t * t * (3.0f - 2.0f * t); return t * t * (3.0f - 2.0f * t);
} }
static double SmoothStep(double edge0, double edge1, double x)
{
var t = MathUtilities.Clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f);
return t * t * (3.0f - 2.0f * t);
}
static Vector2 SmoothStep(Vector2 edge0, Vector2 edge1, Vector2 x) static Vector2 SmoothStep(Vector2 edge0, Vector2 edge1, Vector2 x)
{ {
@ -43,6 +51,15 @@ namespace Avalonia.Rendering.Composition.Expressions
); );
} }
static Vector SmoothStep(Vector edge0, Vector edge1, Vector x)
{
return new (
SmoothStep(edge0.X, edge1.X, x.X),
SmoothStep(edge0.Y, edge1.Y, x.Y)
);
}
static Vector3 SmoothStep(Vector3 edge0, Vector3 edge1, Vector3 x) static Vector3 SmoothStep(Vector3 edge0, Vector3 edge1, Vector3 x)
{ {
return new Vector3( return new Vector3(
@ -52,6 +69,15 @@ namespace Avalonia.Rendering.Composition.Expressions
); );
} }
static Vector3D SmoothStep(Vector3D edge0, Vector3D edge1, Vector3D x)
{
return new (
SmoothStep(edge0.X, edge1.X, x.X),
SmoothStep(edge0.Y, edge1.Y, x.Y),
SmoothStep(edge0.Z, edge1.Z, x.Z)
);
}
static Vector4 SmoothStep(Vector4 edge0, Vector4 edge1, Vector4 x) static Vector4 SmoothStep(Vector4 edge0, Vector4 edge1, Vector4 x)
{ {
@ -69,7 +95,9 @@ namespace Avalonia.Rendering.Composition.Expressions
{ {
{"Abs", (float f) => Math.Abs(f)}, {"Abs", (float f) => Math.Abs(f)},
{"Abs", (Vector2 v) => Vector2.Abs(v)}, {"Abs", (Vector2 v) => Vector2.Abs(v)},
{"Abs", (Vector v) => v.Abs()},
{"Abs", (Vector3 v) => Vector3.Abs(v)}, {"Abs", (Vector3 v) => Vector3.Abs(v)},
{"Abs", (Vector3D v) => v.Abs()},
{"Abs", (Vector4 v) => Vector4.Abs(v)}, {"Abs", (Vector4 v) => Vector4.Abs(v)},
{"ACos", (float f) => (float) Math.Acos(f)}, {"ACos", (float f) => (float) Math.Acos(f)},
@ -79,7 +107,9 @@ namespace Avalonia.Rendering.Composition.Expressions
{"Clamp", (float a1, float a2, float a3) => MathUtilities.Clamp(a1, a2, a3)}, {"Clamp", (float a1, float a2, float a3) => MathUtilities.Clamp(a1, a2, a3)},
{"Clamp", (Vector2 a1, Vector2 a2, Vector2 a3) => Vector2.Clamp(a1, a2, a3)}, {"Clamp", (Vector2 a1, Vector2 a2, Vector2 a3) => Vector2.Clamp(a1, a2, a3)},
{"Clamp", (Vector a1, Vector a2, Vector a3) => Vector.Clamp(a1, a2, a3)},
{"Clamp", (Vector3 a1, Vector3 a2, Vector3 a3) => Vector3.Clamp(a1, a2, a3)}, {"Clamp", (Vector3 a1, Vector3 a2, Vector3 a3) => Vector3.Clamp(a1, a2, a3)},
{"Clamp", (Vector3D a1, Vector3D a2, Vector3D a3) => Vector3D.Clamp(a1, a2, a3)},
{"Clamp", (Vector4 a1, Vector4 a2, Vector4 a3) => Vector4.Clamp(a1, a2, a3)}, {"Clamp", (Vector4 a1, Vector4 a2, Vector4 a3) => Vector4.Clamp(a1, a2, a3)},
{"Concatenate", (Quaternion a1, Quaternion a2) => Quaternion.Concatenate(a1, a2)}, {"Concatenate", (Quaternion a1, Quaternion a2) => Quaternion.Concatenate(a1, a2)},
@ -109,11 +139,15 @@ namespace Avalonia.Rendering.Composition.Expressions
}, },
{"Distance", (Vector2 a1, Vector2 a2) => Vector2.Distance(a1, a2)}, {"Distance", (Vector2 a1, Vector2 a2) => Vector2.Distance(a1, a2)},
{"Distance", (Vector a1, Vector a2) => Vector.Distance(a1, a2)},
{"Distance", (Vector3 a1, Vector3 a2) => Vector3.Distance(a1, a2)}, {"Distance", (Vector3 a1, Vector3 a2) => Vector3.Distance(a1, a2)},
{"Distance", (Vector3D a1, Vector3D a2) => Vector3D.Distance(a1, a2)},
{"Distance", (Vector4 a1, Vector4 a2) => Vector4.Distance(a1, a2)}, {"Distance", (Vector4 a1, Vector4 a2) => Vector4.Distance(a1, a2)},
{"DistanceSquared", (Vector2 a1, Vector2 a2) => Vector2.DistanceSquared(a1, a2)}, {"DistanceSquared", (Vector2 a1, Vector2 a2) => Vector2.DistanceSquared(a1, a2)},
{"DistanceSquared", (Vector a1, Vector a2) => Vector.DistanceSquared(a1, a2)},
{"DistanceSquared", (Vector3 a1, Vector3 a2) => Vector3.DistanceSquared(a1, a2)}, {"DistanceSquared", (Vector3 a1, Vector3 a2) => Vector3.DistanceSquared(a1, a2)},
{"DistanceSquared", (Vector3D a1, Vector3D a2) => Vector3D.DistanceSquared(a1, a2)},
{"DistanceSquared", (Vector4 a1, Vector4 a2) => Vector4.DistanceSquared(a1, a2)}, {"DistanceSquared", (Vector4 a1, Vector4 a2) => Vector4.DistanceSquared(a1, a2)},
{"Floor", (float v) => (float) Math.Floor(v)}, {"Floor", (float v) => (float) Math.Floor(v)},
@ -123,18 +157,24 @@ namespace Avalonia.Rendering.Composition.Expressions
{"Length", (Vector2 a1) => a1.Length()}, {"Length", (Vector2 a1) => a1.Length()},
{"Length", (Vector a1) => a1.Length},
{"Length", (Vector3 a1) => a1.Length()}, {"Length", (Vector3 a1) => a1.Length()},
{"Length", (Vector3D a1) => a1.Length},
{"Length", (Vector4 a1) => a1.Length()}, {"Length", (Vector4 a1) => a1.Length()},
{"Length", (Quaternion a1) => a1.Length()}, {"Length", (Quaternion a1) => a1.Length()},
{"LengthSquared", (Vector2 a1) => a1.LengthSquared()}, {"LengthSquared", (Vector2 a1) => a1.LengthSquared()},
{"LengthSquared", (Vector a1) => a1*a1},
{"LengthSquared", (Vector3 a1) => a1.LengthSquared()}, {"LengthSquared", (Vector3 a1) => a1.LengthSquared()},
{"LengthSquared", (Vector3D a1) => Vector3D.Dot(a1, a1)},
{"LengthSquared", (Vector4 a1) => a1.LengthSquared()}, {"LengthSquared", (Vector4 a1) => a1.LengthSquared()},
{"LengthSquared", (Quaternion a1) => a1.LengthSquared()}, {"LengthSquared", (Quaternion a1) => a1.LengthSquared()},
{"Lerp", (float a1, float a2, float a3) => Lerp(a1, a2, a3)}, {"Lerp", (float a1, float a2, float a3) => Lerp(a1, a2, a3)},
{"Lerp", (Vector2 a1, Vector2 a2, float a3) => Vector2.Lerp(a1, a2, a3)}, {"Lerp", (Vector2 a1, Vector2 a2, float a3) => Vector2.Lerp(a1, a2, a3)},
{"Lerp", (Vector a1, Vector a2, float a3) => new Vector(Lerp(a1.X, a2.X, a3), Lerp(a1.Y, a2.Y, a3))},
{"Lerp", (Vector3 a1, Vector3 a2, float a3) => Vector3.Lerp(a1, a2, a3)}, {"Lerp", (Vector3 a1, Vector3 a2, float a3) => Vector3.Lerp(a1, a2, a3)},
{"Lerp", (Vector3D a1, Vector3D a2, float a3) => new Vector3D(Lerp(a1.X, a2.X, a3), Lerp(a1.Y, a2.Y, a3), Lerp(a1.Z, a2.Z, a3))},
{"Lerp", (Vector4 a1, Vector4 a2, float a3) => Vector4.Lerp(a1, a2, a3)}, {"Lerp", (Vector4 a1, Vector4 a2, float a3) => Vector4.Lerp(a1, a2, a3)},
@ -173,24 +213,31 @@ namespace Avalonia.Rendering.Composition.Expressions
{"Max", (float a1, float a2) => Math.Max(a1, a2)}, {"Max", (float a1, float a2) => Math.Max(a1, a2)},
{"Max", (Vector2 a1, Vector2 a2) => Vector2.Max(a1, a2)}, {"Max", (Vector2 a1, Vector2 a2) => Vector2.Max(a1, a2)},
{"Max", (Vector a1, Vector a2) => Vector.Max(a1, a2)},
{"Max", (Vector3 a1, Vector3 a2) => Vector3.Max(a1, a2)}, {"Max", (Vector3 a1, Vector3 a2) => Vector3.Max(a1, a2)},
{"Max", (Vector3D a1, Vector3D a2) => Vector3D.Max(a1, a2)},
{"Max", (Vector4 a1, Vector4 a2) => Vector4.Max(a1, a2)}, {"Max", (Vector4 a1, Vector4 a2) => Vector4.Max(a1, a2)},
{"Min", (float a1, float a2) => Math.Min(a1, a2)}, {"Min", (float a1, float a2) => Math.Min(a1, a2)},
{"Min", (Vector2 a1, Vector2 a2) => Vector2.Min(a1, a2)}, {"Min", (Vector2 a1, Vector2 a2) => Vector2.Min(a1, a2)},
{"Min", (Vector a1, Vector a2) => Vector.Min(a1, a2)},
{"Min", (Vector3 a1, Vector3 a2) => Vector3.Min(a1, a2)}, {"Min", (Vector3 a1, Vector3 a2) => Vector3.Min(a1, a2)},
{"Min", (Vector3D a1, Vector3D a2) => Vector3D.Min(a1, a2)},
{"Min", (Vector4 a1, Vector4 a2) => Vector4.Min(a1, a2)}, {"Min", (Vector4 a1, Vector4 a2) => Vector4.Min(a1, a2)},
{"Mod", (float a, float b) => a % b}, {"Mod", (float a, float b) => a % b},
{"Normalize", (Quaternion a) => Quaternion.Normalize(a)}, {"Normalize", (Quaternion a) => Quaternion.Normalize(a)},
{"Normalize", (Vector2 a) => Vector2.Normalize(a)}, {"Normalize", (Vector2 a) => Vector2.Normalize(a)},
{"Normalize", (Vector a) => Vector.Normalize(a)},
{"Normalize", (Vector3 a) => Vector3.Normalize(a)}, {"Normalize", (Vector3 a) => Vector3.Normalize(a)},
{"Normalize", (Vector3D a) => Vector3D.Normalize(a)},
{"Normalize", (Vector4 a) => Vector4.Normalize(a)}, {"Normalize", (Vector4 a) => Vector4.Normalize(a)},
{"Pow", (float a, float b) => (float) Math.Pow(a, b)}, {"Pow", (float a, float b) => (float) Math.Pow(a, b)},
{"Quaternion.CreateFromAxisAngle", (Vector3 a, float b) => Quaternion.CreateFromAxisAngle(a, b)}, {"Quaternion.CreateFromAxisAngle", (Vector3 a, float b) => Quaternion.CreateFromAxisAngle(a, b)},
{"Quaternion.CreateFromAxisAngle", (Vector3D a, float b) => Quaternion.CreateFromAxisAngle(a.ToVector3(), b)},
{"Quaternion", (float a, float b, float c, float d) => new Quaternion(a, b, c, d)}, {"Quaternion", (float a, float b, float c, float d) => new Quaternion(a, b, c, d)},
{"Round", (float a) => (float) Math.Round(a)}, {"Round", (float a) => (float) Math.Round(a)},
@ -198,14 +245,18 @@ namespace Avalonia.Rendering.Composition.Expressions
{"Scale", (Matrix3x2 a, float b) => a * b}, {"Scale", (Matrix3x2 a, float b) => a * b},
{"Scale", (Matrix4x4 a, float b) => a * b}, {"Scale", (Matrix4x4 a, float b) => a * b},
{"Scale", (Vector2 a, float b) => a * b}, {"Scale", (Vector2 a, float b) => a * b},
{"Scale", (Vector a, float b) => a * b},
{"Scale", (Vector3 a, float b) => a * b}, {"Scale", (Vector3 a, float b) => a * b},
{"Scale", (Vector3D a, float b) => Vector3D.Multiply(a, b)},
{"Scale", (Vector4 a, float b) => a * b}, {"Scale", (Vector4 a, float b) => a * b},
{"Sin", (float a) => (float) Math.Sin(a)}, {"Sin", (float a) => (float) Math.Sin(a)},
{"SmoothStep", (float a1, float a2, float a3) => SmoothStep(a1, a2, a3)}, {"SmoothStep", (float a1, float a2, float a3) => SmoothStep(a1, a2, a3)},
{"SmoothStep", (Vector2 a1, Vector2 a2, Vector2 a3) => SmoothStep(a1, a2, a3)}, {"SmoothStep", (Vector2 a1, Vector2 a2, Vector2 a3) => SmoothStep(a1, a2, a3)},
{"SmoothStep", (Vector a1, Vector a2, Vector a3) => SmoothStep(a1, a2, a3)},
{"SmoothStep", (Vector3 a1, Vector3 a2, Vector3 a3) => SmoothStep(a1, a2, a3)}, {"SmoothStep", (Vector3 a1, Vector3 a2, Vector3 a3) => SmoothStep(a1, a2, a3)},
{"SmoothStep", (Vector3D a1, Vector3D a2, Vector3D a3) => SmoothStep(a1, a2, a3)},
{"SmoothStep", (Vector4 a1, Vector4 a2, Vector4 a3) => SmoothStep(a1, a2, a3)}, {"SmoothStep", (Vector4 a1, Vector4 a2, Vector4 a3) => SmoothStep(a1, a2, a3)},
// I have no idea how to do a spherical interpolation for a scalar value, so we are doing a linear one // I have no idea how to do a spherical interpolation for a scalar value, so we are doing a linear one
@ -222,9 +273,13 @@ namespace Avalonia.Rendering.Composition.Expressions
{"Transform", (Vector2 a, Matrix3x2 b) => Vector2.Transform(a, b)}, {"Transform", (Vector2 a, Matrix3x2 b) => Vector2.Transform(a, b)},
{"Transform", (Vector3 a, Matrix4x4 b) => Vector3.Transform(a, b)}, {"Transform", (Vector3 a, Matrix4x4 b) => Vector3.Transform(a, b)},
{"Vector2", (float a, float b) => new Vector2(a, b)}, {"Vector2", (float a, float b) => new Vector(a, b)},
{"Vector3", (float a, float b, float c) => new Vector3(a, b, c)}, {"Vector2", (double a, double b) => new Vector(a, b)},
{"Vector3", (float a, float b, float c) => new Vector3D(a, b, c)},
{"Vector3", (double a, double b, double c) => new Vector3D(a, b, c)},
{"Vector3", (Vector2 v2, float z) => new Vector3(v2, z)}, {"Vector3", (Vector2 v2, float z) => new Vector3(v2, z)},
{"Vector3", (Vector v2, float z) => new Vector3D(v2.X, v2.Y, z)},
{"Vector3", (Vector v2, double z) => new Vector3D(v2.X, v2.Y, z)},
{"Vector4", (float a, float b, float c, float d) => new Vector4(a, b, c, d)}, {"Vector4", (float a, float b, float c, float d) => new Vector4(a, b, c, d)},
{"Vector4", (Vector2 v2, float z, float w) => new Vector4(v2, z, w)}, {"Vector4", (Vector2 v2, float z, float w) => new Vector4(v2, z, w)},
{"Vector4", (Vector3 v3, float w) => new Vector4(v3, w)}, {"Vector4", (Vector3 v3, float w) => new Vector4(v3, w)},

43
src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs

@ -49,6 +49,46 @@ namespace Avalonia.Rendering.Composition.Expressions
} }
} }
return CallWithCast(countGroup, arguments, out result, false);
}
bool CallWithCast(List<FfiRecord> countGroup, IReadOnlyList<ExpressionVariant> arguments, out ExpressionVariant result, bool anyCast)
{
result = default;
foreach (var record in countGroup)
{
var match = true;
for (var c = 0; c < arguments.Count; c++)
{
var parameter = record.Types[c];
var arg = arguments[c].Type;
if (parameter != arg)
{
var canCast = (parameter == VariantType.Double && arg == VariantType.Scalar)
|| (parameter == VariantType.Vector3D && arg == VariantType.Vector3)
|| (parameter == VariantType.Vector && arg == VariantType.Vector2)
|| (anyCast && (
(arg == VariantType.Double && parameter == VariantType.Scalar)
|| (arg == VariantType.Vector3D && parameter == VariantType.Vector3)
|| (arg == VariantType.Vector && parameter == VariantType.Vector2)
));
if (!canCast)
{
match = false;
break;
}
}
}
if (match)
{
result = record.Delegate(arguments);
return true;
}
}
if (anyCast == false)
return CallWithCast(countGroup, arguments, out result, true);
return false; return false;
} }
@ -75,8 +115,11 @@ namespace Avalonia.Rendering.Composition.Expressions
{ {
[typeof(bool)] = VariantType.Boolean, [typeof(bool)] = VariantType.Boolean,
[typeof(float)] = VariantType.Scalar, [typeof(float)] = VariantType.Scalar,
[typeof(double)] = VariantType.Double,
[typeof(Vector2)] = VariantType.Vector2, [typeof(Vector2)] = VariantType.Vector2,
[typeof(Vector)] = VariantType.Vector,
[typeof(Vector3)] = VariantType.Vector3, [typeof(Vector3)] = VariantType.Vector3,
[typeof(Vector3D)] = VariantType.Vector3D,
[typeof(Vector4)] = VariantType.Vector4, [typeof(Vector4)] = VariantType.Vector4,
[typeof(Matrix3x2)] = VariantType.Matrix3x2, [typeof(Matrix3x2)] = VariantType.Matrix3x2,
[typeof(Matrix4x4)] = VariantType.Matrix4x4, [typeof(Matrix4x4)] = VariantType.Matrix4x4,

178
src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs

@ -17,6 +17,8 @@ namespace Avalonia.Rendering.Composition.Expressions
Vector2, Vector2,
Vector3, Vector3,
Vector4, Vector4,
Vector,
Vector3D,
AvaloniaMatrix, AvaloniaMatrix,
Matrix3x2, Matrix3x2,
Matrix4x4, Matrix4x4,
@ -38,6 +40,8 @@ namespace Avalonia.Rendering.Composition.Expressions
[FieldOffset(4)] public Vector2 Vector2; [FieldOffset(4)] public Vector2 Vector2;
[FieldOffset(4)] public Vector3 Vector3; [FieldOffset(4)] public Vector3 Vector3;
[FieldOffset(4)] public Vector4 Vector4; [FieldOffset(4)] public Vector4 Vector4;
[FieldOffset(4)] public Vector Vector;
[FieldOffset(4)] public Vector3D Vector3D;
[FieldOffset(4)] public Matrix AvaloniaMatrix; [FieldOffset(4)] public Matrix AvaloniaMatrix;
[FieldOffset(4)] public Matrix3x2 Matrix3x2; [FieldOffset(4)] public Matrix3x2 Matrix3x2;
[FieldOffset(4)] public Matrix4x4 Matrix4x4; [FieldOffset(4)] public Matrix4x4 Matrix4x4;
@ -55,6 +59,15 @@ namespace Avalonia.Rendering.Composition.Expressions
return Vector2.Y; return Vector2.Y;
return default; return default;
} }
if (Type == VariantType.Vector)
{
if (ReferenceEquals(property, "X"))
return Vector.X;
if (ReferenceEquals(property, "Y"))
return Vector.Y;
return default;
}
if (Type == VariantType.Vector3) if (Type == VariantType.Vector3)
{ {
@ -78,6 +91,29 @@ namespace Avalonia.Rendering.Composition.Expressions
return new Vector2(Vector3.Z, Vector3.Y); return new Vector2(Vector3.Z, Vector3.Y);
return default; return default;
} }
if (Type == VariantType.Vector3D)
{
if (ReferenceEquals(property, "X"))
return Vector3D.X;
if (ReferenceEquals(property, "Y"))
return Vector3D.Y;
if (ReferenceEquals(property, "Z"))
return Vector3D.Z;
if(ReferenceEquals(property, "XY"))
return new Vector(Vector3D.X, Vector3D.Y);
if(ReferenceEquals(property, "YX"))
return new Vector(Vector3D.Y, Vector3D.X);
if(ReferenceEquals(property, "XZ"))
return new Vector(Vector3D.X, Vector3D.Z);
if(ReferenceEquals(property, "ZX"))
return new Vector(Vector3D.Z, Vector3D.X);
if(ReferenceEquals(property, "YZ"))
return new Vector(Vector3D.Y, Vector3D.Z);
if(ReferenceEquals(property, "ZY"))
return new Vector(Vector3D.Z, Vector3D.Y);
return default;
}
if (Type == VariantType.Vector4) if (Type == VariantType.Vector4)
{ {
@ -115,14 +151,20 @@ namespace Avalonia.Rendering.Composition.Expressions
return AvaloniaMatrix.M11; return AvaloniaMatrix.M11;
if (ReferenceEquals(property, "M12")) if (ReferenceEquals(property, "M12"))
return AvaloniaMatrix.M12; return AvaloniaMatrix.M12;
if (ReferenceEquals(property, "M13"))
return AvaloniaMatrix.M13;
if (ReferenceEquals(property, "M21")) if (ReferenceEquals(property, "M21"))
return AvaloniaMatrix.M21; return AvaloniaMatrix.M21;
if (ReferenceEquals(property, "M22")) if (ReferenceEquals(property, "M22"))
return AvaloniaMatrix.M22; return AvaloniaMatrix.M22;
if (ReferenceEquals(property, "M23"))
return AvaloniaMatrix.M23;
if (ReferenceEquals(property, "M31")) if (ReferenceEquals(property, "M31"))
return AvaloniaMatrix.M31; return AvaloniaMatrix.M31;
if (ReferenceEquals(property, "M32")) if (ReferenceEquals(property, "M32"))
return AvaloniaMatrix.M32; return AvaloniaMatrix.M32;
if (ReferenceEquals(property, "M33"))
return AvaloniaMatrix.M33;
return default; return default;
} }
@ -220,7 +262,13 @@ namespace Avalonia.Rendering.Composition.Expressions
Type = VariantType.Vector2, Type = VariantType.Vector2,
Vector2 = value Vector2 = value
}; };
public static implicit operator ExpressionVariant(Vector value) =>
new ExpressionVariant
{
Type = VariantType.Vector,
Vector = value
};
public static implicit operator ExpressionVariant(Vector3 value) => public static implicit operator ExpressionVariant(Vector3 value) =>
new ExpressionVariant new ExpressionVariant
@ -228,6 +276,13 @@ namespace Avalonia.Rendering.Composition.Expressions
Type = VariantType.Vector3, Type = VariantType.Vector3,
Vector3 = value Vector3 = value
}; };
public static implicit operator ExpressionVariant(Vector3D value) =>
new ExpressionVariant
{
Type = VariantType.Vector3D,
Vector3D = value
};
public static implicit operator ExpressionVariant(Vector4 value) => public static implicit operator ExpressionVariant(Vector4 value) =>
@ -285,10 +340,16 @@ namespace Avalonia.Rendering.Composition.Expressions
if (left.Type == VariantType.Vector2) if (left.Type == VariantType.Vector2)
return left.Vector2 + right.Vector2; return left.Vector2 + right.Vector2;
if (left.Type == VariantType.Vector)
return left.Vector + right.Vector;
if (left.Type == VariantType.Vector3) if (left.Type == VariantType.Vector3)
return left.Vector3 + right.Vector3; return left.Vector3 + right.Vector3;
if (left.Type == VariantType.Vector3D)
return Avalonia.Vector3D.Add(left.Vector3D, right.Vector3D);
if (left.Type == VariantType.Vector4) if (left.Type == VariantType.Vector4)
return left.Vector4 + right.Vector4; return left.Vector4 + right.Vector4;
@ -317,10 +378,16 @@ namespace Avalonia.Rendering.Composition.Expressions
if (left.Type == VariantType.Vector2) if (left.Type == VariantType.Vector2)
return left.Vector2 - right.Vector2; return left.Vector2 - right.Vector2;
if (left.Type == VariantType.Vector)
return left.Vector - right.Vector;
if (left.Type == VariantType.Vector3) if (left.Type == VariantType.Vector3)
return left.Vector3 - right.Vector3; return left.Vector3 - right.Vector3;
if (left.Type == VariantType.Vector3D)
return Vector3D.Add(left.Vector3D, -right.Vector3D);
if (left.Type == VariantType.Vector4) if (left.Type == VariantType.Vector4)
return left.Vector4 - right.Vector4; return left.Vector4 - right.Vector4;
@ -347,9 +414,15 @@ namespace Avalonia.Rendering.Composition.Expressions
if (left.Type == VariantType.Vector2) if (left.Type == VariantType.Vector2)
return -left.Vector2; return -left.Vector2;
if (left.Type == VariantType.Vector)
return -left.Vector;
if (left.Type == VariantType.Vector3) if (left.Type == VariantType.Vector3)
return -left.Vector3; return -left.Vector3;
if (left.Type == VariantType.Vector3D)
return -left.Vector3D;
if (left.Type == VariantType.Vector4) if (left.Type == VariantType.Vector4)
return -left.Vector4; return -left.Vector4;
@ -383,14 +456,29 @@ namespace Avalonia.Rendering.Composition.Expressions
if (left.Type == VariantType.Vector2 && right.Type == VariantType.Vector2) if (left.Type == VariantType.Vector2 && right.Type == VariantType.Vector2)
return left.Vector2 * right.Vector2; return left.Vector2 * right.Vector2;
if (left.Type == VariantType.Vector && right.Type == VariantType.Vector)
return Vector.Multiply(left.Vector, right.Vector);
if (left.Type == VariantType.Vector2 && right.Type == VariantType.Scalar) if (left.Type == VariantType.Vector2 && right.Type == VariantType.Scalar)
return left.Vector2 * right.Scalar; return left.Vector2 * right.Scalar;
if (left.Type == VariantType.Vector && right.Type == VariantType.Scalar)
return left.Vector * right.Scalar;
if (left.Type == VariantType.Vector && right.Type == VariantType.Double)
return left.Vector * right.Double;
if (left.Type == VariantType.Vector3 && right.Type == VariantType.Vector3) if (left.Type == VariantType.Vector3 && right.Type == VariantType.Vector3)
return left.Vector3 * right.Vector3; return left.Vector3 * right.Vector3;
if (left.Type == VariantType.Vector3D && right.Type == VariantType.Vector3D)
return Vector3D.Multiply(left.Vector3D, right.Vector3D);
if (left.Type == VariantType.Vector3 && right.Type == VariantType.Scalar) if (left.Type == VariantType.Vector3 && right.Type == VariantType.Scalar)
return left.Vector3 * right.Scalar; return left.Vector3 * right.Scalar;
if (left.Type == VariantType.Vector3D && right.Type == VariantType.Scalar)
return Vector3D.Multiply(left.Vector3D, right.Scalar);
if (left.Type == VariantType.Vector4 && right.Type == VariantType.Vector4) if (left.Type == VariantType.Vector4 && right.Type == VariantType.Vector4)
return left.Vector4 * right.Vector4; return left.Vector4 * right.Vector4;
@ -436,15 +524,33 @@ namespace Avalonia.Rendering.Composition.Expressions
if (left.Type == VariantType.Vector2 && right.Type == VariantType.Vector2) if (left.Type == VariantType.Vector2 && right.Type == VariantType.Vector2)
return left.Vector2 / right.Vector2; return left.Vector2 / right.Vector2;
if (left.Type == VariantType.Vector && right.Type == VariantType.Vector)
return Vector.Divide(left.Vector, right.Vector);
if (left.Type == VariantType.Vector2 && right.Type == VariantType.Scalar) if (left.Type == VariantType.Vector2 && right.Type == VariantType.Scalar)
return left.Vector2 / right.Scalar; return left.Vector2 / right.Scalar;
if (left.Type == VariantType.Vector && right.Type == VariantType.Scalar)
return left.Vector / right.Scalar;
if (left.Type == VariantType.Vector && right.Type == VariantType.Double)
return left.Vector / right.Scalar;
if (left.Type == VariantType.Vector3 && right.Type == VariantType.Vector3) if (left.Type == VariantType.Vector3 && right.Type == VariantType.Vector3)
return left.Vector3 / right.Vector3; return left.Vector3 / right.Vector3;
if (left.Type == VariantType.Vector3D && right.Type == VariantType.Vector3D)
return Vector3D.Divide(left.Vector3D, right.Vector3D);
if (left.Type == VariantType.Vector3 && right.Type == VariantType.Scalar) if (left.Type == VariantType.Vector3 && right.Type == VariantType.Scalar)
return left.Vector3 / right.Scalar; return left.Vector3 / right.Scalar;
if (left.Type == VariantType.Vector3D && right.Type == VariantType.Scalar)
return Avalonia.Vector3D.Divide(left.Vector3D, right.Scalar);
if (left.Type == VariantType.Vector3D && right.Type == VariantType.Double)
return Avalonia.Vector3D.Divide(left.Vector3D, right.Double);
if (left.Type == VariantType.Vector4 && right.Type == VariantType.Vector4) if (left.Type == VariantType.Vector4 && right.Type == VariantType.Vector4)
return left.Vector4 / right.Vector4; return left.Vector4 / right.Vector4;
@ -471,9 +577,15 @@ namespace Avalonia.Rendering.Composition.Expressions
if (Type == VariantType.Vector2) if (Type == VariantType.Vector2)
return Vector2 == right.Vector2; return Vector2 == right.Vector2;
if (Type == VariantType.Vector)
return Vector == right.Vector;
if (Type == VariantType.Vector3) if (Type == VariantType.Vector3)
return Vector3 == right.Vector3; return Vector3 == right.Vector3;
if (Type == VariantType.Vector3D)
return Vector3D == right.Vector3D;
if (Type == VariantType.Vector4) if (Type == VariantType.Vector4)
return Vector4 == right.Vector4; return Vector4 == right.Vector4;
@ -571,6 +683,11 @@ namespace Avalonia.Rendering.Composition.Expressions
res = (T) (object) Scalar; res = (T) (object) Scalar;
return true; return true;
} }
if (Type == VariantType.Double)
{
res = (T)(object)Scalar;
return true;
}
} }
if (typeof(T) == typeof(double)) if (typeof(T) == typeof(double))
@ -580,6 +697,12 @@ namespace Avalonia.Rendering.Composition.Expressions
res = (T) (object) Double; res = (T) (object) Double;
return true; return true;
} }
if (Type == VariantType.Scalar)
{
res = (T)(object)(float)Double;
return true;
}
} }
if (typeof(T) == typeof(Vector2)) if (typeof(T) == typeof(Vector2))
@ -589,6 +712,27 @@ namespace Avalonia.Rendering.Composition.Expressions
res = (T) (object) Vector2; res = (T) (object) Vector2;
return true; return true;
} }
if (Type == VariantType.Vector)
{
res = (T) (object) Vector.ToVector2();
return true;
}
}
if (typeof(T) == typeof(Vector))
{
if (Type == VariantType.Vector)
{
res = (T) (object) Vector;
return true;
}
if (Type == VariantType.Vector2)
{
res = (T)(object)new Vector(Vector2);
return true;
}
} }
if (typeof(T) == typeof(Vector3)) if (typeof(T) == typeof(Vector3))
@ -598,6 +742,26 @@ namespace Avalonia.Rendering.Composition.Expressions
res = (T) (object) Vector3; res = (T) (object) Vector3;
return true; return true;
} }
if (Type == VariantType.Vector3D)
{
res = (T) (object) Vector3D.ToVector3();
return true;
}
}
if (typeof(T) == typeof(Vector3D))
{
if (Type == VariantType.Vector3D)
{
res = (T) (object) Vector3D;
return true;
}
if (Type == VariantType.Vector3)
{
res = (T)(object)new Vector3D(Vector3);
return true;
}
} }
if (typeof(T) == typeof(Vector4)) if (typeof(T) == typeof(Vector4))
@ -668,9 +832,15 @@ namespace Avalonia.Rendering.Composition.Expressions
if (typeof(T) == typeof(Vector2)) if (typeof(T) == typeof(Vector2))
return (Vector2) (object) v; return (Vector2) (object) v;
if (typeof(T) == typeof(Vector))
return (Vector) (object) v;
if (typeof(T) == typeof(Vector3)) if (typeof(T) == typeof(Vector3))
return (Vector3) (object) v; return (Vector3) (object) v;
if (typeof(T) == typeof(Vector3D))
return (Vector3D) (object) v;
if (typeof(T) == typeof(Vector4)) if (typeof(T) == typeof(Vector4))
return (Vector4) (object) v; return (Vector4) (object) v;
@ -709,8 +879,12 @@ namespace Avalonia.Rendering.Composition.Expressions
return Double.ToString(CultureInfo.InvariantCulture); return Double.ToString(CultureInfo.InvariantCulture);
if (Type == VariantType.Vector2) if (Type == VariantType.Vector2)
return Vector2.ToString(); return Vector2.ToString();
if (Type == VariantType.Vector)
return Vector.ToString();
if (Type == VariantType.Vector3) if (Type == VariantType.Vector3)
return Vector3.ToString(); return Vector3.ToString();
if (Type == VariantType.Vector3D)
return Vector3D.ToString();
if (Type == VariantType.Vector4) if (Type == VariantType.Vector4)
return Vector4.ToString(); return Vector4.ToString();
if (Type == VariantType.Quaternion) if (Type == VariantType.Quaternion)

33
src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs

@ -6,41 +6,46 @@ namespace Avalonia.Rendering.Composition
{ {
static class MatrixUtils static class MatrixUtils
{ {
public static Matrix4x4 ComputeTransform(Vector2 size, Vector2 anchorPoint, Vector3 centerPoint, public static Matrix ComputeTransform(Vector size, Vector anchorPoint, Vector3D centerPoint,
Matrix4x4 transformMatrix, Vector3 scale, float rotationAngle, Quaternion orientation, Vector3 offset) Matrix transformMatrix, Vector3D scale, float rotationAngle, Quaternion orientation, Vector3D offset)
{ {
// The math here follows the *observed* UWP behavior since there are no docs on how it's supposed to work // The math here follows the *observed* UWP behavior since there are no docs on how it's supposed to work
var anchor = size * anchorPoint;
var mat = Matrix4x4.CreateTranslation(-anchor.X, -anchor.Y, 0);
var center = new Vector3(centerPoint.X, centerPoint.Y, centerPoint.Z); var anchor = Vector.Multiply(size, anchorPoint);
var mat = Matrix.CreateTranslation(-anchor.X, -anchor.Y);
var center = new Vector3D(centerPoint.X, centerPoint.Y, centerPoint.Z);
if (!transformMatrix.IsIdentity) if (!transformMatrix.IsIdentity)
mat = transformMatrix * mat; mat = transformMatrix * mat;
if (scale != new Vector3(1, 1, 1)) if (scale != new Vector3D(1, 1, 1))
mat *= Matrix4x4.CreateScale(scale, center); mat *= ToMatrix(Matrix4x4.CreateScale(scale.ToVector3(), center.ToVector3()));
//TODO: RotationAxis support //TODO: RotationAxis support
if (rotationAngle != 0) if (rotationAngle != 0)
mat *= Matrix4x4.CreateRotationZ(rotationAngle, center); mat *= ToMatrix(Matrix4x4.CreateRotationZ(rotationAngle, center.ToVector3()));
if (orientation != Quaternion.Identity) if (orientation != Quaternion.Identity)
{ {
if (centerPoint != default) if (centerPoint != default)
{ {
mat *= Matrix4x4.CreateTranslation(-center) mat *= ToMatrix(Matrix4x4.CreateTranslation(-center.ToVector3())
* Matrix4x4.CreateFromQuaternion(orientation) * Matrix4x4.CreateFromQuaternion(orientation)
* Matrix4x4.CreateTranslation(center); * Matrix4x4.CreateTranslation(center.ToVector3()));
} }
else else
mat *= Matrix4x4.CreateFromQuaternion(orientation); mat *= ToMatrix(Matrix4x4.CreateFromQuaternion(orientation));
} }
if (offset != default) if (offset != default)
mat *= Matrix4x4.CreateTranslation(offset); {
if (offset.Z == 0)
mat *= Matrix.CreateTranslation(offset.X, offset.Y);
else
mat *= ToMatrix(Matrix4x4.CreateTranslation(offset.ToVector3()));
}
return mat; return mat;
} }

7
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs

@ -81,10 +81,9 @@ namespace Avalonia.Rendering.Composition.Server
// If we only have translation and scale, just scale the padding // If we only have translation and scale, just scale the padding
if (CombinedTransformMatrix is if (CombinedTransformMatrix is
{ {
M12: 0, M13: 0, M14: 0, M12: 0, M13: 0,
M21: 0, M23: 0, M24: 0, M21: 0, M23: 0,
M31: 0, M32: 0, M34: 0, M31: 0, M32: 0
M43: 0, M44: 1
}) })
padding = new Thickness(padding.Left * CombinedTransformMatrix.M11, padding = new Thickness(padding.Left * CombinedTransformMatrix.M11,
padding.Top * CombinedTransformMatrix.M22, padding.Top * CombinedTransformMatrix.M22,

20
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -51,7 +51,7 @@ namespace Avalonia.Rendering.Composition.Server
canvas.PushClip(AdornedVisual._combinedTransformedClipBounds); canvas.PushClip(AdornedVisual._combinedTransformedClipBounds);
} }
var transform = GlobalTransformMatrix; var transform = GlobalTransformMatrix;
canvas.PostTransform = MatrixUtils.ToMatrix(transform); canvas.PostTransform = transform;
canvas.Transform = Matrix.Identity; canvas.Transform = Matrix.Identity;
if (Effect != null) if (Effect != null)
@ -71,7 +71,7 @@ namespace Avalonia.Rendering.Composition.Server
RenderCore(canvas, currentTransformedClip); RenderCore(canvas, currentTransformedClip);
// Hack to force invalidation of SKMatrix // Hack to force invalidation of SKMatrix
canvas.PostTransform = MatrixUtils.ToMatrix(transform); canvas.PostTransform = transform;
canvas.Transform = Matrix.Identity; canvas.Transform = Matrix.Identity;
if (OpacityMaskBrush != null) if (OpacityMaskBrush != null)
@ -106,8 +106,8 @@ namespace Avalonia.Rendering.Composition.Server
return ref _readback2; return ref _readback2;
} }
public Matrix4x4 CombinedTransformMatrix { get; private set; } = Matrix4x4.Identity; public Matrix CombinedTransformMatrix { get; private set; } = Matrix.Identity;
public Matrix4x4 GlobalTransformMatrix { get; private set; } public Matrix GlobalTransformMatrix { get; private set; }
public record struct UpdateResult(Rect? Bounds, bool InvalidatedOld, bool InvalidatedNew) public record struct UpdateResult(Rect? Bounds, bool InvalidatedOld, bool InvalidatedNew)
{ {
@ -134,12 +134,12 @@ namespace Avalonia.Rendering.Composition.Server
{ {
CombinedTransformMatrix = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, CombinedTransformMatrix = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint,
// HACK: Ignore RenderTransform set by the adorner layer // HACK: Ignore RenderTransform set by the adorner layer
AdornedVisual != null ? Matrix4x4.Identity : TransformMatrix, AdornedVisual != null ? Matrix.Identity : TransformMatrix,
Scale, RotationAngle, Orientation, Offset); Scale, RotationAngle, Orientation, Offset);
_combinedTransformDirty = false; _combinedTransformDirty = false;
} }
var parentTransform = (AdornedVisual ?? Parent)?.GlobalTransformMatrix ?? Matrix4x4.Identity; var parentTransform = (AdornedVisual ?? Parent)?.GlobalTransformMatrix ?? Matrix.Identity;
var newTransform = CombinedTransformMatrix * parentTransform; var newTransform = CombinedTransformMatrix * parentTransform;
@ -148,7 +148,7 @@ namespace Avalonia.Rendering.Composition.Server
if (GlobalTransformMatrix != newTransform) if (GlobalTransformMatrix != newTransform)
{ {
_isBackface = Vector3.Transform( _isBackface = Vector3.Transform(
new Vector3(0, 0, float.PositiveInfinity), GlobalTransformMatrix).Z <= 0; new Vector3(0, 0, float.PositiveInfinity), MatrixUtils.ToMatrix4x4(GlobalTransformMatrix)).Z <= 0;
positionChanged = true; positionChanged = true;
} }
@ -179,14 +179,14 @@ namespace Avalonia.Rendering.Composition.Server
TransformedOwnContentBounds = default; TransformedOwnContentBounds = default;
else else
TransformedOwnContentBounds = TransformedOwnContentBounds =
ownBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); ownBounds.TransformToAABB(GlobalTransformMatrix);
} }
if (_clipSizeDirty || positionChanged) if (_clipSizeDirty || positionChanged)
{ {
_transformedClipBounds = ClipToBounds _transformedClipBounds = ClipToBounds
? new Rect(new Size(Size.X, Size.Y)) ? new Rect(new Size(Size.X, Size.Y))
.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)) .TransformToAABB(GlobalTransformMatrix)
: null; : null;
_clipSizeDirty = false; _clipSizeDirty = false;
@ -249,7 +249,7 @@ namespace Avalonia.Rendering.Composition.Server
/// </summary> /// </summary>
public struct ReadbackData public struct ReadbackData
{ {
public Matrix4x4 Matrix; public Matrix Matrix;
public ulong Revision; public ulong Revision;
public long TargetId; public long TargetId;
public bool Visible; public bool Visible;

2
src/Avalonia.Base/Rendering/Composition/Visual.cs

@ -33,7 +33,7 @@ namespace Avalonia.Rendering.Composition
} }
} }
internal Matrix4x4? TryGetServerGlobalTransform() internal Matrix? TryGetServerGlobalTransform()
{ {
if (Root == null) if (Root == null)
return null; return null;

1
src/Avalonia.Base/Rendering/RenderLoop.cs

@ -42,7 +42,6 @@ namespace Avalonia.Rendering
/// Initializes a new instance of the <see cref="RenderLoop"/> class. /// Initializes a new instance of the <see cref="RenderLoop"/> class.
/// </summary> /// </summary>
/// <param name="timer">The render timer.</param> /// <param name="timer">The render timer.</param>
/// <param name="dispatcher">The UI thread dispatcher.</param>
public RenderLoop(IRenderTimer timer) public RenderLoop(IRenderTimer timer)
{ {
_timer = timer; _timer = timer;

30
src/Avalonia.Base/Styling/Activators/NthChildActivator.cs

@ -1,6 +1,4 @@
#nullable enable using Avalonia.LogicalTree;
using System;
using Avalonia.LogicalTree;
namespace Avalonia.Styling.Activators namespace Avalonia.Styling.Activators
{ {
@ -14,7 +12,7 @@ namespace Avalonia.Styling.Activators
private readonly int _step; private readonly int _step;
private readonly int _offset; private readonly int _offset;
private readonly bool _reversed; private readonly bool _reversed;
private int _index = -1; private int? _index;
public NthChildActivator( public NthChildActivator(
ILogical control, ILogical control,
@ -30,7 +28,7 @@ namespace Avalonia.Styling.Activators
protected override bool EvaluateIsActive() protected override bool EvaluateIsActive()
{ {
var index = _index >= 0 ? _index : _provider.GetChildIndex(_control); var index = _index ?? _provider.GetChildIndex(_control);
return NthChildSelector.Evaluate(index, _provider, _step, _offset, _reversed).IsMatch; return NthChildSelector.Evaluate(index, _provider, _step, _offset, _reversed).IsMatch;
} }
@ -50,8 +48,7 @@ namespace Avalonia.Styling.Activators
// 1. Subscribed child index was changed // 1. Subscribed child index was changed
// 2. Child indexes were reset // 2. Child indexes were reset
// 3. We're a reversed (nth-last-child) selector and total count has changed // 3. We're a reversed (nth-last-child) selector and total count has changed
if ((e.Child == _control || e.Action == ChildIndexChangedAction.ChildIndexesReset) || switch (e.Action)
(_reversed && e.Action == ChildIndexChangedAction.TotalCountChanged))
{ {
// We're using the _index field to pass the index of the child to EvaluateIsActive // We're using the _index field to pass the index of the child to EvaluateIsActive
// *only* when the active state is re-evaluated via this event handler. The docs // *only* when the active state is re-evaluated via this event handler. The docs
@ -65,16 +62,17 @@ namespace Avalonia.Styling.Activators
// IChildIndexProvider.GetChildIndex. This is because this event can be fired during // IChildIndexProvider.GetChildIndex. This is because this event can be fired during
// the process of realizing an element of a virtualized list; in this case calling // the process of realizing an element of a virtualized list; in this case calling
// GetChildIndex may not return the correct index as the element isn't yet realized. // GetChildIndex may not return the correct index as the element isn't yet realized.
_index = e.Index; case ChildIndexChangedAction.ChildIndexChanged when e.Child == _control:
ReevaluateIsActive(); _index = e.Index;
_index = -1; ReevaluateIsActive();
_index = null;
break;
case ChildIndexChangedAction.ChildIndexesReset:
case ChildIndexChangedAction.TotalCountChanged when _reversed:
_index = null;
ReevaluateIsActive();
break;
} }
} }
private void TotalCountChanged(object? sender, EventArgs e)
{
if (_reversed)
ReevaluateIsActive();
}
} }
} }

11
src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs

@ -6,9 +6,14 @@ namespace Avalonia.Styling.Activators
internal abstract class StyleActivatorBase : IStyleActivator internal abstract class StyleActivatorBase : IStyleActivator
{ {
private IStyleActivatorSink? _sink; private IStyleActivatorSink? _sink;
private bool _value; private bool? _value;
public bool GetIsActive() => _value = EvaluateIsActive(); public bool GetIsActive()
{
var value = EvaluateIsActive();
_value ??= value;
return value;
}
public bool IsSubscribed => _sink is not null; public bool IsSubscribed => _sink is not null;
@ -63,7 +68,7 @@ namespace Avalonia.Styling.Activators
/// </returns> /// </returns>
protected bool ReevaluateIsActive() protected bool ReevaluateIsActive()
{ {
var value = EvaluateIsActive(); var value = GetIsActive();
if (value != _value) if (value != _value)
{ {

5
src/Avalonia.Base/Threading/Dispatcher.Invoke.cs

@ -581,11 +581,6 @@ public partial class Dispatcher
/// <param name="action"> /// <param name="action">
/// A Func&lt;Task&lt;TResult&gt;&gt; delegate to invoke through the dispatcher. /// A Func&lt;Task&lt;TResult&gt;&gt; delegate to invoke through the dispatcher.
/// </param> /// </param>
/// <param name="priority">
/// The priority that determines in what order the specified
/// callback is invoked relative to the other pending operations
/// in the Dispatcher.
/// </param>
/// <returns> /// <returns>
/// An task that completes after the task returned from callback finishes /// An task that completes after the task returned from callback finishes
/// </returns> /// </returns>

48
src/Avalonia.Base/Vector.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Numerics;
#if !BUILDTASK #if !BUILDTASK
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
#endif #endif
@ -353,5 +354,52 @@ namespace Avalonia
x = this._x; x = this._x;
y = this._y; y = this._y;
} }
internal Vector2 ToVector2() => new Vector2((float)X, (float)Y);
internal Vector(Vector2 v) : this(v.X, v.Y)
{
}
/// <summary>
/// Returns a vector whose elements are the absolute values of each of the specified vector's elements.
/// </summary>
/// <returns></returns>
public Vector Abs() => new(Math.Abs(X), Math.Abs(Y));
/// <summary>
/// Restricts a vector between a minimum and a maximum value.
/// </summary>
public static Vector Clamp(Vector value, Vector min, Vector max) =>
Min(Max(value, min), max);
/// <summary>
/// Returns a vector whose elements are the maximum of each of the pairs of elements in two specified vectors
/// </summary>
public static Vector Max(Vector left, Vector right) =>
new(Math.Max(left.X, right.X), Math.Max(left.Y, right.Y));
/// <summary>
/// Returns a vector whose elements are the minimum of each of the pairs of elements in two specified vectors
/// </summary>
public static Vector Min(Vector left, Vector right) =>
new(Math.Min(left.X, right.X), Math.Min(left.Y, right.Y));
/// <summary>
/// Computes the Euclidean distance between the two given points.
/// </summary>
public static double Distance(Vector value1, Vector value2) => Math.Sqrt(DistanceSquared(value1, value2));
/// <summary>
/// Returns the Euclidean distance squared between two specified points
/// </summary>
public static double DistanceSquared(Vector value1, Vector value2)
{
var difference = value1 - value2;
return Dot(difference, difference);
}
public static implicit operator Vector(Vector2 v) => new(v);
} }
} }

145
src/Avalonia.Base/Vector3D.cs

@ -0,0 +1,145 @@
using System;
using System.Globalization;
using System.Numerics;
using Avalonia.Rendering.Composition.Expressions;
using Avalonia.Utilities;
namespace Avalonia;
public readonly record struct Vector3D(double X, double Y, double Z)
{
/// <summary>
/// Parses a <see cref="Vector"/> string.
/// </summary>
/// <param name="s">The string.</param>
/// <returns>The <see cref="Vector3D"/>.</returns>
public static Vector3D Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Vector."))
{
return new Vector3D(
tokenizer.ReadDouble(),
tokenizer.ReadDouble(),
tokenizer.ReadDouble()
);
}
}
internal Vector3 ToVector3() => new Vector3((float)X, (float)Y, (float)Z);
internal Vector3D(Vector3 v) : this(v.X, v.Y, v.Z)
{
}
public static implicit operator Vector3D(Vector3 vector) => new(vector);
/// <summary>
/// Calculates the dot product of two vectors.
/// </summary>
public static double Dot(Vector3D vector1, Vector3D vector2) =>
(vector1.X * vector2.X)
+ (vector1.Y * vector2.Y)
+ (vector1.Z * vector2.Z);
/// <summary>
/// Adds the second to the first vector
/// </summary>
public static Vector3D Add(Vector3D left, Vector3D right) =>
new Vector3D(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
/// <summary>
/// Adds the second to the first vector
/// </summary>
public static Vector3D operator +(Vector3D left, Vector3D right) => Add(left, right);
/// <summary>
/// Subtracts the second from the first vector
/// </summary>
public static Vector3D Substract(Vector3D left, Vector3D right) =>
new Vector3D(left.X - right.X, left.Y - right.Y, left.Z - right.Z);
/// <summary>
/// Subtracts the second from the first vector
/// </summary>
public static Vector3D operator -(Vector3D left, Vector3D right) => Substract(left, right);
/// <summary>
/// Negates the vector
/// </summary>
public static Vector3D operator -(Vector3D v) => new(-v.X, -v.Y, -v.Z);
/// <summary>
/// Multiplies the first vector by the second.
/// </summary>
public static Vector3D Multiply(Vector3D left, Vector3D right) =>
new(left.X * right.X, left.Y * right.Y, left.Z * right.Z);
/// <summary>
/// Multiplies the vector by the given scalar.
/// </summary>
public static Vector3D Multiply(Vector3D left, double right) =>
new(left.X * right, left.Y * right, left.Z * right);
/// <summary>
/// Multiplies the vector by the given scalar.
/// </summary>
public static Vector3D operator *(Vector3D left, double right) => Multiply(left, right);
/// <summary>
/// Divides the first vector by the second.
/// </summary>
public static Vector3D Divide(Vector3D left, Vector3D right) =>
new(left.X / right.X, left.Y / right.Y, left.Z / right.Z);
/// <summary>
/// Divides the vector by the given scalar.
/// </summary>
public static Vector3D Divide(Vector3D left, double right) =>
new(left.X / right, left.Y / right, left.Z / right);
/// <summary>Returns a vector whose elements are the absolute values of each of the specified vector's elements.</summary>
public Vector3D Abs() => new(Math.Abs(X), Math.Abs(Y), Math.Abs(Z));
/// <summary>
/// Restricts a vector between a minimum and a maximum value.
/// </summary>
public static Vector3D Clamp(Vector3D value, Vector3D min, Vector3D max) =>
Min(Max(value, min), max);
/// <summary>
/// Returns a vector whose elements are the maximum of each of the pairs of elements in two specified vectors
/// </summary>
public static Vector3D Max(Vector3D left, Vector3D right) =>
new(Math.Max(left.X, right.X), Math.Max(left.Y, right.Y), Math.Max(left.Z, right.Z));
/// <summary>
/// Returns a vector whose elements are the minimum of each of the pairs of elements in two specified vectors
/// </summary>
public static Vector3D Min(Vector3D left, Vector3D right) =>
new(Math.Min(left.X, right.X), Math.Min(left.Y, right.Y), Math.Min(left.Z, right.Z));
/// <summary>
/// Length of the vector.
/// </summary>
public double Length => Math.Sqrt(Dot(this, this));
/// <summary>
/// Returns a normalized version of this vector.
/// </summary>
public static Vector3D Normalize(Vector3D value) => Divide(value, value.Length);
/// <summary>
/// Computes the squared Euclidean distance between the two given points.
/// </summary>
public static double DistanceSquared(Vector3D value1, Vector3D value2)
{
var difference = Vector3D.Substract(value1, value2);
return Dot(difference, difference);
}
/// <summary>
/// Computes the Euclidean distance between the two given points.
/// </summary>
public static double Distance(Vector3D value1, Vector3D value2) => Math.Sqrt(DistanceSquared(value1, value2));
}

15
src/Avalonia.Base/composition-schema.xml

@ -21,14 +21,14 @@
<Property Name="Clip" Type="Avalonia.Platform.IGeometryImpl?" Internal="true" /> <Property Name="Clip" Type="Avalonia.Platform.IGeometryImpl?" Internal="true" />
<Property Name="ClipToBounds" Type="bool" Animated="true" DefaultValue="true"/> <Property Name="ClipToBounds" Type="bool" Animated="true" DefaultValue="true"/>
<Property Name="Offset" Type="Vector3" Animated="true"/> <Property Name="Offset" Type="Vector3D" Animated="true"/>
<Property Name="Size" Type="Vector2" Animated="true"/> <Property Name="Size" Type="Vector" Animated="true"/>
<Property Name="AnchorPoint" Type="Vector2" Animated="true"/> <Property Name="AnchorPoint" Type="Vector" Animated="true"/>
<Property Name="CenterPoint" Type="Vector3" Animated="true"/> <Property Name="CenterPoint" Type="Vector3D" Animated="true"/>
<Property Name="RotationAngle" Type="float" Animated="true"/> <Property Name="RotationAngle" Type="float" Animated="true"/>
<Property Name="Orientation" Type="Quaternion" DefaultValue="Quaternion.Identity" Animated="true"/> <Property Name="Orientation" Type="Quaternion" DefaultValue="Quaternion.Identity" Animated="true"/>
<Property Name="Scale" Type="Vector3" DefaultValue="new Vector3(1, 1, 1)" Animated="true"/> <Property Name="Scale" Type="Vector3D" DefaultValue="new Avalonia.Vector3D(1, 1, 1)" Animated="true"/>
<Property Name="TransformMatrix" Type="Matrix4x4" DefaultValue="Matrix4x4.Identity" Animated="true"/> <Property Name="TransformMatrix" Type="Avalonia.Matrix" DefaultValue="Avalonia.Matrix.Identity" Animated="true" Internal="true"/>
<Property Name="AdornedVisual" Type="CompositionVisual?" Internal="true" /> <Property Name="AdornedVisual" Type="CompositionVisual?" Internal="true" />
<Property Name="AdornerIsClipped" Type="bool" Internal="true" /> <Property Name="AdornerIsClipped" Type="bool" Internal="true" />
<Property Name="OpacityMaskBrush" Type="Avalonia.Media.IImmutableBrush?" Internal="true" /> <Property Name="OpacityMaskBrush" Type="Avalonia.Media.IImmutableBrush?" Internal="true" />
@ -57,10 +57,13 @@
<Property Name="Size" Type="Size" /> <Property Name="Size" Type="Size" />
</Object> </Object>
<KeyFrameAnimation Name="Scalar" Type="float"/> <KeyFrameAnimation Name="Scalar" Type="float"/>
<KeyFrameAnimation Name="Double" Type="double"/>
<KeyFrameAnimation Name="Boolean" Type="bool"/> <KeyFrameAnimation Name="Boolean" Type="bool"/>
<KeyFrameAnimation Name="Color" Type="Avalonia.Media.Color"/> <KeyFrameAnimation Name="Color" Type="Avalonia.Media.Color"/>
<KeyFrameAnimation Type="Vector"/>
<KeyFrameAnimation Type="Vector2"/> <KeyFrameAnimation Type="Vector2"/>
<KeyFrameAnimation Type="Vector3"/> <KeyFrameAnimation Type="Vector3"/>
<KeyFrameAnimation Type="Vector3D"/>
<KeyFrameAnimation Type="Vector4"/> <KeyFrameAnimation Type="Vector4"/>
<KeyFrameAnimation Type="Quaternion"/> <KeyFrameAnimation Type="Quaternion"/>
<Object Name="CompositionSimpleTransform" Internal="true" ServerOnly="true" ServerBase="SimpleServerRenderResource"> <Object Name="CompositionSimpleTransform" Internal="true" ServerOnly="true" ServerBase="SimpleServerRenderResource">

14
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@ -38,7 +38,7 @@ namespace Avalonia.Controls
private ICellEditBinding _editBinding; private ICellEditBinding _editBinding;
private IBinding _clipboardContentBinding; private IBinding _clipboardContentBinding;
private ControlTheme _cellTheme; private ControlTheme _cellTheme;
private readonly Classes _cellStyleClasses = new Classes(); private Classes _cellStyleClasses;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="T:Avalonia.Controls.DataGridColumn" /> class. /// Initializes a new instance of the <see cref="T:Avalonia.Controls.DataGridColumn" /> class.
@ -393,17 +393,7 @@ namespace Avalonia.Controls
} }
} }
public Classes CellStyleClasses public Classes CellStyleClasses => _cellStyleClasses ??= new();
{
get => _cellStyleClasses;
set
{
if(_cellStyleClasses != value)
{
_cellStyleClasses.Replace(value);
}
}
}
/// <summary> /// <summary>
/// Backing field for CellTheme property. /// Backing field for CellTheme property.

7
src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml

@ -42,9 +42,10 @@
</ResourceDictionary> </ResourceDictionary>
</ResourceDictionary.ThemeDictionaries> </ResourceDictionary.ThemeDictionaries>
<x:Double x:Key="ListAccentLowOpacity">0.6</x:Double> <x:Double x:Key="ListAccentLowOpacity">0.6</x:Double>
<x:Double x:Key="ListAccentMediumOpacity">0.8</x:Double> <x:Double x:Key="ListAccentMediumOpacity">0.8</x:Double>
<x:Double x:Key="DataGridSortIconMinWidth">32</x:Double>
<StreamGeometry x:Key="DataGridSortIconDescendingPath">M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z</StreamGeometry> <StreamGeometry x:Key="DataGridSortIconDescendingPath">M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z</StreamGeometry>
<StreamGeometry x:Key="DataGridSortIconAscendingPath">M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z</StreamGeometry> <StreamGeometry x:Key="DataGridSortIconAscendingPath">M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z</StreamGeometry>
@ -174,7 +175,7 @@
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"> VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" MinWidth="32" /> <ColumnDefinition Width="Auto" MinWidth="{DynamicResource DataGridSortIconMinWidth}" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<ContentPresenter Content="{TemplateBinding Content}" <ContentPresenter Content="{TemplateBinding Content}"
@ -512,7 +513,7 @@
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"> CornerRadius="{TemplateBinding CornerRadius}">
<Grid ColumnDefinitions="Auto,*,Auto" <Grid ColumnDefinitions="Auto,*,Auto"
RowDefinitions="Auto,*,Auto,Auto" RowDefinitions="Auto,*,Auto,Auto"
ClipToBounds="True"> ClipToBounds="True">
<DataGridColumnHeader Name="PART_TopLeftCornerHeader" <DataGridColumnHeader Name="PART_TopLeftCornerHeader"

37
src/Avalonia.Controls/Control.cs

@ -316,7 +316,8 @@ namespace Avalonia.Controls
((ILogical)this).IsAttachedToLogicalTree) ((ILogical)this).IsAttachedToLogicalTree)
{ {
_isLoaded = true; _isLoaded = true;
OnLoaded();
OnLoaded(new RoutedEventArgs(LoadedEvent, this));
} }
} }
@ -333,28 +334,36 @@ namespace Avalonia.Controls
_loadedQueue.Remove(this); _loadedQueue.Remove(this);
_isLoaded = false; _isLoaded = false;
OnUnloaded();
OnUnloaded(new RoutedEventArgs(UnloadedEvent, this));
} }
} }
/// <summary> /// <summary>
/// Invoked just before the <see cref="Loaded"/> event. /// Invoked just before the <see cref="Loaded"/> event.
/// </summary> /// </summary>
protected virtual void OnLoaded() /// <param name="e">The event args.</param>
protected virtual void OnLoaded(RoutedEventArgs e)
{ {
var eventArgs = new RoutedEventArgs(LoadedEvent); RaiseEvent(e);
eventArgs.Source = null;
RaiseEvent(eventArgs);
} }
/// <summary> /// <summary>
/// Invoked just before the <see cref="Unloaded"/> event. /// Invoked just before the <see cref="Unloaded"/> event.
/// </summary> /// </summary>
protected virtual void OnUnloaded() /// <param name="e">The event args.</param>
protected virtual void OnUnloaded(RoutedEventArgs e)
{ {
var eventArgs = new RoutedEventArgs(UnloadedEvent); RaiseEvent(e);
eventArgs.Source = null; }
RaiseEvent(eventArgs);
/// <summary>
/// Invoked just before the <see cref="SizeChanged"/> event.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void OnSizeChanged(SizeChangedEventArgs e)
{
RaiseEvent(e);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -435,6 +444,10 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Returns a new, type-specific <see cref="AutomationPeer"/> implementation for the control.
/// </summary>
/// <returns>The type-specific <see cref="AutomationPeer"/> implementation.</returns>
protected virtual AutomationPeer OnCreateAutomationPeer() protected virtual AutomationPeer OnCreateAutomationPeer()
{ {
return new NoneAutomationPeer(this); return new NoneAutomationPeer(this);
@ -459,6 +472,7 @@ namespace Avalonia.Controls
return _automationPeer; return _automationPeer;
} }
/// <inheritdoc/>
protected override void OnPointerReleased(PointerReleasedEventArgs e) protected override void OnPointerReleased(PointerReleasedEventArgs e)
{ {
base.OnPointerReleased(e); base.OnPointerReleased(e);
@ -473,6 +487,7 @@ namespace Avalonia.Controls
} }
} }
/// <inheritdoc/>
protected override void OnKeyUp(KeyEventArgs e) protected override void OnKeyUp(KeyEventArgs e)
{ {
base.OnKeyUp(e); base.OnKeyUp(e);
@ -531,7 +546,7 @@ namespace Avalonia.Controls
previousSize: new Size(oldValue.Width, oldValue.Height), previousSize: new Size(oldValue.Width, oldValue.Height),
newSize: new Size(newValue.Width, newValue.Height)); newSize: new Size(newValue.Width, newValue.Height));
RaiseEvent(sizeChangedEventArgs); OnSizeChanged(sizeChangedEventArgs);
} }
} }
} }

86
src/Avalonia.Controls/ItemsControl.cs

@ -23,7 +23,7 @@ namespace Avalonia.Controls
/// Displays a collection of items. /// Displays a collection of items.
/// </summary> /// </summary>
[PseudoClasses(":empty", ":singleitem")] [PseudoClasses(":empty", ":singleitem")]
public class ItemsControl : TemplatedControl, IChildIndexProvider, IScrollSnapPointsInfo public class ItemsControl : TemplatedControl, IChildIndexProvider
{ {
/// <summary> /// <summary>
/// The default value for the <see cref="ItemsPanel"/> property. /// The default value for the <see cref="ItemsPanel"/> property.
@ -67,18 +67,6 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty = public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty =
AvaloniaProperty.Register<ItemsControl, IBinding?>(nameof(DisplayMemberBinding)); AvaloniaProperty.Register<ItemsControl, IBinding?>(nameof(DisplayMemberBinding));
/// <summary>
/// Defines the <see cref="AreHorizontalSnapPointsRegular"/> property.
/// </summary>
public static readonly StyledProperty<bool> AreHorizontalSnapPointsRegularProperty =
AvaloniaProperty.Register<ItemsControl, bool>(nameof(AreHorizontalSnapPointsRegular));
/// <summary>
/// Defines the <see cref="AreVerticalSnapPointsRegular"/> property.
/// </summary>
public static readonly StyledProperty<bool> AreVerticalSnapPointsRegularProperty =
AvaloniaProperty.Register<ItemsControl, bool>(nameof(AreVerticalSnapPointsRegular));
/// <summary> /// <summary>
/// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item. /// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item.
/// </summary> /// </summary>
@ -249,64 +237,6 @@ namespace Avalonia.Controls
/// </remarks> /// </remarks>
public event EventHandler<ContainerClearingEventArgs>? ContainerClearing; public event EventHandler<ContainerClearingEventArgs>? ContainerClearing;
/// <inheritdoc />
public event EventHandler<RoutedEventArgs> HorizontalSnapPointsChanged
{
add
{
if (_itemsPresenter != null)
{
_itemsPresenter.HorizontalSnapPointsChanged += value;
}
}
remove
{
if (_itemsPresenter != null)
{
_itemsPresenter.HorizontalSnapPointsChanged -= value;
}
}
}
/// <inheritdoc />
public event EventHandler<RoutedEventArgs> VerticalSnapPointsChanged
{
add
{
if (_itemsPresenter != null)
{
_itemsPresenter.VerticalSnapPointsChanged += value;
}
}
remove
{
if (_itemsPresenter != null)
{
_itemsPresenter.VerticalSnapPointsChanged -= value;
}
}
}
/// <summary>
/// Gets or sets whether the horizontal snap points for the <see cref="ItemsControl"/> are equidistant from each other.
/// </summary>
public bool AreHorizontalSnapPointsRegular
{
get => GetValue(AreHorizontalSnapPointsRegularProperty);
set => SetValue(AreHorizontalSnapPointsRegularProperty, value);
}
/// <summary>
/// Gets or sets whether the vertical snap points for the <see cref="ItemsControl"/> are equidistant from each other.
/// </summary>
public bool AreVerticalSnapPointsRegular
{
get => GetValue(AreVerticalSnapPointsRegularProperty);
set => SetValue(AreVerticalSnapPointsRegularProperty, value);
}
/// <summary> /// <summary>
/// Gets a default recycle key that can be used when an <see cref="ItemsControl"/> supports /// Gets a default recycle key that can be used when an <see cref="ItemsControl"/> supports
/// a single container type. /// a single container type.
@ -896,19 +826,5 @@ namespace Avalonia.Controls
count = ItemsView.Count; count = ItemsView.Count;
return true; return true;
} }
/// <inheritdoc />
public IReadOnlyList<double> GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment)
{
return _itemsPresenter?.GetIrregularSnapPoints(orientation, snapPointsAlignment) ?? new List<double>();
}
/// <inheritdoc />
public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset)
{
offset = 0;
return _itemsPresenter?.GetRegularSnapPoints(orientation, snapPointsAlignment, out offset) ?? 0;
}
} }
} }

15
src/Avalonia.Controls/LayoutTransformControl.cs

@ -215,7 +215,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Transformation matrix corresponding to _matrixTransform. /// Transformation matrix corresponding to _matrixTransform.
/// </summary> /// </summary>
private Matrix _transformation; private Matrix _transformation = Matrix.Identity;
private IDisposable? _transformChangedEvent; private IDisposable? _transformChangedEvent;
/// <summary> /// <summary>
@ -256,13 +256,16 @@ namespace Avalonia.Controls
/// </remarks> /// </remarks>
private void ApplyLayoutTransform() private void ApplyLayoutTransform()
{ {
if (LayoutTransform == null)
return;
// Get the transform matrix and apply it // Get the transform matrix and apply it
_transformation = RoundMatrix(LayoutTransform.Value, DecimalsAfterRound); var matrix = LayoutTransform is null ?
Matrix.Identity :
RoundMatrix(LayoutTransform.Value, DecimalsAfterRound);
if (_transformation == matrix)
return;
_matrixTransform.Matrix = _transformation; _transformation = matrix;
_matrixTransform.Matrix = matrix;
// New transform means re-layout is necessary // New transform means re-layout is necessary
InvalidateMeasure(); InvalidateMeasure();

8
src/Avalonia.Controls/MenuItem.cs

@ -394,6 +394,14 @@ namespace Avalonia.Controls
_isEmbeddedInMenu = parent?.FindLogicalAncestorOfType<IMenu>(true) != null; _isEmbeddedInMenu = parent?.FindLogicalAncestorOfType<IMenu>(true) != null;
} }
/// <inheritdoc />
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
TryUpdateCanExecute();
}
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{ {
// This will cause the hotkey manager to dispose the observer and the reference to this control // This will cause the hotkey manager to dispose the observer and the reference to this control

120
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@ -11,7 +11,7 @@ namespace Avalonia.Controls.Presenters
/// <summary> /// <summary>
/// Presents items inside an <see cref="Avalonia.Controls.ItemsControl"/>. /// Presents items inside an <see cref="Avalonia.Controls.ItemsControl"/>.
/// </summary> /// </summary>
public class ItemsPresenter : Control, ILogicalScrollable, IScrollSnapPointsInfo public class ItemsPresenter : Control, ILogicalScrollable
{ {
/// <summary> /// <summary>
/// Defines the <see cref="ItemsPanel"/> property. /// Defines the <see cref="ItemsPanel"/> property.
@ -21,37 +21,8 @@ namespace Avalonia.Controls.Presenters
private PanelContainerGenerator? _generator; private PanelContainerGenerator? _generator;
private ILogicalScrollable? _logicalScrollable; private ILogicalScrollable? _logicalScrollable;
private IScrollSnapPointsInfo? _scrollSnapPointsInfo;
private EventHandler? _scrollInvalidated; private EventHandler? _scrollInvalidated;
/// <summary>
/// Defines the <see cref="AreHorizontalSnapPointsRegular"/> property.
/// </summary>
public static readonly StyledProperty<bool> AreHorizontalSnapPointsRegularProperty =
AvaloniaProperty.Register<ItemsPresenter, bool>(nameof(AreHorizontalSnapPointsRegular));
/// <summary>
/// Defines the <see cref="AreVerticalSnapPointsRegular"/> property.
/// </summary>
public static readonly StyledProperty<bool> AreVerticalSnapPointsRegularProperty =
AvaloniaProperty.Register<ItemsPresenter, bool>(nameof(AreVerticalSnapPointsRegular));
/// <summary>
/// Defines the <see cref="HorizontalSnapPointsChanged"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> HorizontalSnapPointsChangedEvent =
RoutedEvent.Register<ItemsPresenter, RoutedEventArgs>(
nameof(HorizontalSnapPointsChanged),
RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="VerticalSnapPointsChanged"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> VerticalSnapPointsChangedEvent =
RoutedEvent.Register<ItemsPresenter, RoutedEventArgs>(
nameof(VerticalSnapPointsChanged),
RoutingStrategies.Bubble);
event EventHandler? ILogicalScrollable.ScrollInvalidated event EventHandler? ILogicalScrollable.ScrollInvalidated
{ {
add => _scrollInvalidated += value; add => _scrollInvalidated += value;
@ -107,47 +78,12 @@ namespace Avalonia.Controls.Presenters
} }
} }
/// <summary>
/// Occurs when the measurements for horizontal snap points change.
/// </summary>
public event EventHandler<RoutedEventArgs>? HorizontalSnapPointsChanged
{
add => AddHandler(HorizontalSnapPointsChangedEvent, value);
remove => RemoveHandler(HorizontalSnapPointsChangedEvent, value);
}
/// <summary>
/// Occurs when the measurements for vertical snap points change.
/// </summary>
public event EventHandler<RoutedEventArgs>? VerticalSnapPointsChanged
{
add => AddHandler(VerticalSnapPointsChangedEvent, value);
remove => RemoveHandler(VerticalSnapPointsChangedEvent, value);
}
bool ILogicalScrollable.IsLogicalScrollEnabled => _logicalScrollable?.IsLogicalScrollEnabled ?? false; bool ILogicalScrollable.IsLogicalScrollEnabled => _logicalScrollable?.IsLogicalScrollEnabled ?? false;
Size ILogicalScrollable.ScrollSize => _logicalScrollable?.ScrollSize ?? default; Size ILogicalScrollable.ScrollSize => _logicalScrollable?.ScrollSize ?? default;
Size ILogicalScrollable.PageScrollSize => _logicalScrollable?.PageScrollSize ?? default; Size ILogicalScrollable.PageScrollSize => _logicalScrollable?.PageScrollSize ?? default;
Size IScrollable.Extent => _logicalScrollable?.Extent ?? default; Size IScrollable.Extent => _logicalScrollable?.Extent ?? default;
Size IScrollable.Viewport => _logicalScrollable?.Viewport ?? default; Size IScrollable.Viewport => _logicalScrollable?.Viewport ?? default;
/// <summary>
/// Gets or sets whether the horizontal snap points for the <see cref="ItemsPresenter"/> are equidistant from each other.
/// </summary>
public bool AreHorizontalSnapPointsRegular
{
get { return GetValue(AreHorizontalSnapPointsRegularProperty); }
set { SetValue(AreHorizontalSnapPointsRegularProperty, value); }
}
/// <summary>
/// Gets or sets whether the vertical snap points for the <see cref="ItemsPresenter"/> are equidistant from each other.
/// </summary>
public bool AreVerticalSnapPointsRegular
{
get { return GetValue(AreVerticalSnapPointsRegularProperty); }
set { SetValue(AreVerticalSnapPointsRegularProperty, value); }
}
public override sealed void ApplyTemplate() public override sealed void ApplyTemplate()
{ {
@ -167,36 +103,14 @@ namespace Avalonia.Controls.Presenters
Panel.TemplatedParent = TemplatedParent; Panel.TemplatedParent = TemplatedParent;
Panel.IsItemsHost = true; Panel.IsItemsHost = true;
_scrollSnapPointsInfo = Panel as IScrollSnapPointsInfo;
LogicalChildren.Add(Panel); LogicalChildren.Add(Panel);
VisualChildren.Add(Panel); VisualChildren.Add(Panel);
if (_scrollSnapPointsInfo != null)
{
_scrollSnapPointsInfo.AreVerticalSnapPointsRegular = AreVerticalSnapPointsRegular;
_scrollSnapPointsInfo.AreHorizontalSnapPointsRegular = AreHorizontalSnapPointsRegular;
}
if (Panel is VirtualizingPanel v) if (Panel is VirtualizingPanel v)
v.Attach(ItemsControl); v.Attach(ItemsControl);
else else
CreateSimplePanelGenerator(); CreateSimplePanelGenerator();
if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo)
{
scrollSnapPointsInfo.VerticalSnapPointsChanged += (s, e) =>
{
e.RoutedEvent = VerticalSnapPointsChangedEvent;
RaiseEvent(e);
};
scrollSnapPointsInfo.HorizontalSnapPointsChanged += (s, e) =>
{
e.RoutedEvent = HorizontalSnapPointsChangedEvent;
RaiseEvent(e);
};
}
_logicalScrollable = Panel as ILogicalScrollable; _logicalScrollable = Panel as ILogicalScrollable;
if (_logicalScrollable is not null) if (_logicalScrollable is not null)
@ -240,16 +154,6 @@ namespace Avalonia.Controls.Presenters
ResetState(); ResetState();
InvalidateMeasure(); InvalidateMeasure();
} }
else if(change.Property == AreHorizontalSnapPointsRegularProperty)
{
if (_scrollSnapPointsInfo != null)
_scrollSnapPointsInfo.AreHorizontalSnapPointsRegular = AreHorizontalSnapPointsRegular;
}
else if (change.Property == AreVerticalSnapPointsRegularProperty)
{
if (_scrollSnapPointsInfo != null)
_scrollSnapPointsInfo.AreVerticalSnapPointsRegular = AreVerticalSnapPointsRegular;
}
} }
internal void Refresh() internal void Refresh()
@ -303,27 +207,5 @@ namespace Avalonia.Controls.Presenters
} }
private void OnLogicalScrollInvalidated(object? sender, EventArgs e) => _scrollInvalidated?.Invoke(this, e); private void OnLogicalScrollInvalidated(object? sender, EventArgs e) => _scrollInvalidated?.Invoke(this, e);
public IReadOnlyList<double> GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment)
{
if(Panel is IScrollSnapPointsInfo scrollSnapPointsInfo)
{
return scrollSnapPointsInfo.GetIrregularSnapPoints(orientation, snapPointsAlignment);
}
return new List<double>();
}
public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset)
{
if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo)
{
return scrollSnapPointsInfo.GetRegularSnapPoints(orientation, snapPointsAlignment, out offset);
}
offset = 0;
return 0;
}
} }
} }

83
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -7,6 +7,7 @@ using Avalonia.Input.GestureRecognizers;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using System.Linq; using System.Linq;
using Avalonia.Layout;
namespace Avalonia.Controls.Presenters namespace Avalonia.Controls.Presenters
{ {
@ -99,6 +100,8 @@ namespace Avalonia.Controls.Presenters
private double _horizontalSnapPointOffset; private double _horizontalSnapPointOffset;
private CompositeDisposable? _ownerSubscriptions; private CompositeDisposable? _ownerSubscriptions;
private ScrollViewer? _owner; private ScrollViewer? _owner;
private IScrollSnapPointsInfo? _scrollSnapPointsInfo;
private bool _isSnapPointsUpdated;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="ScrollContentPresenter"/> class. /// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
@ -377,6 +380,13 @@ namespace Avalonia.Controls.Presenters
CanVerticallyScroll ? double.PositiveInfinity : availableSize.Height); CanVerticallyScroll ? double.PositiveInfinity : availableSize.Height);
Child.Measure(constraint); Child.Measure(constraint);
if (!_isSnapPointsUpdated)
{
_isSnapPointsUpdated = true;
UpdateSnapPoints();
}
return Child.DesiredSize.Constrain(availableSize); return Child.DesiredSize.Constrain(availableSize);
} }
@ -463,7 +473,15 @@ namespace Avalonia.Controls.Presenters
} }
Viewport = finalSize; Viewport = finalSize;
Extent = Child!.Bounds.Size.Inflate(Child.Margin);
var childMargin = Child!.Margin;
if (Child.UseLayoutRounding)
{
var scale = LayoutHelper.GetLayoutScale(Child);
childMargin = LayoutHelper.RoundLayoutThickness(childMargin, scale, scale);
}
Extent = Child!.Bounds.Size.Inflate(childMargin);
_isAnchorElementDirty = true; _isAnchorElementDirty = true;
return finalSize; return finalSize;
@ -570,7 +588,12 @@ namespace Avalonia.Controls.Presenters
private void OnScrollGestureInertiaStartingEnded(object? sender, ScrollGestureInertiaStartingEventArgs e) private void OnScrollGestureInertiaStartingEnded(object? sender, ScrollGestureInertiaStartingEventArgs e)
{ {
if (Content is not IScrollSnapPointsInfo) var scrollable = Content;
if (Content is ItemsControl itemsControl)
scrollable = itemsControl.Presenter?.Panel;
if (scrollable is not IScrollSnapPointsInfo)
return; return;
if (_scrollGestureSnapPoints == null) if (_scrollGestureSnapPoints == null)
@ -676,22 +699,6 @@ namespace Avalonia.Controls.Presenters
_owner?.SetCurrentValue(OffsetProperty, change.GetNewValue<Vector>()); _owner?.SetCurrentValue(OffsetProperty, change.GetNewValue<Vector>());
} }
else if (change.Property == ContentProperty)
{
if (change.OldValue is IScrollSnapPointsInfo oldSnapPointsInfo)
{
oldSnapPointsInfo.VerticalSnapPointsChanged -= ScrollSnapPointsInfoSnapPointsChanged;
oldSnapPointsInfo.HorizontalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged;
}
if (Content is IScrollSnapPointsInfo scrollSnapPointsInfo)
{
scrollSnapPointsInfo.VerticalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged;
scrollSnapPointsInfo.HorizontalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged;
}
UpdateSnapPoints();
}
else if (change.Property == ChildProperty) else if (change.Property == ChildProperty)
{ {
ChildChanged(change); ChildChanged(change);
@ -875,7 +882,9 @@ namespace Avalonia.Controls.Presenters
private void UpdateSnapPoints() private void UpdateSnapPoints()
{ {
if (Content is IScrollSnapPointsInfo scrollSnapPointsInfo) var scrollable = GetScrollSnapPointsInfo(Content);
if (scrollable is IScrollSnapPointsInfo scrollSnapPointsInfo)
{ {
_areVerticalSnapPointsRegular = scrollSnapPointsInfo.AreVerticalSnapPointsRegular; _areVerticalSnapPointsRegular = scrollSnapPointsInfo.AreVerticalSnapPointsRegular;
_areHorizontalSnapPointsRegular = scrollSnapPointsInfo.AreHorizontalSnapPointsRegular; _areHorizontalSnapPointsRegular = scrollSnapPointsInfo.AreHorizontalSnapPointsRegular;
@ -910,7 +919,9 @@ namespace Avalonia.Controls.Presenters
private Vector SnapOffset(Vector offset) private Vector SnapOffset(Vector offset)
{ {
if(Content is not IScrollSnapPointsInfo) var scrollable = GetScrollSnapPointsInfo(Content);
if(scrollable is null)
return offset; return offset;
var diff = GetAlignedDiff(); var diff = GetAlignedDiff();
@ -1012,5 +1023,37 @@ namespace Avalonia.Controls.Presenters
} }
return snapPoints[Math.Min(point, snapPoints.Count - 1)]; return snapPoints[Math.Min(point, snapPoints.Count - 1)];
} }
private IScrollSnapPointsInfo? GetScrollSnapPointsInfo(object? content)
{
var scrollable = content;
if (Content is ItemsControl itemsControl)
scrollable = itemsControl.Presenter?.Panel;
if (Content is ItemsPresenter itemsPresenter)
scrollable = itemsPresenter.Panel;
var snapPointsInfo = scrollable as IScrollSnapPointsInfo;
if(snapPointsInfo != _scrollSnapPointsInfo)
{
if(_scrollSnapPointsInfo != null)
{
_scrollSnapPointsInfo.VerticalSnapPointsChanged -= ScrollSnapPointsInfoSnapPointsChanged;
_scrollSnapPointsInfo.HorizontalSnapPointsChanged -= ScrollSnapPointsInfoSnapPointsChanged;
}
_scrollSnapPointsInfo = snapPointsInfo;
if(_scrollSnapPointsInfo != null)
{
_scrollSnapPointsInfo.VerticalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged;
_scrollSnapPointsInfo.HorizontalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged;
}
}
return snapPointsInfo;
}
} }
} }

36
src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs

@ -223,7 +223,7 @@ namespace Avalonia.Controls
var visualizerVisual = ElementComposition.GetElementVisual(this); var visualizerVisual = ElementComposition.GetElementVisual(this);
if (visual != null && contentVisual != null && visualizerVisual != null) if (visual != null && contentVisual != null && visualizerVisual != null)
{ {
contentVisual.CenterPoint = new Vector3((float)(_content.Bounds.Width / 2), (float)(_content.Bounds.Height / 2), 0); contentVisual.CenterPoint = new Vector3D((_content.Bounds.Width / 2), (_content.Bounds.Height / 2), 0);
switch (RefreshVisualizerState) switch (RefreshVisualizerState)
{ {
case RefreshVisualizerState.Idle: case RefreshVisualizerState.Idle:
@ -236,43 +236,43 @@ namespace Avalonia.Controls
contentVisual.Opacity = MinimumIndicatorOpacity; contentVisual.Opacity = MinimumIndicatorOpacity;
contentVisual.RotationAngle = _startingRotationAngle; contentVisual.RotationAngle = _startingRotationAngle;
visualizerVisual.Offset = IsPullDirectionVertical ? visualizerVisual.Offset = IsPullDirectionVertical ?
new Vector3(visualizerVisual.Offset.X, 0, 0) : new Vector3D(visualizerVisual.Offset.X, 0, 0) :
new Vector3(0, visualizerVisual.Offset.Y, 0); new Vector3D(0, visualizerVisual.Offset.Y, 0);
_content.InvalidateMeasure(); _content.InvalidateMeasure();
break; break;
case RefreshVisualizerState.Interacting: case RefreshVisualizerState.Interacting:
_played = false; _played = false;
contentVisual.Opacity = MinimumIndicatorOpacity; contentVisual.Opacity = MinimumIndicatorOpacity;
contentVisual.RotationAngle = (float)(_startingRotationAngle + _interactionRatio * 2 * Math.PI); contentVisual.RotationAngle = (float)(_startingRotationAngle + _interactionRatio * 2 * Math.PI);
Vector3 offset = default; Vector3D offset = default;
if (IsPullDirectionVertical) if (IsPullDirectionVertical)
{ {
offset = new Vector3(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); offset = new Vector3D(0, (_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0);
} }
else else
{ {
offset = new Vector3((float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0); offset = new Vector3D((_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0);
} }
visual.Offset = offset; visual.Offset = offset;
visualizerVisual.Offset = IsPullDirectionVertical ? visualizerVisual.Offset = IsPullDirectionVertical ?
new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : new Vector3D(visualizerVisual.Offset.X, offset.Y, 0) :
new Vector3(offset.X, visualizerVisual.Offset.Y, 0); new Vector3D(offset.X, visualizerVisual.Offset.Y, 0);
break; break;
case RefreshVisualizerState.Pending: case RefreshVisualizerState.Pending:
contentVisual.Opacity = 1; contentVisual.Opacity = 1;
contentVisual.RotationAngle = _startingRotationAngle + (float)(2 * Math.PI); contentVisual.RotationAngle = _startingRotationAngle + (float)(2 * Math.PI);
if (IsPullDirectionVertical) if (IsPullDirectionVertical)
{ {
offset = new Vector3(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); offset = new Vector3D(0, (_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0);
} }
else else
{ {
offset = new Vector3((float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0); offset = new Vector3D((_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0);
} }
visual.Offset = offset; visual.Offset = offset;
visualizerVisual.Offset = IsPullDirectionVertical ? visualizerVisual.Offset = IsPullDirectionVertical ?
new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : new Vector3D(visualizerVisual.Offset.X, offset.Y, 0) :
new Vector3(offset.X, visualizerVisual.Offset.Y, 0); new Vector3D(offset.X, visualizerVisual.Offset.Y, 0);
if (!_played) if (!_played)
{ {
@ -301,18 +301,18 @@ namespace Avalonia.Controls
* (IsPullDirectionFar ? -1f : 1f); * (IsPullDirectionFar ? -1f : 1f);
if (IsPullDirectionVertical) if (IsPullDirectionVertical)
{ {
offset = new Vector3(0, (float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); offset = new Vector3D(0, (_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0);
} }
else else
{ {
offset = new Vector3((float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0); offset = new Vector3D((_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0);
} }
visual.Offset = offset; visual.Offset = offset;
contentVisual.Offset += IsPullDirectionVertical ? new Vector3(0, (float)(translationRatio * root.Bounds.Height), 0) : contentVisual.Offset += IsPullDirectionVertical ? new Vector3D(0, (translationRatio * root.Bounds.Height), 0) :
new Vector3((float)(translationRatio * root.Bounds.Width), 0, 0); new Vector3D((translationRatio * root.Bounds.Width), 0, 0);
visualizerVisual.Offset = IsPullDirectionVertical ? visualizerVisual.Offset = IsPullDirectionVertical ?
new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : new Vector3D(visualizerVisual.Offset.X, offset.Y, 0) :
new Vector3(offset.X, visualizerVisual.Offset.Y, 0); new Vector3D(offset.X, visualizerVisual.Offset.Y, 0);
break; break;
case RefreshVisualizerState.Peeking: case RefreshVisualizerState.Peeking:
contentVisual.Opacity = 1; contentVisual.Opacity = 1;

6
src/Avalonia.Controls/ToggleSwitch.cs

@ -3,6 +3,7 @@ using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Interactivity;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -200,9 +201,10 @@ namespace Avalonia.Controls
} }
} }
protected override void OnLoaded() /// <inheritdoc/>
protected override void OnLoaded(RoutedEventArgs e)
{ {
base.OnLoaded(); base.OnLoaded(e);
UpdateKnobTransitions(); UpdateKnobTransitions();
} }

156
src/Avalonia.Controls/Utils/VirtualizingSnapPointsList.cs

@ -0,0 +1,156 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
namespace Avalonia.Controls.Utils
{
internal class VirtualizingSnapPointsList : IReadOnlyList<double>
{
private const int ExtraCount = 2;
private readonly RealizedStackElements _realizedElements;
private readonly Orientation _orientation;
private readonly Orientation _parentOrientation;
private readonly SnapPointsAlignment _snapPointsAlignment;
private readonly double _size;
private readonly int _start = -1;
private readonly int _end;
public VirtualizingSnapPointsList(RealizedStackElements realizedElements, int count, Orientation orientation, Orientation parentOrientation, SnapPointsAlignment snapPointsAlignment, double size)
{
_realizedElements = realizedElements;
_orientation = orientation;
_parentOrientation = parentOrientation;
_snapPointsAlignment = snapPointsAlignment;
_size = size;
if (parentOrientation == orientation)
{
_start = Math.Max(0, _realizedElements.FirstIndex - ExtraCount);
_end = Math.Min(count - 1, _realizedElements.LastIndex + ExtraCount);
}
}
public double this[int index]
{
get
{
if(index < 0 || index >= Count)
throw new ArgumentOutOfRangeException("index");
index += _start;
double snapPoint = 0;
var averageElementSize = _size;
Control? container;
switch (_orientation)
{
case Orientation.Horizontal:
container = _realizedElements.GetElement(index);
if (container != null)
{
switch (_snapPointsAlignment)
{
case SnapPointsAlignment.Near:
snapPoint = container.Bounds.Left;
break;
case SnapPointsAlignment.Center:
snapPoint = container.Bounds.Center.X;
break;
case SnapPointsAlignment.Far:
snapPoint = container.Bounds.Right;
break;
}
}
else
{
var ind = index;
if (index > _realizedElements.LastIndex)
{
ind -= _realizedElements.LastIndex + 1;
}
snapPoint = ind * averageElementSize;
switch (_snapPointsAlignment)
{
case SnapPointsAlignment.Center:
snapPoint += averageElementSize / 2;
break;
case SnapPointsAlignment.Far:
snapPoint += averageElementSize;
break;
}
if (index > _realizedElements.LastIndex)
{
var lastElement = _realizedElements.GetElement(_realizedElements.LastIndex);
if (lastElement != null)
{
snapPoint += lastElement.Bounds.Right;
}
}
}
break;
case Orientation.Vertical:
container = _realizedElements.GetElement(index);
if (container != null)
{
switch (_snapPointsAlignment)
{
case SnapPointsAlignment.Near:
snapPoint = container.Bounds.Top;
break;
case SnapPointsAlignment.Center:
snapPoint = container.Bounds.Center.Y;
break;
case SnapPointsAlignment.Far:
snapPoint = container.Bounds.Bottom;
break;
}
}
else
{
var ind = index;
if(index > _realizedElements.LastIndex)
{
ind -= _realizedElements.LastIndex + 1;
}
snapPoint = ind * averageElementSize;
switch (_snapPointsAlignment)
{
case SnapPointsAlignment.Center:
snapPoint += averageElementSize / 2;
break;
case SnapPointsAlignment.Far:
snapPoint += averageElementSize;
break;
}
if (index > _realizedElements.LastIndex)
{
var lastElement = _realizedElements.GetElement(_realizedElements.LastIndex);
if (lastElement != null)
{
snapPoint += lastElement.Bounds.Bottom;
}
}
}
break;
}
return snapPoint;
}
}
public int Count => _parentOrientation != _orientation ? 0 : _end - _start + 1;
public IEnumerator<double> GetEnumerator()
{
for (var i = 0; i < Count; i++)
yield return this[i];
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

103
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -776,107 +776,10 @@ namespace Avalonia.Controls
/// <inheritdoc/> /// <inheritdoc/>
public IReadOnlyList<double> GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment) public IReadOnlyList<double> GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment)
{ {
var snapPoints = new List<double>(); if(_realizedElements == null)
return new List<double>();
switch (orientation) return new VirtualizingSnapPointsList(_realizedElements, ItemsControl?.ItemsSource?.Count() ?? 0, orientation, Orientation, snapPointsAlignment, EstimateElementSizeU());
{
case Orientation.Horizontal:
if (AreHorizontalSnapPointsRegular)
throw new InvalidOperationException();
if (Orientation == Orientation.Horizontal)
{
var averageElementSize = EstimateElementSizeU();
double snapPoint = 0;
for (var i = 0; i < Items.Count; i++)
{
var container = ContainerFromIndex(i);
if (container != null)
{
switch (snapPointsAlignment)
{
case SnapPointsAlignment.Near:
snapPoint = container.Bounds.Left;
break;
case SnapPointsAlignment.Center:
snapPoint = container.Bounds.Center.X;
break;
case SnapPointsAlignment.Far:
snapPoint = container.Bounds.Right;
break;
}
}
else
{
if (snapPoint == 0)
{
switch (snapPointsAlignment)
{
case SnapPointsAlignment.Center:
snapPoint = averageElementSize / 2;
break;
case SnapPointsAlignment.Far:
snapPoint = averageElementSize;
break;
}
}
else
snapPoint += averageElementSize;
}
snapPoints.Add(snapPoint);
}
}
break;
case Orientation.Vertical:
if (AreVerticalSnapPointsRegular)
throw new InvalidOperationException();
if (Orientation == Orientation.Vertical)
{
var averageElementSize = EstimateElementSizeU();
double snapPoint = 0;
for (var i = 0; i < Items.Count; i++)
{
var container = ContainerFromIndex(i);
if (container != null)
{
switch (snapPointsAlignment)
{
case SnapPointsAlignment.Near:
snapPoint = container.Bounds.Top;
break;
case SnapPointsAlignment.Center:
snapPoint = container.Bounds.Center.Y;
break;
case SnapPointsAlignment.Far:
snapPoint = container.Bounds.Bottom;
break;
}
}
else
{
if (snapPoint == 0)
{
switch (snapPointsAlignment)
{
case SnapPointsAlignment.Center:
snapPoint = averageElementSize / 2;
break;
case SnapPointsAlignment.Far:
snapPoint = averageElementSize;
break;
}
}
else
snapPoint += averageElementSize;
}
snapPoints.Add(snapPoint);
}
}
break;
}
return snapPoints;
} }
/// <inheritdoc/> /// <inheritdoc/>

104
src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs

@ -57,42 +57,11 @@ namespace Avalonia.Diagnostics.Views
if (Property?.PropertyType is not { } propertyType) if (Property?.PropertyType is not { } propertyType)
return null; return null;
TControl CreateControl<TControl>(AvaloniaProperty valueProperty,
IValueConverter? converter = null,
Action<TControl>? init = null,
AvaloniaProperty? readonlyProperty = null)
where TControl : Control, new()
{
var control = new TControl();
var bindingMode = Property.IsReadonly ? BindingMode.OneWay : BindingMode.TwoWay;
init?.Invoke(control);
control.Bind(valueProperty,
new Binding(nameof(Property.Value), bindingMode)
{
Source = Property,
Converter = converter ?? new ValueConverter(),
ConverterParameter = propertyType
}).DisposeWith(_cleanup);
if (readonlyProperty != null)
{
control[readonlyProperty] = Property.IsReadonly;
}
else
{
control.IsEnabled = !Property.IsReadonly;
}
return control;
}
if (propertyType == typeof(bool)) if (propertyType == typeof(bool))
return CreateControl<CheckBox>(ToggleButton.IsCheckedProperty); return CreateControl<CheckBox>(ToggleButton.IsCheckedProperty);
//TODO: Infinity, NaN not working with NumericUpDown //TODO: Infinity, NaN not working with NumericUpDown
if (propertyType.IsPrimitive && propertyType != typeof(float) && propertyType != typeof(double)) if (IsValidNumeric(propertyType))
return CreateControl<NumericUpDown>( return CreateControl<NumericUpDown>(
NumericUpDown.ValueProperty, NumericUpDown.ValueProperty,
new ValueToDecimalConverter(), new ValueToDecimalConverter(),
@ -139,7 +108,8 @@ namespace Avalonia.Diagnostics.Views
ColorView.ColorProperty, ColorView.ColorProperty,
new Binding(nameof(Property.Value), BindingMode.TwoWay) new Binding(nameof(Property.Value), BindingMode.TwoWay)
{ {
Source = Property, Converter = Color2Brush Source = Property,
Converter = Color2Brush
}) })
.DisposeWith(_cleanup); .DisposeWith(_cleanup);
@ -261,6 +231,74 @@ namespace Avalonia.Diagnostics.Views
} }
return tb; return tb;
TControl CreateControl<TControl>(AvaloniaProperty valueProperty,
IValueConverter? converter = null,
Action<TControl>? init = null,
AvaloniaProperty? readonlyProperty = null)
where TControl : Control, new()
{
var control = new TControl();
var bindingMode = Property.IsReadonly ? BindingMode.OneWay : BindingMode.TwoWay;
init?.Invoke(control);
control.Bind(valueProperty,
new Binding(nameof(Property.Value), bindingMode)
{
Source = Property,
Converter = converter ?? new ValueConverter(),
ConverterParameter = propertyType
}).DisposeWith(_cleanup);
if (readonlyProperty != null)
{
control[readonlyProperty] = Property.IsReadonly;
}
else
{
control.IsEnabled = !Property.IsReadonly;
}
return control;
}
static bool IsValidNumeric(Type? type)
{
if (type == null || type.IsEnum == true)
{
return false;
}
var typeCode = Type.GetTypeCode(type);
if (typeCode == TypeCode.Object)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
typeCode = Type.GetTypeCode(Nullable.GetUnderlyingType(type));
}
else
{
return false;
}
}
switch (typeCode)
{
case TypeCode.Byte:
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.Int64:
case TypeCode.SByte:
case TypeCode.Single:
case TypeCode.UInt16:
case TypeCode.UInt32:
case TypeCode.UInt64:
return true;
default:
return false;
}
}
} }
//HACK: ValueConverter that skips first target update //HACK: ValueConverter that skips first target update

145
src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml

@ -6,106 +6,71 @@
MinWidth="430" MinWidth="430"
MinHeight="475" MinHeight="475"
Title="About Avalonia" Title="About Avalonia"
Background="Purple"
FontFamily="/Assets/Roboto-Light.ttf#Roboto" FontFamily="/Assets/Roboto-Light.ttf#Roboto"
x:Class="Avalonia.Dialogs.AboutAvaloniaDialog" x:Class="Avalonia.Dialogs.AboutAvaloniaDialog"
x:DataType="dialogs:AboutAvaloniaDialog"> x:DataType="dialogs:AboutAvaloniaDialog">
<Window.Styles> <Window.Styles>
<Style> <Style>
<Style.Resources> <Style.Resources>
<DrawingGroup x:Key="AvaloniaLogo"> <DrawingGroup x:Key="AvaloniaLogo">
<GeometryDrawing Geometry="m 150.66581 0.66454769 c -54.77764 0 -101.0652 38.86360031 -112.62109 90.33008031 a 26.1 26.1 0 0 1 18.92187 25.070312 26.1 26.1 0 0 1 -18.91992 25.08202 c 11.56024 51.46073 57.8456 90.31837 112.61914 90.31837 63.37832 0 115.40039 -52.02207 115.40039 -115.40039 0 -63.378322 -52.02207 -115.40039231 -115.40039 -115.40039231 z m 0 60.00000031 c 30.95192 0 55.40039 24.44847 55.40039 55.400392 0 30.9519 -24.44847 55.40039 -55.40039 55.40039 -30.95191 0 -55.40039 -24.44848 -55.40039 -55.40039 0 -30.951922 24.44848 -55.400392 55.40039 -55.400392 z"> <GeometryDrawing Brush="#FF8B44AC" Geometry="M215 246.5h1.5a30 30 0 0 0 29.8-26.5l.2-100a123.2 123.2 0 1 0-125.4 126.5H215Z"/>
<GeometryDrawing.Brush> <GeometryDrawing Brush="#FFF9F9FB" Geometry="M123.7 42a81.3 81.3 0 0 0-79.3 63.5 18.4 18.4 0 0 1 0 35.4 81.3 81.3 0 0 0 118.2 53.6v9.4H205v-80.7A81.2 81.2 0 0 0 123.7 42Zm-39 81.2a39 39 0 1 1 77.9 0 39 39 0 0 1-77.8 0Z"/>
<LinearGradientBrush StartPoint="272,411" EndPoint="435,248"> <GeometryDrawing Brush="#FFF9F9FB" Geometry="M52 123.2a13 13 0 1 1-26 0 13 13 0 0 1 26 0Z"/>
<LinearGradientBrush.GradientStops>
<GradientStop Color="#B0B0B0" Offset="0" />
<GradientStop Color="#FFFFFF" Offset="0.6784" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
<GeometryDrawing Brush="#B0B0B0">
<GeometryDrawing.Geometry>
<EllipseGeometry Rect="9.6,95.8,40.6,40.6" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="White">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="206.06355, 114.56503,60,116.2" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup> </DrawingGroup>
</Style.Resources> </Style.Resources>
</Style> </Style>
<Style Selector="Rectangle.Abstract">
<Setter Property="Fill" Value="White" />
<Setter Property="Width" Value="750" />
<Setter Property="Height" Value="700" />
</Style>
<Style Selector="Button.Hyperlink">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Margin" Value="-5"/>
<Setter Property="Foreground" Value="#419df2" />
<Setter Property="Command" Value="{Binding OpenBrowser}" />
<Setter Property="Content" Value="{Binding $self.CommandParameter}" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="Cursor" Value="Hand" />
</Style>
</Window.Styles> </Window.Styles>
<Grid Background="#4A255D">
<Canvas>
<Rectangle Classes="Abstract" Canvas.Top="90" Opacity="0.132"> <Grid RowDefinitions="Auto, *" Margin="35,35,35,15">
<Rectangle.RenderTransform>
<RotateTransform Angle="-2" /> <!-- Header -->
</Rectangle.RenderTransform> <Grid RowDefinitions="*, Auto, Auto">
</Rectangle>
<Rectangle Classes="Abstract" Canvas.Top="95" Opacity="0.3"> <Image Width="130"
<Rectangle.RenderTransform> Margin="0, 0,0, 20"
<RotateTransform Angle="-4" /> Grid.Row="0">
</Rectangle.RenderTransform> <Image.Source>
</Rectangle> <DrawingImage Drawing="{DynamicResource AvaloniaLogo}"/>
<Rectangle Classes="Abstract" Canvas.Top="100" Opacity="0.3"> </Image.Source>
<Rectangle.RenderTransform> </Image>
<RotateTransform Angle="-8" />
</Rectangle.RenderTransform> <TextBlock Text="Avalonia"
</Rectangle> FontWeight="Bold"
<Rectangle Classes="Abstract" Canvas.Top="105" Opacity="0.7"> HorizontalAlignment="Center"
<Rectangle.RenderTransform> FontSize="32"
<RotateTransform Angle="-12" /> Grid.Row="1"/>
</Rectangle.RenderTransform>
</Rectangle> <TextBlock Text="{Binding Version}"
</Canvas> FontWeight="Regular"
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="18"> HorizontalAlignment="Center"
<Border Height="70" Width="70"> FontSize="14"
<Image> Grid.Row="2"/>
<Image.Source> </Grid>
<DrawingImage Drawing="{DynamicResource AvaloniaLogo}"/>
</Image.Source> <Grid RowDefinitions="Auto, Auto, *, Auto" Grid.Row="1" Margin="0, 30,0,0">
</Image>
</Border> <TextBlock Grid.Row="0"
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Margin="10,-10,0,0"> Text="This product is built with Avalonia, a cross-platform UI framework."
<TextBlock Text="{Binding Version, StringFormat=Avalonia {0}}" FontSize="40" Foreground="White" /> TextWrapping="Wrap"/>
<TextBlock Text="Development Build" IsVisible="{Binding IsDevelopmentBuild}" Margin="0,-10,0,0" FontSize="15" Foreground="White" />
</StackPanel> <TextBlock Grid.Row="1"
</StackPanel> Text="Avalonia is made possible by the generous support of its contributors and community."
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" Spacing="20" Margin="10 60 10 0"> TextWrapping="Wrap"
<TextBlock Text="This product is built with the Avalonia cross-platform UI Framework. &#x0A;&#x0A;Avalonia is made possible by the generous support of it's contributors and community." TextWrapping="Wrap" TextAlignment="Center" HorizontalAlignment="Center" /> Margin="0,15,0,0"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" >
<TextBlock Text="Main source repository | " /> <Button Grid.Row="2" Background="#8B44AC"
<Button Classes="Hyperlink" CommandParameter="https://github.com/AvaloniaUI/Avalonia/" /> Foreground="White"
</StackPanel> VerticalAlignment="Center"
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" > Padding="12"
<TextBlock Text="Documentation and Information | " /> Content="Learn more about Avalonia"
<Button Classes="Hyperlink" CommandParameter="https://avaloniaui.net/" /> Click="Button_OnClick"
</StackPanel> HorizontalAlignment="Center"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" >
<TextBlock Text="Chat Room | " /> <TextBlock Grid.Row="4" VerticalAlignment="Bottom"
<Button Classes="Hyperlink" CommandParameter="https://t.me/Avalonia" /> Text="{Binding Copyright}"
</StackPanel> HorizontalAlignment="Center"/>
</StackPanel> </Grid>
<StackPanel VerticalAlignment="Bottom" Margin="10">
<TextBlock Text="© 2022 The Avalonia Project" TextWrapping="Wrap" HorizontalAlignment="Center" />
</StackPanel>
</Grid> </Grid>
</Window> </Window>

46
src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs

@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
namespace Avalonia.Dialogs namespace Avalonia.Dialogs
@ -11,35 +12,19 @@ namespace Avalonia.Dialogs
{ {
private static readonly Version s_version = typeof(AboutAvaloniaDialog).Assembly.GetName().Version; private static readonly Version s_version = typeof(AboutAvaloniaDialog).Assembly.GetName().Version;
public static string Version { get; } = s_version.ToString(2); public static string Version { get; } = $@"v{s_version.ToString(2)}";
public static bool IsDevelopmentBuild { get; } = s_version.Revision == 999; public static bool IsDevelopmentBuild { get; } = s_version.Revision == 999;
public static string Copyright { get; } = $"© {DateTime.Now.Year} The Avalonia Project";
public AboutAvaloniaDialog() public AboutAvaloniaDialog()
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
DataContext = this; DataContext = this;
} }
public static void OpenBrowser(string url)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
// If no associated application/json MimeType is found xdg-open opens retrun error
// but it tries to open it anyway using the console editor (nano, vim, other..)
ShellExec($"xdg-open {url}", waitForExit: false);
}
else
{
using Process process = Process.Start(new ProcessStartInfo
{
FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? url : "open",
Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"{url}" : "",
CreateNoWindow = true,
UseShellExecute = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
});
}
}
private static void ShellExec(string cmd, bool waitForExit = true) private static void ShellExec(string cmd, bool waitForExit = true)
{ {
@ -64,5 +49,26 @@ namespace Avalonia.Dialogs
} }
} }
} }
private void Button_OnClick(object sender, RoutedEventArgs e)
{
var url = "https://www.avaloniaui.net/";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
// If no associated application/json MimeType is found xdg-open opens retrun error
// but it tries to open it anyway using the console editor (nano, vim, other..)
ShellExec($"xdg-open {url}", waitForExit: false);
}
else
{
using Process process = Process.Start(new ProcessStartInfo
{
FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? url : "open",
Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"{url}" : "",
CreateNoWindow = true,
UseShellExecute = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
});
}
}
} }
} }

6
src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs

@ -18,9 +18,9 @@ namespace Avalonia.Dialogs.Internal
return; return;
} }
_patterns = filter.Patterns? _patterns = filter.Patterns?
.Select(e => new Regex(Regex.Escape(e).Replace(@"\*", ".*").Replace(@"\?", "."), RegexOptions.Singleline | RegexOptions.IgnoreCase)) .Select(e => new Regex("^" + Regex.Escape(e).Replace("\\*", ".*") + "$", RegexOptions.Singleline | RegexOptions.IgnoreCase))
.ToArray(); .ToArray();
} }
public bool Match(string filename) public bool Match(string filename)

4
src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs

@ -105,7 +105,7 @@ namespace Avalonia.OpenGL.Controls
} }
_visual = _compositor.CreateSurfaceVisual(); _visual = _compositor.CreateSurfaceVisual();
_visual.Size = new Vector2((float)Bounds.Width, (float)Bounds.Height); _visual.Size = new Vector(Bounds.Width, Bounds.Height);
_visual.Surface = _resources.Surface; _visual.Surface = _resources.Surface;
ElementComposition.SetElementChildVisual(this, _visual); ElementComposition.SetElementChildVisual(this, _visual);
using (_resources.Context.MakeCurrent()) using (_resources.Context.MakeCurrent())
@ -118,7 +118,7 @@ namespace Avalonia.OpenGL.Controls
{ {
if (_visual != null && change.Property == BoundsProperty) if (_visual != null && change.Property == BoundsProperty)
{ {
_visual.Size = new Vector2((float)Bounds.Width, (float)Bounds.Height); _visual.Size = new Vector(Bounds.Width, Bounds.Height);
RequestNextFrameRendering(); RequestNextFrameRendering();
} }

2
src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml

@ -10,8 +10,6 @@
CornerRadius="{TemplateBinding CornerRadius}" CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Padding}"> Padding="{TemplateBinding Padding}">
<ItemsPresenter Name="PART_ItemsPresenter" <ItemsPresenter Name="PART_ItemsPresenter"
AreVerticalSnapPointsRegular="{TemplateBinding AreVerticalSnapPointsRegular}"
AreHorizontalSnapPointsRegular="{TemplateBinding AreHorizontalSnapPointsRegular}"
ItemsPanel="{TemplateBinding ItemsPanel}"/> ItemsPanel="{TemplateBinding ItemsPanel}"/>
</Border> </Border>
</ControlTemplate> </ControlTemplate>

2
src/Avalonia.Themes.Fluent/Controls/ListBox.xaml

@ -40,8 +40,6 @@
AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}" AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"
BringIntoViewOnFocusChange="{TemplateBinding (ScrollViewer.BringIntoViewOnFocusChange)}"> BringIntoViewOnFocusChange="{TemplateBinding (ScrollViewer.BringIntoViewOnFocusChange)}">
<ItemsPresenter Name="PART_ItemsPresenter" <ItemsPresenter Name="PART_ItemsPresenter"
AreVerticalSnapPointsRegular="{TemplateBinding AreVerticalSnapPointsRegular}"
AreHorizontalSnapPointsRegular="{TemplateBinding AreHorizontalSnapPointsRegular}"
ItemsPanel="{TemplateBinding ItemsPanel}" ItemsPanel="{TemplateBinding ItemsPanel}"
Margin="{TemplateBinding Padding}"/> Margin="{TemplateBinding Padding}"/>
</ScrollViewer> </ScrollViewer>

47
src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml

@ -1,22 +1,25 @@
<Styles xmlns="https://github.com/avaloniaui" <ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:ClassModifier="internal"> x:ClassModifier="internal">
<Style Selector="TextBlock" > <x:Double x:Key="ControlContentThemeFontSize">14</x:Double>
<Setter Property="FontSize" Value="14" /> <x:Double x:Key="ContentControlFontSize">14</x:Double>
</Style> <x:Double x:Key="TextControlThemeMinHeight">24</x:Double>
<Styles.Resources> <x:Double x:Key="TextControlThemeMinWidth">32</x:Double>
<x:Double x:Key="ControlContentThemeFontSize">14</x:Double> <Thickness x:Key="TextControlThemePadding">4, 2</Thickness>
<x:Double x:Key="ContentControlFontSize">14</x:Double> <Thickness x:Key="ListBoxItemPadding">4,2</Thickness>
<x:Double x:Key="TextControlThemeMinHeight">24</x:Double> <x:Double x:Key="ListViewItemMinHeight">24</x:Double>
<Thickness x:Key="TextControlThemePadding">2,2,6,1</Thickness> <Thickness x:Key="ComboBoxItemThemePadding">11,5,11,7</Thickness>
<x:Double x:Key="ListViewItemMinHeight">32</x:Double> <Thickness x:Key="MenuFlyoutItemThemePaddingNarrow">4 4 8 4</Thickness>
<x:Double x:Key="TreeViewItemMinHeight">24</x:Double> <Thickness x:Key="MenuBarItemPadding">5, 2</Thickness>
<Thickness x:Key="TimePickerHostPadding">0,1,0,2</Thickness> <x:Double x:Key="TreeViewItemMinHeight">24</x:Double>
<Thickness x:Key="DatePickerHostPadding">0,1,0,2</Thickness> <Thickness x:Key="TimePickerHostPadding">0,1,0,2</Thickness>
<Thickness x:Key="DatePickerHostMonthPadding">9,0,0,1</Thickness> <Thickness x:Key="DatePickerHostPadding">0,1,0,2</Thickness>
<Thickness x:Key="ComboBoxEditableTextPadding">10,0,30,0</Thickness> <Thickness x:Key="DatePickerHostMonthPadding">9,0,0,1</Thickness>
<x:Double x:Key="ComboBoxMinHeight">24</x:Double> <Thickness x:Key="ComboBoxEditableTextPadding">10,0,30,0</Thickness>
<Thickness x:Key="ComboBoxPadding">12,1,0,3</Thickness> <x:Double x:Key="ComboBoxMinHeight">24</x:Double>
<x:Double x:Key="NavigationViewItemOnLeftMinHeight">32</x:Double> <Thickness x:Key="ComboBoxPadding">12,1,0,3</Thickness>
</Styles.Resources> <x:Double x:Key="NavigationViewItemOnLeftMinHeight">32</x:Double>
</Styles> <x:Double x:Key="TabItemMinHeight">28</x:Double>
<Thickness x:Key="TabItemHeaderMargin">6, 0</Thickness>
<Thickness x:Key="ButtonPadding">6,4</Thickness>
</ResourceDictionary>

2
src/Avalonia.Themes.Fluent/FluentTheme.xaml

@ -18,7 +18,7 @@
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
<!-- These are not part of MergedDictionaries so we can add or remove them easier --> <!-- These are not part of MergedDictionaries so we can add or remove them easier -->
<StyleInclude x:Key="CompactStyles" Source="/DensityStyles/Compact.xaml" /> <ResourceInclude x:Key="CompactStyles" Source="/DensityStyles/Compact.xaml" />
</ResourceDictionary> </ResourceDictionary>
</Styles.Resources> </Styles.Resources>

35
src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs

@ -16,9 +16,10 @@ namespace Avalonia.Themes.Fluent
/// <summary> /// <summary>
/// Includes the fluent theme in an application. /// Includes the fluent theme in an application.
/// </summary> /// </summary>
public class FluentTheme : Styles public class FluentTheme : Styles, IResourceNode
{ {
private readonly Styles _compactStyles; private readonly ResourceDictionary _compactStyles;
private DensityStyle _densityStyle;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="FluentTheme"/> class. /// Initializes a new instance of the <see cref="FluentTheme"/> class.
@ -28,9 +29,7 @@ namespace Avalonia.Themes.Fluent
{ {
AvaloniaXamlLoader.Load(sp, this); AvaloniaXamlLoader.Load(sp, this);
_compactStyles = (Styles)GetAndRemove("CompactStyles"); _compactStyles = (ResourceDictionary)GetAndRemove("CompactStyles");
EnsureCompactStyles();
Palettes = Resources.MergedDictionaries.OfType<ColorPaletteResourcesCollection>().FirstOrDefault() Palettes = Resources.MergedDictionaries.OfType<ColorPaletteResourcesCollection>().FirstOrDefault()
?? throw new InvalidOperationException("FluentTheme was initialized with missing ColorPaletteResourcesCollection."); ?? throw new InvalidOperationException("FluentTheme was initialized with missing ColorPaletteResourcesCollection.");
@ -43,17 +42,17 @@ namespace Avalonia.Themes.Fluent
return val; return val;
} }
} }
public static readonly StyledProperty<DensityStyle> DensityStyleProperty = public static readonly DirectProperty<FluentTheme, DensityStyle> DensityStyleProperty = AvaloniaProperty.RegisterDirect<FluentTheme, DensityStyle>(
AvaloniaProperty.Register<FluentTheme, DensityStyle>(nameof(DensityStyle)); nameof(DensityStyle), o => o.DensityStyle, (o, v) => o.DensityStyle = v);
/// <summary> /// <summary>
/// Gets or sets the density style of the fluent theme (normal, compact). /// Gets or sets the density style of the fluent theme (normal, compact).
/// </summary> /// </summary>
public DensityStyle DensityStyle public DensityStyle DensityStyle
{ {
get => GetValue(DensityStyleProperty); get => _densityStyle;
set => SetValue(DensityStyleProperty, value); set => SetAndRaise(DensityStyleProperty, ref _densityStyle, value);
} }
public IDictionary<ThemeVariant, ColorPaletteResources> Palettes { get; } public IDictionary<ThemeVariant, ColorPaletteResources> Palettes { get; }
@ -64,20 +63,20 @@ namespace Avalonia.Themes.Fluent
if (change.Property == DensityStyleProperty) if (change.Property == DensityStyleProperty)
{ {
EnsureCompactStyles(); Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty);
} }
} }
private void EnsureCompactStyles() bool IResourceNode.TryGetResource(object key, ThemeVariant? theme, out object? value)
{ {
if (DensityStyle == DensityStyle.Compact) // DensityStyle dictionary should be checked first
if (_densityStyle == DensityStyle.Compact
&& _compactStyles.TryGetResource(key, theme, out value))
{ {
Add(_compactStyles); return true;
}
else
{
Remove(_compactStyles);
} }
return base.TryGetResource(key, theme, out value);
} }
} }
} }

2
src/Avalonia.Themes.Simple/Controls/ItemsControl.xaml

@ -11,8 +11,6 @@
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"> CornerRadius="{TemplateBinding CornerRadius}">
<ItemsPresenter Name="PART_ItemsPresenter" <ItemsPresenter Name="PART_ItemsPresenter"
AreVerticalSnapPointsRegular="{TemplateBinding AreVerticalSnapPointsRegular}"
AreHorizontalSnapPointsRegular="{TemplateBinding AreHorizontalSnapPointsRegular}"
ItemsPanel="{TemplateBinding ItemsPanel}" /> ItemsPanel="{TemplateBinding ItemsPanel}" />
</Border> </Border>
</ControlTemplate> </ControlTemplate>

2
src/Avalonia.Themes.Simple/Controls/ListBox.xaml

@ -27,8 +27,6 @@
HorizontalSnapPointsType="{TemplateBinding (ScrollViewer.HorizontalSnapPointsType)}"> HorizontalSnapPointsType="{TemplateBinding (ScrollViewer.HorizontalSnapPointsType)}">
<ItemsPresenter Name="PART_ItemsPresenter" <ItemsPresenter Name="PART_ItemsPresenter"
Margin="{TemplateBinding Padding}" Margin="{TemplateBinding Padding}"
AreVerticalSnapPointsRegular="{TemplateBinding AreVerticalSnapPointsRegular}"
AreHorizontalSnapPointsRegular="{TemplateBinding AreHorizontalSnapPointsRegular}"
ItemsPanel="{TemplateBinding ItemsPanel}" /> ItemsPanel="{TemplateBinding ItemsPanel}" />
</ScrollViewer> </ScrollViewer>
</Border> </Border>

3
src/Avalonia.Themes.Simple/Controls/TransitioningContentControl.xaml

@ -13,11 +13,10 @@
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}" CornerRadius="{TemplateBinding CornerRadius}"
ContentTemplate="{TemplateBinding ContentTemplate}" ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}" Padding="{TemplateBinding Padding}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" /> HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" />
<ContentPresenter Name="PART_TransitionContentPresenter" <ContentPresenter Name="PART_ContentPresenter2"
Background="{TemplateBinding Background}" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"

2
src/Avalonia.X11/X11Clipboard.cs

@ -224,7 +224,7 @@ namespace Avalonia.X11
private Task<object> SendDataRequest(IntPtr format) private Task<object> SendDataRequest(IntPtr format)
{ {
if (_requestedDataTcs == null || _requestedFormatsTcs.Task.IsCompleted) if (_requestedDataTcs == null || _requestedDataTcs.Task.IsCompleted)
_requestedDataTcs = new TaskCompletionSource<object>(); _requestedDataTcs = new TaskCompletionSource<object>();
XConvertSelection(_x11.Display, _x11.Atoms.CLIPBOARD, format, format, _handle, IntPtr.Zero); XConvertSelection(_x11.Display, _x11.Atoms.CLIPBOARD, format, format, _handle, IntPtr.Zero);
return _requestedDataTcs.Task; return _requestedDataTcs.Task;

1
src/tools/Avalonia.Generators/Avalonia.Generators.csproj

@ -6,6 +6,7 @@
<DefineConstants>$(DefineConstants);XAMLX_INTERNAL</DefineConstants> <DefineConstants>$(DefineConstants);XAMLX_INTERNAL</DefineConstants>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<IsRoslynComponent>true</IsRoslynComponent> <IsRoslynComponent>true</IsRoslynComponent>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

84
tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs

@ -71,11 +71,14 @@ namespace Avalonia.Base.UnitTests.Animation
var clock = new TestClock(); var clock = new TestClock();
var animationRun = animation.RunAsync(border, clock); var animationRun = animation.RunAsync(border, clock);
border.Measure(Size.Infinity);
border.Arrange(new Rect(border.DesiredSize));
clock.Step(TimeSpan.Zero); clock.Step(TimeSpan.Zero);
// Initial Delay. // Initial Delay.
clock.Step(TimeSpan.FromSeconds(1)); clock.Step(TimeSpan.FromSeconds(0));
Assert.Equal(border.Width, 0d); Assert.Equal(100d, border.Width);
clock.Step(TimeSpan.FromSeconds(6)); clock.Step(TimeSpan.FromSeconds(6));
@ -126,7 +129,84 @@ namespace Avalonia.Base.UnitTests.Animation
clock.Step(TimeSpan.FromSeconds(0.100d)); clock.Step(TimeSpan.FromSeconds(0.100d));
Assert.Equal(border.Width, 300d); Assert.Equal(border.Width, 300d);
} }
[Theory]
[InlineData(FillMode.Backward, 0, 0d, 0.7d)]
[InlineData(FillMode.Both, 0, 0d, 0.7d)]
[InlineData(FillMode.Forward, 100, 0d, 0.7d)]
[InlineData(FillMode.Backward, 0, 0.3d, 0.7d)]
[InlineData(FillMode.Both, 0, 0.3d, 0.7d)]
[InlineData(FillMode.Forward, 100, 0.3d, 0.7d)]
public void Check_FillMode_Start_Value(FillMode fillMode, double target, double startCue, double endCue)
{
var keyframe1 = new KeyFrame()
{
Setters = { new Setter(Layoutable.WidthProperty, 0d), }, Cue = new Cue(startCue)
};
var keyframe2 = new KeyFrame()
{
Setters = { new Setter(Layoutable.WidthProperty, 300d), }, Cue = new Cue(endCue)
};
var animation = new Animation()
{
Duration = TimeSpan.FromSeconds(10d),
Delay = TimeSpan.FromSeconds(5d),
FillMode = fillMode,
Children = { keyframe1, keyframe2 }
};
var border = new Border() { Height = 100d, Width = 100d, };
var clock = new TestClock();
animation.RunAsync(border, clock);
clock.Step(TimeSpan.Zero);
Assert.Equal(target, border.Width);
}
[Theory]
[InlineData(FillMode.Backward, 100, 0.3d, 1d)]
[InlineData(FillMode.Both, 300, 0.3d, 1d)]
[InlineData(FillMode.Forward, 300, 0.3d, 1d)]
[InlineData(FillMode.Backward, 100, 0.3d, 0.7d)]
[InlineData(FillMode.Both, 300, 0.3d, 0.7d)]
[InlineData(FillMode.Forward, 300, 0.3d, 0.7d)]
public void Check_FillMode_End_Value(FillMode fillMode, double target, double startCue, double endCue)
{
var keyframe1 = new KeyFrame()
{
Setters = { new Setter(Layoutable.WidthProperty, 0d), }, Cue = new Cue(0.7d)
};
var keyframe2 = new KeyFrame()
{
Setters = { new Setter(Layoutable.WidthProperty, 300d), }, Cue = new Cue(1d)
};
var animation = new Animation()
{
Duration = TimeSpan.FromSeconds(10d),
Delay = TimeSpan.FromSeconds(5d),
FillMode = fillMode,
Children = { keyframe1, keyframe2 }
};
var border = new Border() { Height = 100d, Width = 100d, };
var clock = new TestClock();
animation.RunAsync(border, clock);
clock.Step(TimeSpan.FromSeconds(0));
clock.Step(TimeSpan.FromSeconds(20));
Assert.Equal(target, border.Width);
}
[Fact] [Fact]
public void Dispose_Subscription_Should_Stop_Animation() public void Dispose_Subscription_Should_Stop_Animation()
{ {

13
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs

@ -243,6 +243,19 @@ namespace Avalonia.Base.UnitTests
Assert.Equal(new[] { 11 }, target.CoerceFooInvocations); Assert.Equal(new[] { 11 }, target.CoerceFooInvocations);
} }
[Fact]
public void Second_Coerce_Of_Default_Value_Is_Passed_Uncoerced_Value()
{
var target = new Class1();
target.MinFoo = 20;
target.CoerceFooInvocations.Clear();
target.CoerceValue(Class1.FooProperty);
target.CoerceValue(Class1.FooProperty);
Assert.Equal(new[] { 11, 11 }, target.CoerceFooInvocations);
}
[Fact] [Fact]
public void ClearValue_Respects_Coerced_Default_Value() public void ClearValue_Respects_Coerced_Default_Value()
{ {

8
tests/Avalonia.Base.UnitTests/Media/BoxShadowTests.cs

@ -41,5 +41,13 @@ namespace Avalonia.Base.UnitTests.Media
Assert.Equal(Colors.Red, parsed.Color); Assert.Equal(Colors.Red, parsed.Color);
} }
} }
[Fact]
public void BoxShadows_Should_ToString()
{
const string source = "-20 -20 60 #CCFFFFFF, 20 20 60 #33000000";
var parsed = BoxShadows.Parse(source);
Assert.Equal(source, parsed.ToString(), true);
}
} }
} }

68
tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs

@ -5,7 +5,7 @@ using Avalonia.Base.UnitTests.Animation;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.PropertyStore; using Avalonia.Media;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Moq; using Moq;
@ -963,6 +963,72 @@ namespace Avalonia.Base.UnitTests.Styling
Assert.Equal(0.0, target.Double); Assert.Equal(0.0, target.Double);
} }
[Fact]
public void Animations_With_Activator_Trigger_Should_Be_Activated_And_Deactivated()
{
var clock = new TestClock();
var border = new Border();
var root = new TestRoot
{
Clock = clock,
Styles =
{
new Style(x => x.OfType<Border>().Not(default(Selector).Class("foo")))
{
Setters =
{
new Setter(Border.BackgroundProperty, Brushes.Yellow),
},
Animations =
{
new Avalonia.Animation.Animation
{
Duration = TimeSpan.FromSeconds(1.0),
Children =
{
new KeyFrame
{
Setters =
{
new Setter(Border.BackgroundProperty, Brushes.Green)
},
Cue = new Cue(0.0)
},
new KeyFrame
{
Setters =
{
new Setter(Border.BackgroundProperty, Brushes.Green)
},
Cue = new Cue(1.0)
}
}
}
}
},
new Style(x => x.OfType<Border>().Class("foo"))
{
Setters =
{
new Setter(Border.BackgroundProperty, Brushes.Blue),
}
}
},
Child = border
};
root.Measure(Size.Infinity);
Assert.Equal(Brushes.Yellow, border.Background);
clock.Step(TimeSpan.FromSeconds(0.5));
Assert.Equal(Brushes.Green, border.Background);
border.Classes.Add("foo");
Assert.Equal(Brushes.Blue, border.Background);
}
private class Class1 : Control private class Class1 : Control
{ {
public static readonly StyledProperty<string> FooProperty = public static readonly StyledProperty<string> FooProperty =

19
tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs

@ -169,6 +169,25 @@ namespace Avalonia.Controls.UnitTests
new Rect(0, 100, 100, 25)); new Rect(0, 100, 100, 25));
} }
[Fact]
public void Bounds_On_Transform_Applied_Then_Removed_Are_Correct()
{
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
var control = CreateWithChildAndMeasureAndTransform(
100,
25,
new RotateTransform { Angle = 90 });
Assert.Equal(new Size(25, 100), control.DesiredSize);
control.LayoutTransform = null;
control.Measure(Size.Infinity);
control.Arrange(new Rect(control.DesiredSize));
Assert.Equal(new Size(100, 25), control.DesiredSize);
}
[Fact] [Fact]
public void Should_Generate_RotateTransform_90_degrees() public void Should_Generate_RotateTransform_90_degrees()
{ {

51
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
@ -1223,6 +1224,29 @@ namespace Avalonia.Controls.UnitTests
Assert.Same(item, root.FocusManager.GetFocusedElement()); Assert.Same(item, root.FocusManager.GetFocusedElement());
} }
[Fact]
public void Reads_Only_Realized_Items_From_ItemsSource()
{
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
var data = new DataVirtualizingList();
var target = new ListBox
{
Template = ListBoxTemplate(),
ItemsSource = data,
};
Prepare(target);
var panel = Assert.IsType<VirtualizingStackPanel>(target.ItemsPanelRoot);
Assert.Equal(0, panel.FirstRealizedIndex);
Assert.Equal(9, panel.LastRealizedIndex);
Assert.Equal(
Enumerable.Range(0, 10).Select(x => $"Item{x}"),
data.GetRealizedItems());
}
private static void RaiseKeyEvent(Control target, Key key, KeyModifiers inputModifiers = 0) private static void RaiseKeyEvent(Control target, Key key, KeyModifiers inputModifiers = 0)
{ {
target.RaiseEvent(new KeyEventArgs target.RaiseEvent(new KeyEventArgs
@ -1252,5 +1276,32 @@ namespace Avalonia.Controls.UnitTests
public event NotifyCollectionChangedEventHandler CollectionChanged; public event NotifyCollectionChangedEventHandler CollectionChanged;
} }
private class DataVirtualizingList : IList
{
private readonly List<string> _inner = new(Enumerable.Repeat<string>(null, 100));
public object this[int index]
{
get => _inner[index] = $"Item{index}";
set => throw new NotSupportedException();
}
public IEnumerable<string> GetRealizedItems() => _inner.Where(x => x is not null);
public bool IsFixedSize => true;
public bool IsReadOnly => true;
public int Count => _inner.Count;
public bool IsSynchronized => false;
public object SyncRoot => this;
public int Add(object value) => throw new NotSupportedException();
public void Clear() => throw new NotSupportedException();
public bool Contains(object value) => throw new NotImplementedException();
public void CopyTo(Array array, int index) => throw new NotImplementedException();
public IEnumerator GetEnumerator() => _inner.GetEnumerator();
public int IndexOf(object value) => throw new NotImplementedException();
public void Insert(int index, object value) => throw new NotSupportedException();
public void Remove(object value) => throw new NotSupportedException();
public void RemoveAt(int index) => throw new NotSupportedException();
}
} }
} }

12
tests/Avalonia.Controls.UnitTests/MenuItemTests.cs

@ -209,13 +209,13 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(0, canExecuteCallCount); Assert.Equal(0, canExecuteCallCount);
contextMenu.Open(); contextMenu.Open();
Assert.Equal(2, canExecuteCallCount);//2 because popup is changing logical child Assert.Equal(3, canExecuteCallCount);// 3 because popup is changing logical child and moreover we need to invalidate again after the item is attached to the visual tree
command.RaiseCanExecuteChanged(); command.RaiseCanExecuteChanged();
Assert.Equal(3, canExecuteCallCount); Assert.Equal(4, canExecuteCallCount);
target.CommandParameter = true; target.CommandParameter = true;
Assert.Equal(4, canExecuteCallCount); Assert.Equal(5, canExecuteCallCount);
} }
} }
@ -249,13 +249,13 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(0, canExecuteCallCount); Assert.Equal(0, canExecuteCallCount);
flyout.ShowAt(button); flyout.ShowAt(button);
Assert.Equal(1, canExecuteCallCount); Assert.Equal(2, canExecuteCallCount); // 2 because we need to invalidate after the item is attached to the visual tree
command.RaiseCanExecuteChanged(); command.RaiseCanExecuteChanged();
Assert.Equal(2, canExecuteCallCount); Assert.Equal(3, canExecuteCallCount);
target.CommandParameter = true; target.CommandParameter = true;
Assert.Equal(3, canExecuteCallCount); Assert.Equal(4, canExecuteCallCount);
} }
} }

31
tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs

@ -4,6 +4,7 @@ using System.Reactive.Linq;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.UnitTests;
using Xunit; using Xunit;
namespace Avalonia.Controls.UnitTests.Presenters namespace Avalonia.Controls.UnitTests.Presenters
@ -244,6 +245,36 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.Equal(new Size(110, 110), target.Extent); Assert.Equal(new Size(110, 110), target.Extent);
} }
[Fact]
public void Extent_Should_Include_Content_Margin_Scaled_With_Layout_Rounding()
{
var root = new TestRoot
{
LayoutScaling = 1.25,
UseLayoutRounding = true
};
var target = new ScrollContentPresenter
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Content = new Border
{
Width = 200,
Height = 200,
Margin = new Thickness(2)
}
};
root.Child = target;
target.UpdateChild();
target.Measure(new Size(1000, 1000));
target.Arrange(new Rect(0, 0, 1000, 1000));
Assert.Equal(new Size(203.2, 203.2), target.Viewport);
Assert.Equal(new Size(203.2, 203.2), target.Extent);
}
[Fact] [Fact]
public void Extent_Width_Should_Be_Arrange_Width_When_CanScrollHorizontally_False() public void Extent_Width_Should_Be_Arrange_Width_When_CanScrollHorizontally_False()
{ {

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

@ -22,5 +22,4 @@
<Import Project="..\..\build\HarfBuzzSharp.props" /> <Import Project="..\..\build\HarfBuzzSharp.props" />
<Import Project="..\..\build\XUnit.props" /> <Import Project="..\..\build\XUnit.props" />
<Import Project="..\..\build\SharedVersion.props" /> <Import Project="..\..\build\SharedVersion.props" />
<Import Project="..\..\build\AvaloniaPublicKey.props" />
</Project> </Project>

Loading…
Cancel
Save