From 700b1996029fef16568292be2312e2c229b8bac5 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 13 Jun 2023 19:52:13 +0600 Subject: [PATCH 01/42] Use double for compositor types --- .../Pages/CompositionPage.axaml.cs | 10 +- samples/ControlCatalog/Pages/GesturePage.cs | 16 +- samples/GpuInterop/DrawingSurfaceDemoBase.cs | 4 +- samples/Sandbox/MainWindow.axaml | 7 + .../Composition/Animations/Interpolators.cs | 25 +++ .../Composition/CompositingRenderer.cs | 6 +- .../CompositionCustomVisualHandler.cs | 2 +- .../Composition/CompositionTarget.cs | 2 +- .../Expressions/BuiltInExpressionFfi.cs | 59 +++++- .../Expressions/DelegateExpressionFfi.cs | 43 +++++ .../Expressions/ExpressionVariant.cs | 178 +++++++++++++++++- .../Rendering/Composition/MatrixUtils.cs | 33 ++-- .../ServerCompositionContainerVisual.cs | 7 +- .../Server/ServerCompositionVisual.cs | 20 +- .../Rendering/Composition/Visual.cs | 2 +- src/Avalonia.Base/Vector.cs | 48 +++++ src/Avalonia.Base/Vector3D.cs | 145 ++++++++++++++ src/Avalonia.Base/composition-schema.xml | 15 +- .../PullToRefresh/RefreshVisualizer.cs | 36 ++-- .../Controls/OpenGlControlBase.cs | 4 +- 20 files changed, 583 insertions(+), 79 deletions(-) create mode 100644 src/Avalonia.Base/Vector3D.cs diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs index 0d3061f361..5bf46510dc 100644 --- a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs @@ -144,8 +144,8 @@ public partial class CompositionPage : UserControl { if(_solidVisual == null) return; - _solidVisual.Size = new Vector2((float)v.Bounds.Width / 3, (float)v.Bounds.Height / 3); - _solidVisual.Offset = new Vector3((float)v.Bounds.Width / 3, (float)v.Bounds.Height / 3, 0); + _solidVisual.Size = new (v.Bounds.Width / 3, v.Bounds.Height / 3); + _solidVisual.Offset = new (v.Bounds.Width / 3, v.Bounds.Height / 3, 0); } v.AttachedToVisualTree += delegate { @@ -164,7 +164,7 @@ public partial class CompositionPage : UserControl animation.Direction = PlaybackDirection.Alternate; _solidVisual.StartAnimation("Color", animation); - _solidVisual.AnchorPoint = new Vector2(0, 0); + _solidVisual.AnchorPoint = new (0, 0); var scale = _solidVisual.Compositor.CreateVector3KeyFrameAnimation(); scale.Duration = TimeSpan.FromSeconds(5); @@ -195,8 +195,8 @@ public partial class CompositionPage : UserControl if (_customVisual == null) return; var h = (float)Math.Min(v.Bounds.Height, v.Bounds.Width / 3); - _customVisual.Size = new Vector2((float)v.Bounds.Width, h); - _customVisual.Offset = new Vector3(0, (float)(v.Bounds.Height - h) / 2, 0); + _customVisual.Size = new (v.Bounds.Width, h); + _customVisual.Offset = new (0, (v.Bounds.Height - h) / 2, 0); } v.AttachedToVisualTree += delegate { diff --git a/samples/ControlCatalog/Pages/GesturePage.cs b/samples/ControlCatalog/Pages/GesturePage.cs index cc4429f414..c81ba4fb17 100644 --- a/samples/ControlCatalog/Pages/GesturePage.cs +++ b/samples/ControlCatalog/Pages/GesturePage.cs @@ -13,7 +13,7 @@ namespace ControlCatalog.Pages public class GesturePage : UserControl { private bool _isInit; - private float _currentScale; + private double _currentScale; public GesturePage() { @@ -53,7 +53,7 @@ namespace ControlCatalog.Pages if(compositionVisual!= null) { _currentScale = 1; - compositionVisual.Scale = new Vector3(1,1,1); + compositionVisual.Scale = new (1,1,1); compositionVisual.Offset = default; image.InvalidateMeasure(); } @@ -69,7 +69,7 @@ namespace ControlCatalog.Pages } _currentScale = 1; - Vector3 currentOffset = default; + Vector3D currentOffset = default; CompositionVisual? compositionVisual = null; @@ -133,11 +133,11 @@ namespace ControlCatalog.Pages 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; - 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), 0); @@ -157,7 +157,7 @@ namespace ControlCatalog.Pages var ball = control.FindLogicalDescendantOfType(); - Vector3 defaultOffset = default; + Vector3D defaultOffset = default; CompositionVisual? ballCompositionVisual = null; @@ -181,11 +181,11 @@ namespace ControlCatalog.Pages 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!); 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; } diff --git a/samples/GpuInterop/DrawingSurfaceDemoBase.cs b/samples/GpuInterop/DrawingSurfaceDemoBase.cs index aad813ea82..367436e5a5 100644 --- a/samples/GpuInterop/DrawingSurfaceDemoBase.cs +++ b/samples/GpuInterop/DrawingSurfaceDemoBase.cs @@ -48,7 +48,7 @@ public abstract class DrawingSurfaceDemoBase : Control, IGpuDemo Surface = _compositor.CreateDrawingSurface(); _visual = _compositor.CreateSurfaceVisual(); - _visual.Size = new Vector2((float)Bounds.Width, (float)Bounds.Height); + _visual.Size = new (Bounds.Width, Bounds.Height); _visual.Surface = Surface; ElementComposition.SetElementChildVisual(this, _visual); var (res, info) = await DoInitialize(_compositor, Surface); @@ -72,7 +72,7 @@ public abstract class DrawingSurfaceDemoBase : Control, IGpuDemo if (root == null) 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); RenderFrame(size); if (SupportsDisco && Disco > 0) diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index 6929f192c7..f96abcac96 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -1,4 +1,11 @@ + + + + + + + diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs b/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs index e19c0ff098..0df21c2c1d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs @@ -19,6 +19,12 @@ namespace Avalonia.Rendering.Composition.Animations public static ScalarInterpolator Instance { get; } = new ScalarInterpolator(); } + class DoubleInterpolator : IInterpolator + { + public double Interpolate(double @from, double to, float progress) => @from + (to - @from) * progress; + + public static DoubleInterpolator Instance { get; } = new (); + } class Vector2Interpolator : IInterpolator { @@ -28,6 +34,15 @@ namespace Avalonia.Rendering.Composition.Animations public static Vector2Interpolator Instance { get; } = new Vector2Interpolator(); } + class VectorInterpolator : IInterpolator + { + 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 { 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(); } + class Vector3DInterpolator : IInterpolator + { + 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 { public Vector4 Interpolate(Vector4 @from, Vector4 to, float progress) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 61a3f7e831..368743f1b5 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -241,8 +241,8 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester continue; // 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.Size = new Vector2((float)visual.Bounds.Width, (float)visual.Bounds.Height); + comp.Offset = new (visual.Bounds.Left, visual.Bounds.Top, 0); + comp.Size = new (visual.Bounds.Width, visual.Bounds.Height); comp.Visible = visual.IsVisible; comp.Opacity = (float)visual.Opacity; comp.ClipToBounds = visual.ClipToBounds; @@ -269,7 +269,7 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester renderTransform *= (-offset) * visual.RenderTransform.Value * (offset); } - comp.TransformMatrix = MatrixUtils.ToMatrix4x4(renderTransform); + comp.TransformMatrix = renderTransform; try { diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs b/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs index b7ed6fe612..598d4163d1 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs @@ -28,7 +28,7 @@ public abstract class CompositionCustomVisualHandler _host.Compositor.VerifyAccess(); } - protected Vector2 EffectiveSize + protected Vector EffectiveSize { get { diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index 91d2fb59de..121d11bf2a 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -70,7 +70,7 @@ namespace Avalonia.Rendering.Composition return false; } - var m33 = MatrixUtils.ToMatrix(m.Value); + var m33 = m.Value; return m33.TryInvert(out matrix); } diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs index 896ae4fc3a..2fdf163153 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs @@ -16,6 +16,7 @@ namespace Avalonia.Rendering.Composition.Expressions private readonly DelegateExpressionFfi _registry; 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) { @@ -34,6 +35,13 @@ namespace Avalonia.Rendering.Composition.Expressions var t = MathUtilities.Clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); 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) { @@ -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) { 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) { @@ -69,7 +95,9 @@ namespace Avalonia.Rendering.Composition.Expressions { {"Abs", (float f) => Math.Abs(f)}, {"Abs", (Vector2 v) => Vector2.Abs(v)}, + {"Abs", (Vector v) => v.Abs()}, {"Abs", (Vector3 v) => Vector3.Abs(v)}, + {"Abs", (Vector3D v) => v.Abs()}, {"Abs", (Vector4 v) => Vector4.Abs(v)}, {"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", (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", (Vector3D a1, Vector3D a2, Vector3D a3) => Vector3D.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)}, @@ -109,11 +139,15 @@ namespace Avalonia.Rendering.Composition.Expressions }, {"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", (Vector3D a1, Vector3D a2) => Vector3D.Distance(a1, a2)}, {"Distance", (Vector4 a1, Vector4 a2) => Vector4.Distance(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", (Vector3D a1, Vector3D a2) => Vector3D.DistanceSquared(a1, a2)}, {"DistanceSquared", (Vector4 a1, Vector4 a2) => Vector4.DistanceSquared(a1, a2)}, {"Floor", (float v) => (float) Math.Floor(v)}, @@ -123,18 +157,24 @@ namespace Avalonia.Rendering.Composition.Expressions {"Length", (Vector2 a1) => a1.Length()}, + {"Length", (Vector a1) => a1.Length}, {"Length", (Vector3 a1) => a1.Length()}, + {"Length", (Vector3D a1) => a1.Length}, {"Length", (Vector4 a1) => a1.Length()}, {"Length", (Quaternion a1) => a1.Length()}, {"LengthSquared", (Vector2 a1) => a1.LengthSquared()}, + {"LengthSquared", (Vector a1) => a1*a1}, {"LengthSquared", (Vector3 a1) => a1.LengthSquared()}, + {"LengthSquared", (Vector3D a1) => Vector3D.Dot(a1, a1)}, {"LengthSquared", (Vector4 a1) => a1.LengthSquared()}, {"LengthSquared", (Quaternion a1) => a1.LengthSquared()}, {"Lerp", (float a1, float a2, float a3) => 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", (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)}, @@ -173,24 +213,31 @@ namespace Avalonia.Rendering.Composition.Expressions {"Max", (float a1, float a2) => Math.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", (Vector3D a1, Vector3D a2) => Vector3D.Max(a1, a2)}, {"Max", (Vector4 a1, Vector4 a2) => Vector4.Max(a1, a2)}, {"Min", (float a1, float a2) => Math.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", (Vector3D a1, Vector3D a2) => Vector3D.Min(a1, a2)}, {"Min", (Vector4 a1, Vector4 a2) => Vector4.Min(a1, a2)}, {"Mod", (float a, float b) => a % b}, {"Normalize", (Quaternion a) => Quaternion.Normalize(a)}, {"Normalize", (Vector2 a) => Vector2.Normalize(a)}, + {"Normalize", (Vector a) => Vector.Normalize(a)}, {"Normalize", (Vector3 a) => Vector3.Normalize(a)}, + {"Normalize", (Vector3D a) => Vector3D.Normalize(a)}, {"Normalize", (Vector4 a) => Vector4.Normalize(a)}, {"Pow", (float a, float b) => (float) Math.Pow(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)}, {"Round", (float a) => (float) Math.Round(a)}, @@ -198,14 +245,18 @@ namespace Avalonia.Rendering.Composition.Expressions {"Scale", (Matrix3x2 a, float b) => a * b}, {"Scale", (Matrix4x4 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", (Vector3D a, float b) => Vector3D.Multiply(a, b)}, {"Scale", (Vector4 a, float b) => a * b}, {"Sin", (float a) => (float) Math.Sin(a)}, {"SmoothStep", (float a1, float a2, float 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", (Vector3D a1, Vector3D a2, Vector3D 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 @@ -222,9 +273,13 @@ namespace Avalonia.Rendering.Composition.Expressions {"Transform", (Vector2 a, Matrix3x2 b) => Vector2.Transform(a, b)}, {"Transform", (Vector3 a, Matrix4x4 b) => Vector3.Transform(a, b)}, - {"Vector2", (float a, float b) => new Vector2(a, b)}, - {"Vector3", (float a, float b, float c) => new Vector3(a, b, c)}, + {"Vector2", (float a, float b) => new Vector(a, b)}, + {"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", (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", (Vector2 v2, float z, float w) => new Vector4(v2, z, w)}, {"Vector4", (Vector3 v3, float w) => new Vector4(v3, w)}, diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs index c15487065c..ad8d370478 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs +++ b/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 countGroup, IReadOnlyList 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; } @@ -75,8 +115,11 @@ namespace Avalonia.Rendering.Composition.Expressions { [typeof(bool)] = VariantType.Boolean, [typeof(float)] = VariantType.Scalar, + [typeof(double)] = VariantType.Double, [typeof(Vector2)] = VariantType.Vector2, + [typeof(Vector)] = VariantType.Vector, [typeof(Vector3)] = VariantType.Vector3, + [typeof(Vector3D)] = VariantType.Vector3D, [typeof(Vector4)] = VariantType.Vector4, [typeof(Matrix3x2)] = VariantType.Matrix3x2, [typeof(Matrix4x4)] = VariantType.Matrix4x4, diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs index 21f14283b5..a97b959fe1 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs @@ -17,6 +17,8 @@ namespace Avalonia.Rendering.Composition.Expressions Vector2, Vector3, Vector4, + Vector, + Vector3D, AvaloniaMatrix, Matrix3x2, Matrix4x4, @@ -38,6 +40,8 @@ namespace Avalonia.Rendering.Composition.Expressions [FieldOffset(4)] public Vector2 Vector2; [FieldOffset(4)] public Vector3 Vector3; [FieldOffset(4)] public Vector4 Vector4; + [FieldOffset(4)] public Vector Vector; + [FieldOffset(4)] public Vector3D Vector3D; [FieldOffset(4)] public Matrix AvaloniaMatrix; [FieldOffset(4)] public Matrix3x2 Matrix3x2; [FieldOffset(4)] public Matrix4x4 Matrix4x4; @@ -55,6 +59,15 @@ namespace Avalonia.Rendering.Composition.Expressions return Vector2.Y; 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) { @@ -78,6 +91,29 @@ namespace Avalonia.Rendering.Composition.Expressions return new Vector2(Vector3.Z, Vector3.Y); 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) { @@ -115,14 +151,20 @@ namespace Avalonia.Rendering.Composition.Expressions return AvaloniaMatrix.M11; if (ReferenceEquals(property, "M12")) return AvaloniaMatrix.M12; + if (ReferenceEquals(property, "M13")) + return AvaloniaMatrix.M13; if (ReferenceEquals(property, "M21")) return AvaloniaMatrix.M21; if (ReferenceEquals(property, "M22")) return AvaloniaMatrix.M22; + if (ReferenceEquals(property, "M23")) + return AvaloniaMatrix.M23; if (ReferenceEquals(property, "M31")) return AvaloniaMatrix.M31; if (ReferenceEquals(property, "M32")) return AvaloniaMatrix.M32; + if (ReferenceEquals(property, "M33")) + return AvaloniaMatrix.M33; return default; } @@ -220,7 +262,13 @@ namespace Avalonia.Rendering.Composition.Expressions Type = VariantType.Vector2, Vector2 = value }; - + + public static implicit operator ExpressionVariant(Vector value) => + new ExpressionVariant + { + Type = VariantType.Vector, + Vector = value + }; public static implicit operator ExpressionVariant(Vector3 value) => new ExpressionVariant @@ -228,6 +276,13 @@ namespace Avalonia.Rendering.Composition.Expressions Type = VariantType.Vector3, Vector3 = value }; + + public static implicit operator ExpressionVariant(Vector3D value) => + new ExpressionVariant + { + Type = VariantType.Vector3D, + Vector3D = value + }; public static implicit operator ExpressionVariant(Vector4 value) => @@ -285,10 +340,16 @@ namespace Avalonia.Rendering.Composition.Expressions if (left.Type == VariantType.Vector2) return left.Vector2 + right.Vector2; + + if (left.Type == VariantType.Vector) + return left.Vector + right.Vector; if (left.Type == VariantType.Vector3) return left.Vector3 + right.Vector3; + if (left.Type == VariantType.Vector3D) + return Avalonia.Vector3D.Add(left.Vector3D, right.Vector3D); + if (left.Type == VariantType.Vector4) return left.Vector4 + right.Vector4; @@ -317,10 +378,16 @@ namespace Avalonia.Rendering.Composition.Expressions if (left.Type == VariantType.Vector2) return left.Vector2 - right.Vector2; + + if (left.Type == VariantType.Vector) + return left.Vector - right.Vector; if (left.Type == VariantType.Vector3) return left.Vector3 - right.Vector3; + if (left.Type == VariantType.Vector3D) + return Vector3D.Add(left.Vector3D, -right.Vector3D); + if (left.Type == VariantType.Vector4) return left.Vector4 - right.Vector4; @@ -347,9 +414,15 @@ namespace Avalonia.Rendering.Composition.Expressions if (left.Type == VariantType.Vector2) return -left.Vector2; + + if (left.Type == VariantType.Vector) + return -left.Vector; if (left.Type == VariantType.Vector3) return -left.Vector3; + + if (left.Type == VariantType.Vector3D) + return -left.Vector3D; if (left.Type == VariantType.Vector4) return -left.Vector4; @@ -383,14 +456,29 @@ namespace Avalonia.Rendering.Composition.Expressions if (left.Type == VariantType.Vector2 && right.Type == VariantType.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) 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) 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) 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) return left.Vector4 * right.Vector4; @@ -436,15 +524,33 @@ namespace Avalonia.Rendering.Composition.Expressions if (left.Type == VariantType.Vector2 && right.Type == VariantType.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) 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) 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) 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) return left.Vector4 / right.Vector4; @@ -471,9 +577,15 @@ namespace Avalonia.Rendering.Composition.Expressions if (Type == VariantType.Vector2) return Vector2 == right.Vector2; - + + if (Type == VariantType.Vector) + return Vector == right.Vector; + if (Type == VariantType.Vector3) return Vector3 == right.Vector3; + + if (Type == VariantType.Vector3D) + return Vector3D == right.Vector3D; if (Type == VariantType.Vector4) return Vector4 == right.Vector4; @@ -571,6 +683,11 @@ namespace Avalonia.Rendering.Composition.Expressions res = (T) (object) Scalar; return true; } + if (Type == VariantType.Double) + { + res = (T)(object)Scalar; + return true; + } } if (typeof(T) == typeof(double)) @@ -580,6 +697,12 @@ namespace Avalonia.Rendering.Composition.Expressions res = (T) (object) Double; return true; } + + if (Type == VariantType.Scalar) + { + res = (T)(object)(float)Double; + return true; + } } if (typeof(T) == typeof(Vector2)) @@ -589,6 +712,27 @@ namespace Avalonia.Rendering.Composition.Expressions res = (T) (object) Vector2; 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)) @@ -598,6 +742,26 @@ namespace Avalonia.Rendering.Composition.Expressions res = (T) (object) Vector3; 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)) @@ -668,9 +832,15 @@ namespace Avalonia.Rendering.Composition.Expressions if (typeof(T) == typeof(Vector2)) return (Vector2) (object) v; + + if (typeof(T) == typeof(Vector)) + return (Vector) (object) v; if (typeof(T) == typeof(Vector3)) return (Vector3) (object) v; + + if (typeof(T) == typeof(Vector3D)) + return (Vector3D) (object) v; if (typeof(T) == typeof(Vector4)) return (Vector4) (object) v; @@ -709,8 +879,12 @@ namespace Avalonia.Rendering.Composition.Expressions return Double.ToString(CultureInfo.InvariantCulture); if (Type == VariantType.Vector2) return Vector2.ToString(); + if (Type == VariantType.Vector) + return Vector.ToString(); if (Type == VariantType.Vector3) return Vector3.ToString(); + if (Type == VariantType.Vector3D) + return Vector3D.ToString(); if (Type == VariantType.Vector4) return Vector4.ToString(); if (Type == VariantType.Quaternion) diff --git a/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs index 3d8d5bae78..714895cd71 100644 --- a/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs +++ b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs @@ -6,41 +6,46 @@ namespace Avalonia.Rendering.Composition { static class MatrixUtils { - public static Matrix4x4 ComputeTransform(Vector2 size, Vector2 anchorPoint, Vector3 centerPoint, - Matrix4x4 transformMatrix, Vector3 scale, float rotationAngle, Quaternion orientation, Vector3 offset) + public static Matrix ComputeTransform(Vector size, Vector anchorPoint, Vector3D centerPoint, + 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 - - 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) mat = transformMatrix * mat; - if (scale != new Vector3(1, 1, 1)) - mat *= Matrix4x4.CreateScale(scale, center); + if (scale != new Vector3D(1, 1, 1)) + mat *= ToMatrix(Matrix4x4.CreateScale(scale.ToVector3(), center.ToVector3())); //TODO: RotationAxis support if (rotationAngle != 0) - mat *= Matrix4x4.CreateRotationZ(rotationAngle, center); + mat *= ToMatrix(Matrix4x4.CreateRotationZ(rotationAngle, center.ToVector3())); if (orientation != Quaternion.Identity) { if (centerPoint != default) { - mat *= Matrix4x4.CreateTranslation(-center) - * Matrix4x4.CreateFromQuaternion(orientation) - * Matrix4x4.CreateTranslation(center); + mat *= ToMatrix(Matrix4x4.CreateTranslation(-center.ToVector3()) + * Matrix4x4.CreateFromQuaternion(orientation) + * Matrix4x4.CreateTranslation(center.ToVector3())); } else - mat *= Matrix4x4.CreateFromQuaternion(orientation); + mat *= ToMatrix(Matrix4x4.CreateFromQuaternion(orientation)); } 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; } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs index b9e6833d21..c63e7917b3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs +++ b/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 (CombinedTransformMatrix is { - M12: 0, M13: 0, M14: 0, - M21: 0, M23: 0, M24: 0, - M31: 0, M32: 0, M34: 0, - M43: 0, M44: 1 + M12: 0, M13: 0, + M21: 0, M23: 0, + M31: 0, M32: 0 }) padding = new Thickness(padding.Left * CombinedTransformMatrix.M11, padding.Top * CombinedTransformMatrix.M22, diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index 0eeffda123..3ff1bf2b98 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -51,7 +51,7 @@ namespace Avalonia.Rendering.Composition.Server canvas.PushClip(AdornedVisual._combinedTransformedClipBounds); } var transform = GlobalTransformMatrix; - canvas.PostTransform = MatrixUtils.ToMatrix(transform); + canvas.PostTransform = transform; canvas.Transform = Matrix.Identity; if (Effect != null) @@ -71,7 +71,7 @@ namespace Avalonia.Rendering.Composition.Server RenderCore(canvas, currentTransformedClip); // Hack to force invalidation of SKMatrix - canvas.PostTransform = MatrixUtils.ToMatrix(transform); + canvas.PostTransform = transform; canvas.Transform = Matrix.Identity; if (OpacityMaskBrush != null) @@ -106,8 +106,8 @@ namespace Avalonia.Rendering.Composition.Server return ref _readback2; } - public Matrix4x4 CombinedTransformMatrix { get; private set; } = Matrix4x4.Identity; - public Matrix4x4 GlobalTransformMatrix { get; private set; } + public Matrix CombinedTransformMatrix { get; private set; } = Matrix.Identity; + public Matrix GlobalTransformMatrix { get; private set; } 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, // HACK: Ignore RenderTransform set by the adorner layer - AdornedVisual != null ? Matrix4x4.Identity : TransformMatrix, + AdornedVisual != null ? Matrix.Identity : TransformMatrix, Scale, RotationAngle, Orientation, Offset); _combinedTransformDirty = false; } - var parentTransform = (AdornedVisual ?? Parent)?.GlobalTransformMatrix ?? Matrix4x4.Identity; + var parentTransform = (AdornedVisual ?? Parent)?.GlobalTransformMatrix ?? Matrix.Identity; var newTransform = CombinedTransformMatrix * parentTransform; @@ -148,7 +148,7 @@ namespace Avalonia.Rendering.Composition.Server if (GlobalTransformMatrix != newTransform) { _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; } @@ -179,14 +179,14 @@ namespace Avalonia.Rendering.Composition.Server TransformedOwnContentBounds = default; else TransformedOwnContentBounds = - ownBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); + ownBounds.TransformToAABB(GlobalTransformMatrix); } if (_clipSizeDirty || positionChanged) { _transformedClipBounds = ClipToBounds ? new Rect(new Size(Size.X, Size.Y)) - .TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)) + .TransformToAABB(GlobalTransformMatrix) : null; _clipSizeDirty = false; @@ -249,7 +249,7 @@ namespace Avalonia.Rendering.Composition.Server /// public struct ReadbackData { - public Matrix4x4 Matrix; + public Matrix Matrix; public ulong Revision; public long TargetId; public bool Visible; diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index 6d6818256a..f4f1f3e8d9 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -33,7 +33,7 @@ namespace Avalonia.Rendering.Composition } } - internal Matrix4x4? TryGetServerGlobalTransform() + internal Matrix? TryGetServerGlobalTransform() { if (Root == null) return null; diff --git a/src/Avalonia.Base/Vector.cs b/src/Avalonia.Base/Vector.cs index 166ae6b93b..15722901a6 100644 --- a/src/Avalonia.Base/Vector.cs +++ b/src/Avalonia.Base/Vector.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Numerics; #if !BUILDTASK using Avalonia.Animation.Animators; #endif @@ -353,5 +354,52 @@ namespace Avalonia x = this._x; y = this._y; } + + internal Vector2 ToVector2() => new Vector2((float)X, (float)Y); + + internal Vector(Vector2 v) : this(v.X, v.Y) + { + + } + + /// + /// Returns a vector whose elements are the absolute values of each of the specified vector's elements. + /// + /// + public Vector Abs() => new(Math.Abs(X), Math.Abs(Y)); + + /// + /// Restricts a vector between a minimum and a maximum value. + /// + public static Vector Clamp(Vector value, Vector min, Vector max) => + Min(Max(value, min), max); + + /// + /// Returns a vector whose elements are the maximum of each of the pairs of elements in two specified vectors + /// + public static Vector Max(Vector left, Vector right) => + new(Math.Max(left.X, right.X), Math.Max(left.Y, right.Y)); + + /// + /// Returns a vector whose elements are the minimum of each of the pairs of elements in two specified vectors + /// + public static Vector Min(Vector left, Vector right) => + new(Math.Min(left.X, right.X), Math.Min(left.Y, right.Y)); + + /// + /// Computes the Euclidean distance between the two given points. + /// + public static double Distance(Vector value1, Vector value2) => Math.Sqrt(DistanceSquared(value1, value2)); + + /// + /// Returns the Euclidean distance squared between two specified points + /// + 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); } } diff --git a/src/Avalonia.Base/Vector3D.cs b/src/Avalonia.Base/Vector3D.cs new file mode 100644 index 0000000000..ec6463c987 --- /dev/null +++ b/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) +{ + /// + /// Parses a string. + /// + /// The string. + /// The . + 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); + + /// + /// Calculates the dot product of two vectors. + /// + public static double Dot(Vector3D vector1, Vector3D vector2) => + (vector1.X * vector2.X) + + (vector1.Y * vector2.Y) + + (vector1.Z * vector2.Z); + + /// + /// Adds the second to the first vector + /// + public static Vector3D Add(Vector3D left, Vector3D right) => + new Vector3D(left.X + right.X, left.Y + right.Y, left.Z + right.Z); + + /// + /// Adds the second to the first vector + /// + public static Vector3D operator +(Vector3D left, Vector3D right) => Add(left, right); + + /// + /// Subtracts the second from the first vector + /// + public static Vector3D Substract(Vector3D left, Vector3D right) => + new Vector3D(left.X - right.X, left.Y - right.Y, left.Z - right.Z); + + /// + /// Subtracts the second from the first vector + /// + public static Vector3D operator -(Vector3D left, Vector3D right) => Substract(left, right); + + /// + /// Negates the vector + /// + public static Vector3D operator -(Vector3D v) => new(-v.X, -v.Y, -v.Z); + + /// + /// Multiplies the first vector by the second. + /// + public static Vector3D Multiply(Vector3D left, Vector3D right) => + new(left.X * right.X, left.Y * right.Y, left.Z * right.Z); + + /// + /// Multiplies the vector by the given scalar. + /// + public static Vector3D Multiply(Vector3D left, double right) => + new(left.X * right, left.Y * right, left.Z * right); + + /// + /// Multiplies the vector by the given scalar. + /// + public static Vector3D operator *(Vector3D left, double right) => Multiply(left, right); + + /// + /// Divides the first vector by the second. + /// + public static Vector3D Divide(Vector3D left, Vector3D right) => + new(left.X / right.X, left.Y / right.Y, left.Z / right.Z); + + /// + /// Divides the vector by the given scalar. + /// + public static Vector3D Divide(Vector3D left, double right) => + new(left.X / right, left.Y / right, left.Z / right); + + /// Returns a vector whose elements are the absolute values of each of the specified vector's elements. + public Vector3D Abs() => new(Math.Abs(X), Math.Abs(Y), Math.Abs(Z)); + + /// + /// Restricts a vector between a minimum and a maximum value. + /// + public static Vector3D Clamp(Vector3D value, Vector3D min, Vector3D max) => + Min(Max(value, min), max); + + /// + /// Returns a vector whose elements are the maximum of each of the pairs of elements in two specified vectors + /// + 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)); + + /// + /// Returns a vector whose elements are the minimum of each of the pairs of elements in two specified vectors + /// + 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)); + + /// + /// Length of the vector. + /// + public double Length => Math.Sqrt(Dot(this, this)); + + /// + /// Returns a normalized version of this vector. + /// + public static Vector3D Normalize(Vector3D value) => Divide(value, value.Length); + + /// + /// Computes the squared Euclidean distance between the two given points. + /// + public static double DistanceSquared(Vector3D value1, Vector3D value2) + { + var difference = Vector3D.Substract(value1, value2); + return Dot(difference, difference); + } + + /// + /// Computes the Euclidean distance between the two given points. + /// + public static double Distance(Vector3D value1, Vector3D value2) => Math.Sqrt(DistanceSquared(value1, value2)); +} \ No newline at end of file diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index 97b2686584..05aea3808e 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -21,14 +21,14 @@ - - - - + + + + - - + + @@ -57,10 +57,13 @@ + + + diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs index 39ff8e3a92..406b4443a6 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -223,7 +223,7 @@ namespace Avalonia.Controls var visualizerVisual = ElementComposition.GetElementVisual(this); 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) { case RefreshVisualizerState.Idle: @@ -236,43 +236,43 @@ namespace Avalonia.Controls contentVisual.Opacity = MinimumIndicatorOpacity; contentVisual.RotationAngle = _startingRotationAngle; visualizerVisual.Offset = IsPullDirectionVertical ? - new Vector3(visualizerVisual.Offset.X, 0, 0) : - new Vector3(0, visualizerVisual.Offset.Y, 0); + new Vector3D(visualizerVisual.Offset.X, 0, 0) : + new Vector3D(0, visualizerVisual.Offset.Y, 0); _content.InvalidateMeasure(); break; case RefreshVisualizerState.Interacting: _played = false; contentVisual.Opacity = MinimumIndicatorOpacity; contentVisual.RotationAngle = (float)(_startingRotationAngle + _interactionRatio * 2 * Math.PI); - Vector3 offset = default; + Vector3D offset = default; 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 { - 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; visualizerVisual.Offset = IsPullDirectionVertical ? - new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : - new Vector3(offset.X, visualizerVisual.Offset.Y, 0); + new Vector3D(visualizerVisual.Offset.X, offset.Y, 0) : + new Vector3D(offset.X, visualizerVisual.Offset.Y, 0); break; case RefreshVisualizerState.Pending: contentVisual.Opacity = 1; contentVisual.RotationAngle = _startingRotationAngle + (float)(2 * Math.PI); if (IsPullDirectionVertical) { - offset = new Vector3(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); + offset = new Vector3D(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); } else { - offset = new Vector3((float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0); + offset = new Vector3D((float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0); } visual.Offset = offset; visualizerVisual.Offset = IsPullDirectionVertical ? - new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : - new Vector3(offset.X, visualizerVisual.Offset.Y, 0); + new Vector3D(visualizerVisual.Offset.X, offset.Y, 0) : + new Vector3D(offset.X, visualizerVisual.Offset.Y, 0); if (!_played) { @@ -301,18 +301,18 @@ namespace Avalonia.Controls * (IsPullDirectionFar ? -1f : 1f); 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 { - 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; - contentVisual.Offset += IsPullDirectionVertical ? new Vector3(0, (float)(translationRatio * root.Bounds.Height), 0) : - new Vector3((float)(translationRatio * root.Bounds.Width), 0, 0); + contentVisual.Offset += IsPullDirectionVertical ? new Vector3D(0, (translationRatio * root.Bounds.Height), 0) : + new Vector3D((translationRatio * root.Bounds.Width), 0, 0); visualizerVisual.Offset = IsPullDirectionVertical ? - new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : - new Vector3(offset.X, visualizerVisual.Offset.Y, 0); + new Vector3D(visualizerVisual.Offset.X, offset.Y, 0) : + new Vector3D(offset.X, visualizerVisual.Offset.Y, 0); break; case RefreshVisualizerState.Peeking: contentVisual.Opacity = 1; diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs index 83bf795b03..cefc2a4c06 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs @@ -105,7 +105,7 @@ namespace Avalonia.OpenGL.Controls } _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; ElementComposition.SetElementChildVisual(this, _visual); using (_resources.Context.MakeCurrent()) @@ -118,7 +118,7 @@ namespace Avalonia.OpenGL.Controls { 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(); } From e7ea247c01cce61b60caadfff378ee2dd7311c3c Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 13 Jun 2023 22:21:18 +0600 Subject: [PATCH 02/42] Remove some casts for RefreshVisualizer --- src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs index 406b4443a6..c49043ea46 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -263,11 +263,11 @@ namespace Avalonia.Controls contentVisual.RotationAngle = _startingRotationAngle + (float)(2 * Math.PI); if (IsPullDirectionVertical) { - offset = new Vector3D(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); + offset = new Vector3D(0, (_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); } else { - offset = new Vector3D((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; visualizerVisual.Offset = IsPullDirectionVertical ? From 8bac5f78ffb19963b0db9bf086a5d24d7887e781 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 16 Jun 2023 09:42:05 +0200 Subject: [PATCH 03/42] try debug issue --- .../Diagnostics/Views/PropertyValueEditorView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs index 7b58671996..2ac228ae4d 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs @@ -73,7 +73,7 @@ namespace Avalonia.Diagnostics.Views { Source = Property, Converter = converter ?? new ValueConverter(), - ConverterParameter = propertyType + ConverterParameter = readonlyProperty?.Name ?? valueProperty.Name, }).DisposeWith(_cleanup); if (readonlyProperty != null) From dcb46d4d9ac1468135bfaa41626d84224f7b8165 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 12 Jun 2023 08:51:06 +0000 Subject: [PATCH 04/42] report irregular snappoints in virtualizing stack panel based on itemsource count --- src/Avalonia.Controls/VirtualizingStackPanel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index b234686f5e..c262d416be 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -777,7 +777,7 @@ namespace Avalonia.Controls public IReadOnlyList GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment) { var snapPoints = new List(); - + var itemCount = ItemsControl?.ItemsSource?.Count() ?? 0; switch (orientation) { case Orientation.Horizontal: @@ -787,7 +787,7 @@ namespace Avalonia.Controls { var averageElementSize = EstimateElementSizeU(); double snapPoint = 0; - for (var i = 0; i < Items.Count; i++) + for (var i = 0; i < itemCount; i++) { var container = ContainerFromIndex(i); if (container != null) @@ -834,7 +834,7 @@ namespace Avalonia.Controls { var averageElementSize = EstimateElementSizeU(); double snapPoint = 0; - for (var i = 0; i < Items.Count; i++) + for (var i = 0; i < itemCount; i++) { var container = ContainerFromIndex(i); if (container != null) From 6b06e5c5fb9ba7a1ffa15d3e1b62f821cc28f45e Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 12 Jun 2023 11:43:09 +0000 Subject: [PATCH 05/42] remove IScrollSnapPointsInfo implementation from ItemsControl --- src/Avalonia.Controls/ItemsControl.cs | 86 +------------ .../Presenters/ItemsPresenter.cs | 120 +----------------- .../Presenters/ScrollContentPresenter.cs | 65 +++++++--- .../Controls/ItemsControl.xaml | 2 - .../Controls/ListBox.xaml | 2 - .../Controls/ItemsControl.xaml | 2 - .../Controls/ListBox.xaml | 2 - 7 files changed, 48 insertions(+), 231 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 06427991f8..83aa88a7b6 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls /// Displays a collection of items. /// [PseudoClasses(":empty", ":singleitem")] - public class ItemsControl : TemplatedControl, IChildIndexProvider, IScrollSnapPointsInfo + public class ItemsControl : TemplatedControl, IChildIndexProvider { /// /// The default value for the property. @@ -67,18 +67,6 @@ namespace Avalonia.Controls public static readonly StyledProperty DisplayMemberBindingProperty = AvaloniaProperty.Register(nameof(DisplayMemberBinding)); - /// - /// Defines the property. - /// - public static readonly StyledProperty AreHorizontalSnapPointsRegularProperty = - AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty AreVerticalSnapPointsRegularProperty = - AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular)); - /// /// Gets or sets the to use for binding to the display member of each item. /// @@ -249,64 +237,6 @@ namespace Avalonia.Controls /// public event EventHandler? ContainerClearing; - /// - public event EventHandler HorizontalSnapPointsChanged - { - add - { - if (_itemsPresenter != null) - { - _itemsPresenter.HorizontalSnapPointsChanged += value; - } - } - - remove - { - if (_itemsPresenter != null) - { - _itemsPresenter.HorizontalSnapPointsChanged -= value; - } - } - } - - /// - public event EventHandler VerticalSnapPointsChanged - { - add - { - if (_itemsPresenter != null) - { - _itemsPresenter.VerticalSnapPointsChanged += value; - } - } - - remove - { - if (_itemsPresenter != null) - { - _itemsPresenter.VerticalSnapPointsChanged -= value; - } - } - } - - /// - /// Gets or sets whether the horizontal snap points for the are equidistant from each other. - /// - public bool AreHorizontalSnapPointsRegular - { - get => GetValue(AreHorizontalSnapPointsRegularProperty); - set => SetValue(AreHorizontalSnapPointsRegularProperty, value); - } - - /// - /// Gets or sets whether the vertical snap points for the are equidistant from each other. - /// - public bool AreVerticalSnapPointsRegular - { - get => GetValue(AreVerticalSnapPointsRegularProperty); - set => SetValue(AreVerticalSnapPointsRegularProperty, value); - } - /// /// Gets a default recycle key that can be used when an supports /// a single container type. @@ -896,19 +826,5 @@ namespace Avalonia.Controls count = ItemsView.Count; return true; } - - /// - public IReadOnlyList GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment) - { - return _itemsPresenter?.GetIrregularSnapPoints(orientation, snapPointsAlignment) ?? new List(); - } - - /// - public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset) - { - offset = 0; - - return _itemsPresenter?.GetRegularSnapPoints(orientation, snapPointsAlignment, out offset) ?? 0; - } } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 9610be088b..63e512492e 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -11,7 +11,7 @@ namespace Avalonia.Controls.Presenters /// /// Presents items inside an . /// - public class ItemsPresenter : Control, ILogicalScrollable, IScrollSnapPointsInfo + public class ItemsPresenter : Control, ILogicalScrollable { /// /// Defines the property. @@ -21,37 +21,8 @@ namespace Avalonia.Controls.Presenters private PanelContainerGenerator? _generator; private ILogicalScrollable? _logicalScrollable; - private IScrollSnapPointsInfo? _scrollSnapPointsInfo; private EventHandler? _scrollInvalidated; - /// - /// Defines the property. - /// - public static readonly StyledProperty AreHorizontalSnapPointsRegularProperty = - AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty AreVerticalSnapPointsRegularProperty = - AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular)); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent HorizontalSnapPointsChangedEvent = - RoutedEvent.Register( - nameof(HorizontalSnapPointsChanged), - RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent VerticalSnapPointsChangedEvent = - RoutedEvent.Register( - nameof(VerticalSnapPointsChanged), - RoutingStrategies.Bubble); - event EventHandler? ILogicalScrollable.ScrollInvalidated { add => _scrollInvalidated += value; @@ -107,47 +78,12 @@ namespace Avalonia.Controls.Presenters } } - /// - /// Occurs when the measurements for horizontal snap points change. - /// - public event EventHandler? HorizontalSnapPointsChanged - { - add => AddHandler(HorizontalSnapPointsChangedEvent, value); - remove => RemoveHandler(HorizontalSnapPointsChangedEvent, value); - } - - /// - /// Occurs when the measurements for vertical snap points change. - /// - public event EventHandler? VerticalSnapPointsChanged - { - add => AddHandler(VerticalSnapPointsChangedEvent, value); - remove => RemoveHandler(VerticalSnapPointsChangedEvent, value); - } - bool ILogicalScrollable.IsLogicalScrollEnabled => _logicalScrollable?.IsLogicalScrollEnabled ?? false; Size ILogicalScrollable.ScrollSize => _logicalScrollable?.ScrollSize ?? default; Size ILogicalScrollable.PageScrollSize => _logicalScrollable?.PageScrollSize ?? default; Size IScrollable.Extent => _logicalScrollable?.Extent ?? default; Size IScrollable.Viewport => _logicalScrollable?.Viewport ?? default; - /// - /// Gets or sets whether the horizontal snap points for the are equidistant from each other. - /// - public bool AreHorizontalSnapPointsRegular - { - get { return GetValue(AreHorizontalSnapPointsRegularProperty); } - set { SetValue(AreHorizontalSnapPointsRegularProperty, value); } - } - - /// - /// Gets or sets whether the vertical snap points for the are equidistant from each other. - /// - public bool AreVerticalSnapPointsRegular - { - get { return GetValue(AreVerticalSnapPointsRegularProperty); } - set { SetValue(AreVerticalSnapPointsRegularProperty, value); } - } public override sealed void ApplyTemplate() { @@ -167,36 +103,14 @@ namespace Avalonia.Controls.Presenters Panel.TemplatedParent = TemplatedParent; Panel.IsItemsHost = true; - _scrollSnapPointsInfo = Panel as IScrollSnapPointsInfo; LogicalChildren.Add(Panel); VisualChildren.Add(Panel); - if (_scrollSnapPointsInfo != null) - { - _scrollSnapPointsInfo.AreVerticalSnapPointsRegular = AreVerticalSnapPointsRegular; - _scrollSnapPointsInfo.AreHorizontalSnapPointsRegular = AreHorizontalSnapPointsRegular; - } - if (Panel is VirtualizingPanel v) v.Attach(ItemsControl); else 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; if (_logicalScrollable is not null) @@ -240,16 +154,6 @@ namespace Avalonia.Controls.Presenters ResetState(); 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() @@ -303,27 +207,5 @@ namespace Avalonia.Controls.Presenters } private void OnLogicalScrollInvalidated(object? sender, EventArgs e) => _scrollInvalidated?.Invoke(this, e); - - public IReadOnlyList GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment) - { - if(Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) - { - return scrollSnapPointsInfo.GetIrregularSnapPoints(orientation, snapPointsAlignment); - } - - return new List(); - } - - 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; - } } } diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 1c9dd79b13..701e0e4ead 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -7,6 +7,7 @@ using Avalonia.Input.GestureRecognizers; using Avalonia.Utilities; using Avalonia.VisualTree; using System.Linq; +using Avalonia.Interactivity; namespace Avalonia.Controls.Presenters { @@ -99,6 +100,7 @@ namespace Avalonia.Controls.Presenters private double _horizontalSnapPointOffset; private CompositeDisposable? _ownerSubscriptions; private ScrollViewer? _owner; + private IScrollSnapPointsInfo? _scrollSnapPointsInfo; /// /// Initializes static members of the class. @@ -570,7 +572,12 @@ namespace Avalonia.Controls.Presenters 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; if (_scrollGestureSnapPoints == null) @@ -676,22 +683,6 @@ namespace Avalonia.Controls.Presenters _owner?.SetCurrentValue(OffsetProperty, change.GetNewValue()); } - 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) { ChildChanged(change); @@ -875,7 +866,9 @@ namespace Avalonia.Controls.Presenters private void UpdateSnapPoints() { - if (Content is IScrollSnapPointsInfo scrollSnapPointsInfo) + var scrollable = GetScrollSnapPointsInfo(Content); + + if (scrollable is IScrollSnapPointsInfo scrollSnapPointsInfo) { _areVerticalSnapPointsRegular = scrollSnapPointsInfo.AreVerticalSnapPointsRegular; _areHorizontalSnapPointsRegular = scrollSnapPointsInfo.AreHorizontalSnapPointsRegular; @@ -910,7 +903,9 @@ namespace Avalonia.Controls.Presenters private Vector SnapOffset(Vector offset) { - if(Content is not IScrollSnapPointsInfo) + var scrollable = GetScrollSnapPointsInfo(Content); + + if(scrollable is null) return offset; var diff = GetAlignedDiff(); @@ -1012,5 +1007,37 @@ namespace Avalonia.Controls.Presenters } 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; + } } } diff --git a/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml b/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml index 91f9536847..07327b530c 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml @@ -10,8 +10,6 @@ CornerRadius="{TemplateBinding CornerRadius}" Padding="{TemplateBinding Padding}"> diff --git a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml index 2dde18a523..3757f685bf 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml @@ -40,8 +40,6 @@ AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}" BringIntoViewOnFocusChange="{TemplateBinding (ScrollViewer.BringIntoViewOnFocusChange)}"> diff --git a/src/Avalonia.Themes.Simple/Controls/ItemsControl.xaml b/src/Avalonia.Themes.Simple/Controls/ItemsControl.xaml index ba6b37fd90..e4f4bbc4a2 100644 --- a/src/Avalonia.Themes.Simple/Controls/ItemsControl.xaml +++ b/src/Avalonia.Themes.Simple/Controls/ItemsControl.xaml @@ -11,8 +11,6 @@ BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}"> diff --git a/src/Avalonia.Themes.Simple/Controls/ListBox.xaml b/src/Avalonia.Themes.Simple/Controls/ListBox.xaml index eeefa498a7..73c27db756 100644 --- a/src/Avalonia.Themes.Simple/Controls/ListBox.xaml +++ b/src/Avalonia.Themes.Simple/Controls/ListBox.xaml @@ -27,8 +27,6 @@ HorizontalSnapPointsType="{TemplateBinding (ScrollViewer.HorizontalSnapPointsType)}"> From fbe50fad7bd84e97f37847309f47eca2bc39690b Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 14 Jun 2023 08:11:49 +0000 Subject: [PATCH 06/42] ensure snap points are updated on first measure. --- .../Presenters/ScrollContentPresenter.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 701e0e4ead..bd694b200c 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -101,6 +101,7 @@ namespace Avalonia.Controls.Presenters private CompositeDisposable? _ownerSubscriptions; private ScrollViewer? _owner; private IScrollSnapPointsInfo? _scrollSnapPointsInfo; + private bool _isSnapPointsUpdated; /// /// Initializes static members of the class. @@ -379,6 +380,13 @@ namespace Avalonia.Controls.Presenters CanVerticallyScroll ? double.PositiveInfinity : availableSize.Height); Child.Measure(constraint); + + if (!_isSnapPointsUpdated) + { + _isSnapPointsUpdated = true; + UpdateSnapPoints(); + } + return Child.DesiredSize.Constrain(availableSize); } From 41dd04e177e5ffbb5352a60503ba4f064b8299dd Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Jun 2023 11:36:11 +0200 Subject: [PATCH 07/42] Add failing test for #11641. --- .../ListBoxTests.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index aeebfabd41..03497c335d 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; @@ -1223,6 +1224,29 @@ namespace Avalonia.Controls.UnitTests 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(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) { target.RaiseEvent(new KeyEventArgs @@ -1252,5 +1276,32 @@ namespace Avalonia.Controls.UnitTests public event NotifyCollectionChangedEventHandler CollectionChanged; } + + private class DataVirtualizingList : IList + { + private readonly List _inner = new(Enumerable.Repeat(null, 100)); + + public object this[int index] + { + get => _inner[index] = $"Item{index}"; + set => throw new NotSupportedException(); + } + + public IEnumerable 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(); + } } } From 8f97ae8bbcd4dae8f5c31cf4fd97e4c3c64e5080 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 14 Jun 2023 14:00:50 +0000 Subject: [PATCH 08/42] lazy get snapppoints using realized elements in VirtualizingStackPanel --- .../Utils/VirtualizingSnapPointsList.cs | 191 ++++++++++++++++++ .../VirtualizingStackPanel.cs | 103 +--------- 2 files changed, 194 insertions(+), 100 deletions(-) create mode 100644 src/Avalonia.Controls/Utils/VirtualizingSnapPointsList.cs diff --git a/src/Avalonia.Controls/Utils/VirtualizingSnapPointsList.cs b/src/Avalonia.Controls/Utils/VirtualizingSnapPointsList.cs new file mode 100644 index 0000000000..afa084bd9b --- /dev/null +++ b/src/Avalonia.Controls/Utils/VirtualizingSnapPointsList.cs @@ -0,0 +1,191 @@ +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 + { + 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 GetEnumerator() + { + return new VirtualizingEnumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return new VirtualizingEnumerator(this); + } + + private class VirtualizingEnumerator : IEnumerator + { + private readonly VirtualizingSnapPointsList _list; + private int _index; + + public VirtualizingEnumerator(VirtualizingSnapPointsList list) + { + _list = list; + _index = -1; + } + + public double Current => _list[_index]; + + object IEnumerator.Current => Current; + + public void Dispose() + { + _index = 0; + } + + public bool MoveNext() + { + _index++; + + return _index < _list.Count; + } + + public void Reset() + { + _index = -1; + } + } + } +} diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index c262d416be..6a3c7e17ee 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -776,107 +776,10 @@ namespace Avalonia.Controls /// public IReadOnlyList GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment) { - var snapPoints = new List(); - var itemCount = ItemsControl?.ItemsSource?.Count() ?? 0; - switch (orientation) - { - case Orientation.Horizontal: - if (AreHorizontalSnapPointsRegular) - throw new InvalidOperationException(); - if (Orientation == Orientation.Horizontal) - { - var averageElementSize = EstimateElementSizeU(); - double snapPoint = 0; - for (var i = 0; i < itemCount; 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 < itemCount; 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; - } + if(_realizedElements == null) + return new List(); - return snapPoints; + return new VirtualizingSnapPointsList(_realizedElements, ItemsControl?.ItemsSource?.Count() ?? 0, orientation, Orientation, snapPointsAlignment, EstimateElementSizeU()); } /// From 16c2bb7cd58218136318a609f7f2c8cac882b14d Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 16 Jun 2023 08:51:21 +0000 Subject: [PATCH 09/42] drop virtualizing enumerator class --- .../Utils/VirtualizingSnapPointsList.cs | 41 ++----------------- 1 file changed, 3 insertions(+), 38 deletions(-) diff --git a/src/Avalonia.Controls/Utils/VirtualizingSnapPointsList.cs b/src/Avalonia.Controls/Utils/VirtualizingSnapPointsList.cs index afa084bd9b..f92756c59b 100644 --- a/src/Avalonia.Controls/Utils/VirtualizingSnapPointsList.cs +++ b/src/Avalonia.Controls/Utils/VirtualizingSnapPointsList.cs @@ -147,45 +147,10 @@ namespace Avalonia.Controls.Utils public IEnumerator GetEnumerator() { - return new VirtualizingEnumerator(this); + for (var i = 0; i < Count; i++) + yield return this[i]; } - IEnumerator IEnumerable.GetEnumerator() - { - return new VirtualizingEnumerator(this); - } - - private class VirtualizingEnumerator : IEnumerator - { - private readonly VirtualizingSnapPointsList _list; - private int _index; - - public VirtualizingEnumerator(VirtualizingSnapPointsList list) - { - _list = list; - _index = -1; - } - - public double Current => _list[_index]; - - object IEnumerator.Current => Current; - - public void Dispose() - { - _index = 0; - } - - public bool MoveNext() - { - _index++; - - return _index < _list.Count; - } - - public void Reset() - { - _index = -1; - } - } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } From ad8d0eeafca1f6eec316136026b899a04c543fdc Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 16 Jun 2023 11:31:47 +0200 Subject: [PATCH 10/42] fix: DevTools InvalidCastException --- .../Views/PropertyValueEditorView.cs | 104 ++++++++++++------ 1 file changed, 71 insertions(+), 33 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs index 2ac228ae4d..01ddf82c1e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs @@ -57,42 +57,11 @@ namespace Avalonia.Diagnostics.Views if (Property?.PropertyType is not { } propertyType) return null; - TControl CreateControl(AvaloniaProperty valueProperty, - IValueConverter? converter = null, - Action? 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 = readonlyProperty?.Name ?? valueProperty.Name, - }).DisposeWith(_cleanup); - - if (readonlyProperty != null) - { - control[readonlyProperty] = Property.IsReadonly; - } - else - { - control.IsEnabled = !Property.IsReadonly; - } - - return control; - } - if (propertyType == typeof(bool)) return CreateControl(ToggleButton.IsCheckedProperty); //TODO: Infinity, NaN not working with NumericUpDown - if (propertyType.IsPrimitive && propertyType != typeof(float) && propertyType != typeof(double)) + if (IsValidNumeric(propertyType)) return CreateControl( NumericUpDown.ValueProperty, new ValueToDecimalConverter(), @@ -139,7 +108,8 @@ namespace Avalonia.Diagnostics.Views ColorView.ColorProperty, new Binding(nameof(Property.Value), BindingMode.TwoWay) { - Source = Property, Converter = Color2Brush + Source = Property, + Converter = Color2Brush }) .DisposeWith(_cleanup); @@ -261,6 +231,74 @@ namespace Avalonia.Diagnostics.Views } return tb; + + TControl CreateControl(AvaloniaProperty valueProperty + , IValueConverter? converter = null + , Action? 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) + { + 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 From b253e205b0c756a69abd82ef92c8db2df59994c4 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 16 Jun 2023 16:44:08 +0200 Subject: [PATCH 11/42] fix: formatting --- .../Diagnostics/Views/PropertyValueEditorView.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs index 01ddf82c1e..aec0cb37bc 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs @@ -232,11 +232,11 @@ namespace Avalonia.Diagnostics.Views return tb; - TControl CreateControl(AvaloniaProperty valueProperty - , IValueConverter? converter = null - , Action? init = null - , AvaloniaProperty? readonlyProperty = null) - where TControl : Control, new() + TControl CreateControl(AvaloniaProperty valueProperty, + IValueConverter? converter = null, + Action? init = null, + AvaloniaProperty? readonlyProperty = null) + where TControl : Control, new() { var control = new TControl(); var bindingMode = Property.IsReadonly ? BindingMode.OneWay : BindingMode.TwoWay; From 718911ce272b4b7a0cc658cc01cef72c1d1b442f Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 16 Jun 2023 17:24:29 +0200 Subject: [PATCH 12/42] fix(BoxShadows): ToString() --- src/Avalonia.Base/Media/BoxShadow.cs | 24 +++++++++++-------- src/Avalonia.Base/Media/BoxShadows.cs | 10 ++++---- .../Media/BoxShadowTests.cs | 8 +++++++ 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Base/Media/BoxShadow.cs b/src/Avalonia.Base/Media/BoxShadow.cs index 91529353ed..a0e1873d30 100644 --- a/src/Avalonia.Base/Media/BoxShadow.cs +++ b/src/Avalonia.Base/Media/BoxShadow.cs @@ -73,40 +73,44 @@ namespace Avalonia.Media public override string ToString() { var sb = StringBuilderCache.Acquire(); + ToString(sb); + return StringBuilderCache.GetStringAndRelease(sb); + } + internal void ToString(StringBuilder sb) + { if (this == default) { - return "none"; + sb.Append("none"); + return; } if (IsInset) { - sb.Append("inset"); + sb.Append("inset "); } if (OffsetX != 0.0) { - sb.AppendFormat(" {0}", OffsetX.ToString(CultureInfo.InvariantCulture)); + sb.AppendFormat("{0} ", OffsetX.ToString(CultureInfo.InvariantCulture)); } if (OffsetY != 0.0) { - sb.AppendFormat(" {0}", OffsetY.ToString(CultureInfo.InvariantCulture)); + sb.AppendFormat("{0} ", OffsetY.ToString(CultureInfo.InvariantCulture)); } - + if (Blur != 0.0) { - sb.AppendFormat(" {0}", Blur.ToString(CultureInfo.InvariantCulture)); + sb.AppendFormat("{0} ", Blur.ToString(CultureInfo.InvariantCulture)); } if (Spread != 0.0) { - sb.AppendFormat(" {0}", Spread.ToString(CultureInfo.InvariantCulture)); + sb.AppendFormat("{0} ", Spread.ToString(CultureInfo.InvariantCulture)); } - sb.AppendFormat(" {0}", Color.ToString()); - - return StringBuilderCache.GetStringAndRelease(sb); + sb.AppendFormat("{0}", Color.ToString()); } public static unsafe BoxShadow Parse(string s) diff --git a/src/Avalonia.Base/Media/BoxShadows.cs b/src/Avalonia.Base/Media/BoxShadows.cs index 385f73f703..3ddd8e0a95 100644 --- a/src/Avalonia.Base/Media/BoxShadows.cs +++ b/src/Avalonia.Base/Media/BoxShadows.cs @@ -39,20 +39,20 @@ namespace Avalonia.Media public override string ToString() { - var sb = StringBuilderCache.Acquire(); - if (Count == 0) { return "none"; } + var sb = StringBuilderCache.Acquire(); 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); - } [EditorBrowsable(EditorBrowsableState.Never)] diff --git a/tests/Avalonia.Base.UnitTests/Media/BoxShadowTests.cs b/tests/Avalonia.Base.UnitTests/Media/BoxShadowTests.cs index 5f1860d177..c9092bb133 100644 --- a/tests/Avalonia.Base.UnitTests/Media/BoxShadowTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/BoxShadowTests.cs @@ -41,5 +41,13 @@ namespace Avalonia.Base.UnitTests.Media 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); + } } } From c0e7dc8ecfe26c8007483bf1898b1a3b7c64d0df Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sat, 17 Jun 2023 01:12:15 +0200 Subject: [PATCH 13/42] Fixed style activator not publishing changes Allows style animations to correctly stop. Also fixed NthChildActivator when ChildIndexChanged and Index == -1. --- .../Styling/Activators/NthChildActivator.cs | 30 ++++---- .../Styling/Activators/StyleActivatorBase.cs | 11 ++- .../Styling/StyleTests.cs | 68 ++++++++++++++++++- 3 files changed, 89 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs index 8bdcec2e53..8fe0bb2537 100644 --- a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs @@ -1,6 +1,4 @@ -#nullable enable -using System; -using Avalonia.LogicalTree; +using Avalonia.LogicalTree; namespace Avalonia.Styling.Activators { @@ -14,7 +12,7 @@ namespace Avalonia.Styling.Activators private readonly int _step; private readonly int _offset; private readonly bool _reversed; - private int _index = -1; + private int? _index; public NthChildActivator( ILogical control, @@ -30,7 +28,7 @@ namespace Avalonia.Styling.Activators 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; } @@ -50,8 +48,7 @@ namespace Avalonia.Styling.Activators // 1. Subscribed child index was changed // 2. Child indexes were reset // 3. We're a reversed (nth-last-child) selector and total count has changed - if ((e.Child == _control || e.Action == ChildIndexChangedAction.ChildIndexesReset) || - (_reversed && e.Action == ChildIndexChangedAction.TotalCountChanged)) + switch (e.Action) { // 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 @@ -65,16 +62,17 @@ namespace Avalonia.Styling.Activators // 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 // GetChildIndex may not return the correct index as the element isn't yet realized. - _index = e.Index; - ReevaluateIsActive(); - _index = -1; + case ChildIndexChangedAction.ChildIndexChanged when e.Child == _control: + _index = e.Index; + 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(); - } } } diff --git a/src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs b/src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs index 06848a7524..3dbf927b86 100644 --- a/src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs +++ b/src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs @@ -6,9 +6,14 @@ namespace Avalonia.Styling.Activators internal abstract class StyleActivatorBase : IStyleActivator { 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; @@ -63,7 +68,7 @@ namespace Avalonia.Styling.Activators /// protected bool ReevaluateIsActive() { - var value = EvaluateIsActive(); + var value = GetIsActive(); if (value != _value) { diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs b/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs index ca6d1e5326..52bee42d9d 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs @@ -5,7 +5,7 @@ using Avalonia.Base.UnitTests.Animation; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Data; -using Avalonia.PropertyStore; +using Avalonia.Media; using Avalonia.Styling; using Avalonia.UnitTests; using Moq; @@ -963,6 +963,72 @@ namespace Avalonia.Base.UnitTests.Styling 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().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().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 { public static readonly StyledProperty FooProperty = From bbade94d98e417fcec0d207db66807ed403b111d Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sun, 18 Jun 2023 14:38:53 +0800 Subject: [PATCH 14/42] add new unit tests for fillmodes/delay behavior --- .../Animation/AnimationIterationTests.cs | 84 ++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs index dde59365a1..ca76c19de8 100644 --- a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs @@ -71,11 +71,14 @@ namespace Avalonia.Base.UnitTests.Animation var clock = new TestClock(); var animationRun = animation.RunAsync(border, clock); + border.Measure(Size.Infinity); + border.Arrange(new Rect(border.DesiredSize)); + clock.Step(TimeSpan.Zero); // Initial Delay. - clock.Step(TimeSpan.FromSeconds(1)); - Assert.Equal(border.Width, 0d); + clock.Step(TimeSpan.FromSeconds(0)); + Assert.Equal(100d, border.Width); clock.Step(TimeSpan.FromSeconds(6)); @@ -126,7 +129,84 @@ namespace Avalonia.Base.UnitTests.Animation clock.Step(TimeSpan.FromSeconds(0.100d)); 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] public void Dispose_Subscription_Should_Stop_Animation() { From 634ccedd0c7ead5e31aa50ee0b2a1ba82380215e Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sun, 18 Jun 2023 14:40:26 +0800 Subject: [PATCH 15/42] Fix #11819 --- .../Animation/AnimationInstance`1.cs | 119 +++++++++--------- 1 file changed, 62 insertions(+), 57 deletions(-) diff --git a/src/Avalonia.Base/Animation/AnimationInstance`1.cs b/src/Avalonia.Base/Animation/AnimationInstance`1.cs index 92d2c2c8b5..9d4575b9ad 100644 --- a/src/Avalonia.Base/Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Base/Animation/AnimationInstance`1.cs @@ -58,6 +58,9 @@ namespace Avalonia.Animation if (_animation.Duration < TimeSpan.Zero) throw new InvalidOperationException("Duration value cannot be negative."); + if (_animation.Delay < TimeSpan.Zero) + throw new InvalidOperationException("Delay value cannot be negative."); + _easeFunc = _animation.Easing; _speedRatioConv = 1d / _animation.SpeedRatio; @@ -151,72 +154,74 @@ namespace Avalonia.Animation var iterDelay = _iterationDelay.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(); + 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 iterationTime = iterDuration + iterDelay; - var opsTime = indexTime - initDelay; - var playbackTime = opsTime % iterationTime; + var easedTime = _easeFunc!.Ease(_playbackReversed ? 0.0 : 1.0); + _lastInterpValue = _interpolator(easedTime, _neutralValue); + DoComplete(); + } - _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 - // 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(); - } + // Check if normalized time needs to be reversed according to PlaybackDirection - if (playbackTime <= iterDuration) - { - // 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) + switch (_playbackDirection) { - // The last iteration's trailing delay should be skipped. - if (_currentIteration + 1 < _iterationCount) - DoDelay(); - else - DoComplete(); + 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. + if (_currentIteration + 1 < _iterationCount) + DoDelay(); + else + DoComplete(); } } From 4e642b3e8cfd8dbde390494c5f2da30e46212920 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 18 Jun 2023 10:59:38 -0400 Subject: [PATCH 16/42] Add OnSizeChanged method to Control --- src/Avalonia.Controls/Control.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index ae7d4f3fb4..885b3f302e 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -357,6 +357,15 @@ namespace Avalonia.Controls RaiseEvent(eventArgs); } + /// + /// Invoked just before the event. + /// + /// The event args. + protected virtual void OnSizeChanged(SizeChangedEventArgs e) + { + RaiseEvent(e); + } + /// protected sealed override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) { @@ -435,6 +444,10 @@ namespace Avalonia.Controls } } + /// + /// Returns a new, type-specific implementation for the control. + /// + /// The type-specific implementation. protected virtual AutomationPeer OnCreateAutomationPeer() { return new NoneAutomationPeer(this); @@ -459,6 +472,7 @@ namespace Avalonia.Controls return _automationPeer; } + /// protected override void OnPointerReleased(PointerReleasedEventArgs e) { base.OnPointerReleased(e); @@ -473,6 +487,7 @@ namespace Avalonia.Controls } } + /// protected override void OnKeyUp(KeyEventArgs e) { base.OnKeyUp(e); @@ -531,7 +546,7 @@ namespace Avalonia.Controls previousSize: new Size(oldValue.Width, oldValue.Height), newSize: new Size(newValue.Width, newValue.Height)); - RaiseEvent(sizeChangedEventArgs); + OnSizeChanged(sizeChangedEventArgs); } } } From 85390a1812f5fedd19b64ff64ce5bd794dba31cd Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 18 Jun 2023 11:19:01 -0400 Subject: [PATCH 17/42] Add EventArgs parameter to OnLoaded/Unloaded methods --- .../AppWithoutLifetime/MainWindow.axaml.cs | 5 +++-- samples/AppWithoutLifetime/Sub.axaml.cs | 6 ++++-- samples/SafeAreaDemo/Views/MainView.xaml.cs | 6 ++++-- src/Avalonia.Controls/Control.cs | 20 +++++++++---------- src/Avalonia.Controls/ToggleSwitch.cs | 6 ++++-- 5 files changed, 25 insertions(+), 18 deletions(-) diff --git a/samples/AppWithoutLifetime/MainWindow.axaml.cs b/samples/AppWithoutLifetime/MainWindow.axaml.cs index d27d5d5653..df3ed3cea0 100644 --- a/samples/AppWithoutLifetime/MainWindow.axaml.cs +++ b/samples/AppWithoutLifetime/MainWindow.axaml.cs @@ -17,10 +17,11 @@ public partial class MainWindow : Window AvaloniaXamlLoader.Load(this); } - protected override void OnLoaded() + /// + protected override void OnLoaded(RoutedEventArgs e) { this.AttachDevTools(); - base.OnLoaded(); + base.OnLoaded(e); } public void Open(object sender, RoutedEventArgs e) diff --git a/samples/AppWithoutLifetime/Sub.axaml.cs b/samples/AppWithoutLifetime/Sub.axaml.cs index 50c770b3a2..3a7ce787bc 100644 --- a/samples/AppWithoutLifetime/Sub.axaml.cs +++ b/samples/AppWithoutLifetime/Sub.axaml.cs @@ -1,5 +1,6 @@ using Avalonia; using Avalonia.Controls; +using Avalonia.Interactivity; using Avalonia.Markup.Xaml; namespace AppWithoutLifetime; @@ -16,9 +17,10 @@ public partial class Sub : Window AvaloniaXamlLoader.Load(this); } - protected override void OnLoaded() + /// + protected override void OnLoaded(RoutedEventArgs e) { this.AttachDevTools(); - base.OnLoaded(); + base.OnLoaded(e); } } diff --git a/samples/SafeAreaDemo/Views/MainView.xaml.cs b/samples/SafeAreaDemo/Views/MainView.xaml.cs index 2b651225e7..4b8c5e5f15 100644 --- a/samples/SafeAreaDemo/Views/MainView.xaml.cs +++ b/samples/SafeAreaDemo/Views/MainView.xaml.cs @@ -1,4 +1,5 @@ using Avalonia.Controls; +using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using SafeAreaDemo.ViewModels; @@ -11,9 +12,10 @@ namespace SafeAreaDemo.Views AvaloniaXamlLoader.Load(this); } - protected override void OnLoaded() + /// + protected override void OnLoaded(RoutedEventArgs e) { - base.OnLoaded(); + base.OnLoaded(e); var insetsManager = TopLevel.GetTopLevel(this)?.InsetsManager; if (insetsManager != null && DataContext is MainViewModel viewModel) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index ae7d4f3fb4..6c45374c21 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -316,7 +316,8 @@ namespace Avalonia.Controls ((ILogical)this).IsAttachedToLogicalTree) { _isLoaded = true; - OnLoaded(); + + OnLoaded(new RoutedEventArgs(LoadedEvent, this)); } } @@ -333,28 +334,27 @@ namespace Avalonia.Controls _loadedQueue.Remove(this); _isLoaded = false; - OnUnloaded(); + + OnUnloaded(new RoutedEventArgs(UnloadedEvent, this)); } } /// /// Invoked just before the event. /// - protected virtual void OnLoaded() + /// The event args. + protected virtual void OnLoaded(RoutedEventArgs e) { - var eventArgs = new RoutedEventArgs(LoadedEvent); - eventArgs.Source = null; - RaiseEvent(eventArgs); + RaiseEvent(e); } /// /// Invoked just before the event. /// - protected virtual void OnUnloaded() + /// The event args. + protected virtual void OnUnloaded(RoutedEventArgs e) { - var eventArgs = new RoutedEventArgs(UnloadedEvent); - eventArgs.Source = null; - RaiseEvent(eventArgs); + RaiseEvent(e); } /// diff --git a/src/Avalonia.Controls/ToggleSwitch.cs b/src/Avalonia.Controls/ToggleSwitch.cs index 48b068d324..dc6b946381 100644 --- a/src/Avalonia.Controls/ToggleSwitch.cs +++ b/src/Avalonia.Controls/ToggleSwitch.cs @@ -3,6 +3,7 @@ using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Interactivity; using Avalonia.LogicalTree; namespace Avalonia.Controls @@ -200,9 +201,10 @@ namespace Avalonia.Controls } } - protected override void OnLoaded() + /// + protected override void OnLoaded(RoutedEventArgs e) { - base.OnLoaded(); + base.OnLoaded(e); UpdateKnobTransitions(); } From 34ba4c9c823188b01713c69d34fe43fae9af768a Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 15 Jun 2023 12:00:50 +0200 Subject: [PATCH 18/42] Actually invalidate menuitems when it got attached to the visual tree --- src/Avalonia.Controls/MenuItem.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 1bb53f90a8..08c36122ea 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -394,6 +394,13 @@ namespace Avalonia.Controls _isEmbeddedInMenu = parent?.FindLogicalAncestorOfType(true) != null; } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + TryUpdateCanExecute(); + } + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { // This will cause the hotkey manager to dispose the observer and the reference to this control From e095ea012b6b89b37f855734d021ec59dad45db3 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 15 Jun 2023 12:20:07 +0200 Subject: [PATCH 19/42] Add missing --- src/Avalonia.Controls/MenuItem.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 08c36122ea..40d6b6b0f4 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -394,6 +394,7 @@ namespace Avalonia.Controls _isEmbeddedInMenu = parent?.FindLogicalAncestorOfType(true) != null; } + /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); From ebb9bf5052ed5fb5b6d436edb4bf49a07a24eb8c Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 16 Jun 2023 14:48:49 +0200 Subject: [PATCH 20/42] fix unit tests --- tests/Avalonia.Controls.UnitTests/MenuItemTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index 08aedceac3..92a565490b 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -209,13 +209,13 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, canExecuteCallCount); 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(); - Assert.Equal(3, canExecuteCallCount); + Assert.Equal(4, canExecuteCallCount); target.CommandParameter = true; - Assert.Equal(4, canExecuteCallCount); + Assert.Equal(5, canExecuteCallCount); } } @@ -249,13 +249,13 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, canExecuteCallCount); 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(); - Assert.Equal(2, canExecuteCallCount); + Assert.Equal(3, canExecuteCallCount); target.CommandParameter = true; - Assert.Equal(3, canExecuteCallCount); + Assert.Equal(4, canExecuteCallCount); } } From 97d595a74b54455a06992ae98e1aa89de0ff2668 Mon Sep 17 00:00:00 2001 From: enycw001 <3189263@qq.com> Date: Mon, 19 Jun 2023 18:08:28 +0800 Subject: [PATCH 21/42] fix_patterns --- .../Internal/ManagedFileChooserFilterViewModel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs index 8389a25386..8e24da97a4 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs @@ -18,9 +18,9 @@ namespace Avalonia.Dialogs.Internal return; } - _patterns = filter.Patterns? - .Select(e => new Regex(Regex.Escape(e).Replace(@"\*", ".*").Replace(@"\?", "."), RegexOptions.Singleline | RegexOptions.IgnoreCase)) - .ToArray(); + _patterns = filter.Patterns? + .Select(e => new Regex("^" + Regex.Escape(e).Replace("\\*", ".*") + "$", RegexOptions.Singleline | RegexOptions.IgnoreCase)) + .ToArray(); } public bool Match(string filename) From 7c2062c102b50934fb45068637cfef865467028b Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sun, 18 Jun 2023 19:15:59 +0200 Subject: [PATCH 22/42] Reset LayoutTransformControl matrix when transform is removed --- .../LayoutTransformControl.cs | 15 +++++++++------ .../LayoutTransformControlTests.cs | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index 06069a897e..5f8d4a561d 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -215,7 +215,7 @@ namespace Avalonia.Controls /// /// Transformation matrix corresponding to _matrixTransform. /// - private Matrix _transformation; + private Matrix _transformation = Matrix.Identity; private IDisposable? _transformChangedEvent; /// @@ -256,13 +256,16 @@ namespace Avalonia.Controls /// private void ApplyLayoutTransform() { - if (LayoutTransform == null) - return; - // 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 InvalidateMeasure(); diff --git a/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs b/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs index 60139c2881..c8a5af05a4 100644 --- a/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs @@ -169,6 +169,25 @@ namespace Avalonia.Controls.UnitTests 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] public void Should_Generate_RotateTransform_90_degrees() { From 112648c1a8bb0de8a5f17a3175b66f0719e76fd7 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 23 May 2023 16:04:39 +0000 Subject: [PATCH 23/42] drop splash screen activity --- .../ControlCatalog.Android.csproj | 9 ++ .../ControlCatalog.Android/MainActivity.cs | 12 ++- .../ControlCatalog.Android/SplashActivity.cs | 35 -------- samples/MobileSandbox.Android/MainActivity.cs | 5 +- .../MobileSandbox.Android.csproj | 10 +++ .../MobileSandbox.Android/SplashActivity.cs | 17 ---- .../AvaloniaMainActivity.App.cs | 60 +++++++++++++ .../Avalonia.Android/AvaloniaMainActivity.cs | 88 +++++++------------ .../AvaloniaSplashActivity.cs | 40 --------- 9 files changed, 125 insertions(+), 151 deletions(-) delete mode 100644 samples/ControlCatalog.Android/SplashActivity.cs delete mode 100644 samples/MobileSandbox.Android/SplashActivity.cs create mode 100644 src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs delete mode 100644 src/Android/Avalonia.Android/AvaloniaSplashActivity.cs diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index a43ea4539a..f668067f80 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -10,6 +10,11 @@ apk true + + + + + Resources\drawable\Icon.png @@ -37,6 +42,10 @@ + + + + diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs index 486d14661e..9b5b06e5fb 100644 --- a/samples/ControlCatalog.Android/MainActivity.cs +++ b/samples/ControlCatalog.Android/MainActivity.cs @@ -5,8 +5,16 @@ using Avalonia.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)] - public class MainActivity : AvaloniaMainActivity + [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] + public class MainActivity : AvaloniaMainActivity { + protected override Avalonia.AppBuilder CustomizeAppBuilder(Avalonia.AppBuilder builder) + { + return base.CustomizeAppBuilder(builder) + .AfterSetup(_ => + { + Pages.EmbedSample.Implementation = new EmbedSampleAndroid(); + }); + } } } diff --git a/samples/ControlCatalog.Android/SplashActivity.cs b/samples/ControlCatalog.Android/SplashActivity.cs deleted file mode 100644 index a0b68b129b..0000000000 --- a/samples/ControlCatalog.Android/SplashActivity.cs +++ /dev/null @@ -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 - { - 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(); - } - } -} diff --git a/samples/MobileSandbox.Android/MainActivity.cs b/samples/MobileSandbox.Android/MainActivity.cs index d65f0dec92..807afb0bd0 100644 --- a/samples/MobileSandbox.Android/MainActivity.cs +++ b/samples/MobileSandbox.Android/MainActivity.cs @@ -1,12 +1,11 @@ using Android.App; using Android.Content.PM; -using Avalonia; using Avalonia.Android; namespace MobileSandbox.Android { - [Activity(Label = "MobileSandbox.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] - public class MainActivity : AvaloniaMainActivity + [Activity(Label = "MobileSandbox.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] + public class MainActivity : AvaloniaMainActivity { } } diff --git a/samples/MobileSandbox.Android/MobileSandbox.Android.csproj b/samples/MobileSandbox.Android/MobileSandbox.Android.csproj index 94f0d31e4d..3b67d79f7b 100644 --- a/samples/MobileSandbox.Android/MobileSandbox.Android.csproj +++ b/samples/MobileSandbox.Android/MobileSandbox.Android.csproj @@ -10,6 +10,12 @@ apk true + + + + + + Resources\drawable\Icon.png @@ -37,6 +43,10 @@ + + + + diff --git a/samples/MobileSandbox.Android/SplashActivity.cs b/samples/MobileSandbox.Android/SplashActivity.cs deleted file mode 100644 index ced092554d..0000000000 --- a/samples/MobileSandbox.Android/SplashActivity.cs +++ /dev/null @@ -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 - { - protected override void OnResume() - { - base.OnResume(); - - StartActivity(new Intent(Application.Context, typeof(MainActivity))); - } - } -} diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs new file mode 100644 index 0000000000..3fcfde3ee4 --- /dev/null +++ b/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 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(); + + 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; + } + } + } +} diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs index b2cd150933..499924d8fb 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs @@ -11,27 +11,49 @@ using AndroidX.AppCompat.App; namespace Avalonia.Android { - public abstract class AvaloniaMainActivity : AppCompatActivity, IActivityResultHandler, IActivityNavigationService + public class AvaloniaMainActivity : AppCompatActivity, IActivityResultHandler, IActivityNavigationService { - internal static object ViewContent; - public Action ActivityResult { get; set; } public Action RequestPermissionsResult { get; set; } + + public event EventHandler 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 : AvaloniaMainActivity where TApp : Application, new() + { internal AvaloniaView View; private GlobalLayoutListener _listener; protected override void OnCreate(Bundle savedInstanceState) { - View = new AvaloniaView(this); - if (ViewContent != null) - { - View.Content = ViewContent; - } + InitializeApp(); - if (Avalonia.Application.Current.ApplicationLifetime is SingleViewLifetime lifetime) - { - lifetime.View = View; - } base.OnCreate(savedInstanceState); SetContentView(View); @@ -41,20 +63,6 @@ namespace Avalonia.Android View.ViewTreeObserver?.AddOnGlobalLayoutListener(_listener); } - public object Content - { - get - { - return ViewContent; - } - set - { - ViewContent = value; - if (View != null) - View.Content = value; - } - } - protected override void OnResume() { base.OnResume(); @@ -66,20 +74,6 @@ namespace Avalonia.Android } } - public event EventHandler BackRequested; - - public override void OnBackPressed() - { - var eventArgs = new AndroidBackRequestedEventArgs(); - - BackRequested?.Invoke(this, eventArgs); - - if (!eventArgs.Handled) - { - base.OnBackPressed(); - } - } - protected override void OnDestroy() { View.Content = null; @@ -89,20 +83,6 @@ namespace Avalonia.Android 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 { private AvaloniaView _view; diff --git a/src/Android/Avalonia.Android/AvaloniaSplashActivity.cs b/src/Android/Avalonia.Android/AvaloniaSplashActivity.cs deleted file mode 100644 index 38038ef26c..0000000000 --- a/src/Android/Avalonia.Android/AvaloniaSplashActivity.cs +++ /dev/null @@ -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 : AvaloniaSplashActivity where TApp : Application, new() - { - protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder.UseAndroid(); - - protected override AppBuilder CreateAppBuilder() - { - var builder = AppBuilder.Configure(); - - return CustomizeAppBuilder(builder); - } - } -} From 97f81c6d571f453661206e0c95db73277b89ca81 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 23 May 2023 16:05:03 +0000 Subject: [PATCH 24/42] add animated splash screens for android api 31+ --- .../drawable-night-v31/avalonia_anim.xml | 66 +++++++++++++++++ .../Resources/drawable-v31/avalonia_anim.xml | 71 +++++++++++++++++++ .../Resources/values-v31/styles.xml | 21 ++++++ .../Resources/values/styles.xml | 11 +-- .../drawable-night-v31/avalonia_anim.xml | 66 +++++++++++++++++ .../Resources/drawable-v31/avalonia_anim.xml | 71 +++++++++++++++++++ .../Resources/values-night/colors.xml | 4 ++ .../Resources/values-v31/styles.xml | 16 +++++ .../Resources/values/styles.xml | 7 +- 9 files changed, 317 insertions(+), 16 deletions(-) create mode 100644 samples/ControlCatalog.Android/Resources/drawable-night-v31/avalonia_anim.xml create mode 100644 samples/ControlCatalog.Android/Resources/drawable-v31/avalonia_anim.xml create mode 100644 samples/ControlCatalog.Android/Resources/values-v31/styles.xml create mode 100644 samples/MobileSandbox.Android/Resources/drawable-night-v31/avalonia_anim.xml create mode 100644 samples/MobileSandbox.Android/Resources/drawable-v31/avalonia_anim.xml create mode 100644 samples/MobileSandbox.Android/Resources/values-night/colors.xml create mode 100644 samples/MobileSandbox.Android/Resources/values-v31/styles.xml diff --git a/samples/ControlCatalog.Android/Resources/drawable-night-v31/avalonia_anim.xml b/samples/ControlCatalog.Android/Resources/drawable-night-v31/avalonia_anim.xml new file mode 100644 index 0000000000..dde4b5a7dd --- /dev/null +++ b/samples/ControlCatalog.Android/Resources/drawable-night-v31/avalonia_anim.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog.Android/Resources/drawable-v31/avalonia_anim.xml b/samples/ControlCatalog.Android/Resources/drawable-v31/avalonia_anim.xml new file mode 100644 index 0000000000..94f27d9e63 --- /dev/null +++ b/samples/ControlCatalog.Android/Resources/drawable-v31/avalonia_anim.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog.Android/Resources/values-v31/styles.xml b/samples/ControlCatalog.Android/Resources/values-v31/styles.xml new file mode 100644 index 0000000000..d5ecec4391 --- /dev/null +++ b/samples/ControlCatalog.Android/Resources/values-v31/styles.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/samples/ControlCatalog.Android/Resources/values/styles.xml b/samples/ControlCatalog.Android/Resources/values/styles.xml index 3e1270256d..6e534de2f0 100644 --- a/samples/ControlCatalog.Android/Resources/values/styles.xml +++ b/samples/ControlCatalog.Android/Resources/values/styles.xml @@ -6,16 +6,7 @@ - - - - - diff --git a/samples/MobileSandbox.Android/Resources/drawable-night-v31/avalonia_anim.xml b/samples/MobileSandbox.Android/Resources/drawable-night-v31/avalonia_anim.xml new file mode 100644 index 0000000000..dde4b5a7dd --- /dev/null +++ b/samples/MobileSandbox.Android/Resources/drawable-night-v31/avalonia_anim.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/MobileSandbox.Android/Resources/drawable-v31/avalonia_anim.xml b/samples/MobileSandbox.Android/Resources/drawable-v31/avalonia_anim.xml new file mode 100644 index 0000000000..94f27d9e63 --- /dev/null +++ b/samples/MobileSandbox.Android/Resources/drawable-v31/avalonia_anim.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/MobileSandbox.Android/Resources/values-night/colors.xml b/samples/MobileSandbox.Android/Resources/values-night/colors.xml new file mode 100644 index 0000000000..3d47b6fc58 --- /dev/null +++ b/samples/MobileSandbox.Android/Resources/values-night/colors.xml @@ -0,0 +1,4 @@ + + + #212121 + diff --git a/samples/MobileSandbox.Android/Resources/values-v31/styles.xml b/samples/MobileSandbox.Android/Resources/values-v31/styles.xml new file mode 100644 index 0000000000..7518f078e5 --- /dev/null +++ b/samples/MobileSandbox.Android/Resources/values-v31/styles.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/samples/MobileSandbox.Android/Resources/values/styles.xml b/samples/MobileSandbox.Android/Resources/values/styles.xml index 2759d2904a..22085806da 100644 --- a/samples/MobileSandbox.Android/Resources/values/styles.xml +++ b/samples/MobileSandbox.Android/Resources/values/styles.xml @@ -6,12 +6,7 @@ - - - From 9d051a6a64c6039bd8b075c7997468741a78acc8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 21 Jun 2023 09:39:02 +0200 Subject: [PATCH 25/42] Added failing test for #11484. --- .../AvaloniaObjectTests_Coercion.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs index 42720cbb4c..e664829677 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs @@ -243,6 +243,19 @@ namespace Avalonia.Base.UnitTests 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] public void ClearValue_Respects_Coerced_Default_Value() { From a2d572e006cfd30eb6825f0cf3e33b55cef0e91e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 21 Jun 2023 09:43:28 +0200 Subject: [PATCH 26/42] Don't set uncoerced value to coerced default val. Fixes #11484 --- src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs index 4518289335..a69d7b7094 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs @@ -216,7 +216,7 @@ namespace Avalonia.PropertyStore valueChanged = !EqualityComparer.Default.Equals(Value, v); Value = v; Priority = priority; - if (_uncommon is not null) + if (!isCoercedDefaultValue && _uncommon is not null) _uncommon._uncoercedValue = value; } @@ -225,7 +225,7 @@ namespace Avalonia.PropertyStore baseValueChanged = !EqualityComparer.Default.Equals(_baseValue, v); _baseValue = v; BasePriority = priority; - if (_uncommon is not null) + if (!isCoercedDefaultValue && _uncommon is not null) _uncommon._uncoercedBaseValue = value; } From 21647102bb620e544220fb116ddeea484f30285f Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Wed, 21 Jun 2023 12:30:49 +0200 Subject: [PATCH 27/42] Fix simple theme for TransitioningContentControl --- .../Controls/TransitioningContentControl.xaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Simple/Controls/TransitioningContentControl.xaml b/src/Avalonia.Themes.Simple/Controls/TransitioningContentControl.xaml index ceccdf56c7..5f532df848 100644 --- a/src/Avalonia.Themes.Simple/Controls/TransitioningContentControl.xaml +++ b/src/Avalonia.Themes.Simple/Controls/TransitioningContentControl.xaml @@ -13,11 +13,10 @@ BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}" ContentTemplate="{TemplateBinding ContentTemplate}" - Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" /> - Date: Wed, 21 Jun 2023 15:05:19 +0200 Subject: [PATCH 28/42] First draft --- src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml | 148 +++++++----------- .../AboutAvaloniaDialog.xaml.cs | 5 +- 2 files changed, 62 insertions(+), 91 deletions(-) diff --git a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml index 22362fa5f5..2bd33ac296 100644 --- a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml +++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml @@ -6,106 +6,74 @@ MinWidth="430" MinHeight="475" Title="About Avalonia" - Background="Purple" FontFamily="/Assets/Roboto-Light.ttf#Roboto" x:Class="Avalonia.Dialogs.AboutAvaloniaDialog" x:DataType="dialogs:AboutAvaloniaDialog"> + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// The gesture recognizer. - /// internal void CaptureGestureRecognizer(GestureRecognizer? gestureRecognizer) { if (CapturedGestureRecognizer != gestureRecognizer) diff --git a/src/Avalonia.Base/Media/MediaContext.cs b/src/Avalonia.Base/Media/MediaContext.cs index 84a4a8c873..0a290052c7 100644 --- a/src/Avalonia.Base/Media/MediaContext.cs +++ b/src/Avalonia.Base/Media/MediaContext.cs @@ -213,10 +213,10 @@ internal partial class MediaContext : ICompositorScheduler } /// - /// Executes the callback in the next iteration of the current UI-thread + /// Executes the callback in the next iteration of the current UI-thread /// render loop / layout pass that. /// - /// Code to execute. + /// public void BeginInvokeOnRender(Action callback) { if (_invokeOnRenderCallbacks == null) diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index a0cd2a8dcd..df3c9ebfc5 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -161,7 +161,7 @@ namespace Avalonia.Platform void PopGeometryClip(); /// - + /// Get Fetaure from type /// /// Attempts to get an optional feature from the drawing context implementation /// diff --git a/src/Avalonia.Base/Rendering/RenderLoop.cs b/src/Avalonia.Base/Rendering/RenderLoop.cs index aaecf5deaf..80410ba181 100644 --- a/src/Avalonia.Base/Rendering/RenderLoop.cs +++ b/src/Avalonia.Base/Rendering/RenderLoop.cs @@ -42,7 +42,6 @@ namespace Avalonia.Rendering /// Initializes a new instance of the class. /// /// The render timer. - /// The UI thread dispatcher. public RenderLoop(IRenderTimer timer) { _timer = timer; diff --git a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs index bb1663eac0..7d586e1cbd 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs @@ -581,11 +581,6 @@ public partial class Dispatcher /// /// A Func<Task<TResult>> delegate to invoke through the dispatcher. /// - /// - /// The priority that determines in what order the specified - /// callback is invoked relative to the other pending operations - /// in the Dispatcher. - /// /// /// An task that completes after the task returned from callback finishes /// From 25a59718464ef3ec4972b9a7484b9ed02471a760 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 22 Jun 2023 12:44:42 +0200 Subject: [PATCH 36/42] fix(DevTools): Invalid Enum Editor --- .../Diagnostics/Views/PropertyValueEditorView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs index aec0cb37bc..f6e9842ac8 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs @@ -265,7 +265,7 @@ namespace Avalonia.Diagnostics.Views static bool IsValidNumeric(Type? type) { - if (type == null) + if (type == null || type.IsEnum == true) { return false; } From 349d7e9e6763d9d477fc57945f992675b5c480de Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 23 Jun 2023 19:38:08 -0400 Subject: [PATCH 37/42] Fix compat fluent theme not being applied --- .../DensityStyles/Compact.xaml | 13 +++---- src/Avalonia.Themes.Fluent/FluentTheme.xaml | 2 +- .../FluentTheme.xaml.cs | 35 +++++++++---------- 3 files changed, 22 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml b/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml index d335e6f4b9..f6efefa05a 100644 --- a/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml +++ b/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml @@ -1,10 +1,3 @@ - - - 14 14 24 @@ -18,5 +11,7 @@ 24 12,1,0,3 32 - - + + diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml index c84772aa61..f4467e40ce 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -18,7 +18,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs index 5af22dbd1d..378041356a 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs @@ -16,9 +16,10 @@ namespace Avalonia.Themes.Fluent /// /// Includes the fluent theme in an application. /// - public class FluentTheme : Styles + public class FluentTheme : Styles, IResourceNode { - private readonly Styles _compactStyles; + private readonly ResourceDictionary _compactStyles; + private DensityStyle _densityStyle; /// /// Initializes a new instance of the class. @@ -28,9 +29,7 @@ namespace Avalonia.Themes.Fluent { AvaloniaXamlLoader.Load(sp, this); - _compactStyles = (Styles)GetAndRemove("CompactStyles"); - - EnsureCompactStyles(); + _compactStyles = (ResourceDictionary)GetAndRemove("CompactStyles"); Palettes = Resources.MergedDictionaries.OfType().FirstOrDefault() ?? throw new InvalidOperationException("FluentTheme was initialized with missing ColorPaletteResourcesCollection."); @@ -43,17 +42,17 @@ namespace Avalonia.Themes.Fluent return val; } } - - public static readonly StyledProperty DensityStyleProperty = - AvaloniaProperty.Register(nameof(DensityStyle)); + + public static readonly DirectProperty DensityStyleProperty = AvaloniaProperty.RegisterDirect( + nameof(DensityStyle), o => o.DensityStyle, (o, v) => o.DensityStyle = v); /// /// Gets or sets the density style of the fluent theme (normal, compact). /// public DensityStyle DensityStyle { - get => GetValue(DensityStyleProperty); - set => SetValue(DensityStyleProperty, value); + get => _densityStyle; + set => SetAndRaise(DensityStyleProperty, ref _densityStyle, value); } public IDictionary Palettes { get; } @@ -64,20 +63,20 @@ namespace Avalonia.Themes.Fluent 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); - } - else - { - Remove(_compactStyles); + return true; } + + return base.TryGetResource(key, theme, out value); } } } From dcd1b59d3101a3d28d47fe3e888371a83b12d6b1 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 23 Jun 2023 19:38:55 -0400 Subject: [PATCH 38/42] Make compact resources better --- .../DensityStyles/Compact.xaml | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml b/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml index f6efefa05a..830198f6d1 100644 --- a/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml +++ b/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml @@ -1,17 +1,25 @@ - 14 - 14 - 24 - 2,2,6,1 - 32 - 24 - 0,1,0,2 - 0,1,0,2 - 9,0,0,1 - 10,0,30,0 - 24 - 12,1,0,3 - 32  + 14 + 14 + 24 + 32 + 4, 2 + 4,2 + 24 + 11,5,11,7 + 4 4 8 4 + 5, 2 + 24 + 0,1,0,2 + 0,1,0,2 + 9,0,0,1 + 10,0,30,0 + 24 + 12,1,0,3 + 32 + 28 + 6, 0 + 6,4 From 5fb1cf9f829c437befd163ea9ab6b5657e03e1af Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Mon, 26 Jun 2023 11:43:44 +0200 Subject: [PATCH 39/42] Fix DataGridColumn.CellStyleClasses not settable from xaml --- src/Avalonia.Controls.DataGrid/DataGridColumn.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index d28c0969c4..a5695afeb7 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -38,7 +38,7 @@ namespace Avalonia.Controls private ICellEditBinding _editBinding; private IBinding _clipboardContentBinding; private ControlTheme _cellTheme; - private readonly Classes _cellStyleClasses = new Classes(); + private Classes _cellStyleClasses; /// /// Initializes a new instance of the class. @@ -393,17 +393,7 @@ namespace Avalonia.Controls } } - public Classes CellStyleClasses - { - get => _cellStyleClasses; - set - { - if(_cellStyleClasses != value) - { - _cellStyleClasses.Replace(value); - } - } - } + public Classes CellStyleClasses => _cellStyleClasses ??= new(); /// /// Backing field for CellTheme property. From 862c175725bfb3978a32d6d03e94b2a8b1f9308c Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 27 Jun 2023 12:27:30 +0200 Subject: [PATCH 40/42] Fix ScrollContentPresenter's child margin with layout rounding --- .../Presenters/ScrollContentPresenter.cs | 12 +++++-- .../Presenters/ScrollContentPresenterTests.cs | 31 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index bd694b200c..bd3d2b5171 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -7,7 +7,7 @@ using Avalonia.Input.GestureRecognizers; using Avalonia.Utilities; using Avalonia.VisualTree; using System.Linq; -using Avalonia.Interactivity; +using Avalonia.Layout; namespace Avalonia.Controls.Presenters { @@ -473,7 +473,15 @@ namespace Avalonia.Controls.Presenters } 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; return finalSize; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs index f6130d49d6..c7ea5c1b69 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs @@ -4,6 +4,7 @@ using System.Reactive.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Layout; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Controls.UnitTests.Presenters @@ -244,6 +245,36 @@ namespace Avalonia.Controls.UnitTests.Presenters 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] public void Extent_Width_Should_Be_Arrange_Width_When_CanScrollHorizontally_False() { From b488afaf799eb50bcc1de5f6728024557a3373e3 Mon Sep 17 00:00:00 2001 From: Valters Melnalksnis Date: Tue, 27 Jun 2023 13:49:20 +0300 Subject: [PATCH 41/42] Fix non-configurable minimum width for DataGridColumnHeader --- src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index 082eac60be..0cc620dae9 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -42,9 +42,10 @@ - + 0.6 0.8 + 32 M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z @@ -174,7 +175,7 @@ VerticalAlignment="{TemplateBinding VerticalContentAlignment}"> - + - Date: Wed, 28 Jun 2023 01:27:12 +0300 Subject: [PATCH 42/42] fix SendDataRequest re-run task --- src/Avalonia.X11/X11Clipboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.X11/X11Clipboard.cs b/src/Avalonia.X11/X11Clipboard.cs index 04d1aae194..6e18151fae 100644 --- a/src/Avalonia.X11/X11Clipboard.cs +++ b/src/Avalonia.X11/X11Clipboard.cs @@ -224,7 +224,7 @@ namespace Avalonia.X11 private Task SendDataRequest(IntPtr format) { - if (_requestedDataTcs == null || _requestedFormatsTcs.Task.IsCompleted) + if (_requestedDataTcs == null || _requestedDataTcs.Task.IsCompleted) _requestedDataTcs = new TaskCompletionSource(); XConvertSelection(_x11.Display, _x11.Atoms.CLIPBOARD, format, format, _handle, IntPtr.Zero); return _requestedDataTcs.Task;