From b094699f760b1f760174d841fd3a5fddfb7e663d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 9 May 2022 19:36:06 +0300 Subject: [PATCH 001/171] Compositor works with X11. Somewhat --- samples/ControlCatalog.NetCore/Program.cs | 1 + src/Avalonia.Base/Avalonia.Base.csproj | 5 +- .../Collections/IAvaloniaReadOnlyList.cs | 1 + .../Collections/Pooled/PooledList.cs | 2 +- src/Avalonia.Base/Matrix.cs | 39 + .../Animations/AnimatedValueStore.cs | 40 + .../Animations/CompositionAnimation.cs | 63 ++ .../Animations/CompositionAnimationGroup.cs | 26 + .../Animations/ExpressionAnimation.cs | 34 + .../Animations/ExpressionAnimationInstance.cs | 44 ++ .../Animations/IAnimationInstance.cs | 11 + .../Animations/ICompositionAnimationBase.cs | 12 + .../Animations/ImplicitAnimationCollection.cs | 73 ++ .../Composition/Animations/Interpolators.cs | 73 ++ .../Animations/KeyFrameAnimation.cs | 53 ++ .../Animations/KeyFrameAnimationInstance.cs | 139 ++++ .../Composition/Animations/KeyFrames.cs | 80 ++ .../Animations/PropertySetSnapshot.cs | 46 ++ .../Composition/CompositingRenderer.cs | 194 +++++ .../Composition/CompositionDrawListVisual.cs | 44 ++ .../Composition/CompositionEasingFunction.cs | 97 +++ .../Composition/CompositionGradientBrush.cs | 16 + .../Composition/CompositionObject.cs | 125 +++ .../Composition/CompositionPropertySet.cs | 132 ++++ .../Composition/CompositionTarget.cs | 107 +++ .../Rendering/Composition/Compositor.cs | 143 ++++ .../Composition/CompositorRenderLoopTask.cs | 20 + .../Rendering/Composition/ContainerVisual.cs | 20 + .../Rendering/Composition/CustomDrawVisual.cs | 56 ++ .../Drawing/CompositionDrawList.cs | 81 ++ .../Drawing/CompositionDrawingContext.cs | 383 +++++++++ .../Rendering/Composition/Enums.cs | 120 +++ .../Expressions/BuiltInExpressionFfi.cs | 234 ++++++ .../Expressions/DelegateExpressionFfi.cs | 181 +++++ .../Composition/Expressions/Expression.cs | 331 ++++++++ .../ExpressionEvaluationContext.cs | 31 + .../Expressions/ExpressionParseException.cs | 14 + .../Expressions/ExpressionParser.cs | 298 +++++++ .../Expressions/ExpressionVariant.cs | 739 ++++++++++++++++++ .../Composition/Expressions/TokenParser.cs | 256 ++++++ .../Composition/ICompositionSurface.cs | 9 + .../Rendering/Composition/MatrixUtils.cs | 46 ++ .../Composition/Server/DrawingContextProxy.cs | 142 ++++ .../Composition/Server/ReadbackIndices.cs | 46 ++ .../Server/ServerCompositionBrush.cs | 7 + .../Server/ServerCompositionDrawListVisual.cs | 41 + .../Server/ServerCompositionGradientBrush.cs | 15 + .../ServerCompositionLinearGradientBrush.cs | 22 + .../Server/ServerCompositionSurface.cs | 9 + .../Server/ServerCompositionTarget.cs | 53 ++ .../Composition/Server/ServerCompositor.cs | 90 +++ .../Server/ServerContainerVisual.cs | 29 + .../Server/ServerCustomDrawVisual.cs | 31 + .../Composition/Server/ServerList.cs | 46 ++ .../Composition/Server/ServerObject.cs | 36 + .../Server/ServerSolidColorVisual.cs | 16 + .../Composition/Server/ServerSpriteVisual.cs | 20 + .../Composition/Server/ServerVisual.cs | 92 +++ .../Rendering/Composition/Transport/Batch.cs | 37 + .../Rendering/Composition/Transport/Change.cs | 82 ++ .../Composition/Transport/ChangeSet.cs | 36 + .../Composition/Transport/ChangeSetPool.cs | 42 + .../Transport/CustomDrawVisualChanges.cs | 20 + .../Transport/DrawListVisualChanges.cs | 48 ++ .../Composition/Transport/ListChange.cs | 19 + .../Composition/Transport/ListChangeSet.cs | 25 + .../Transport/ServerListProxyHelper.cs | 92 +++ .../Composition/Transport/VisualChanges.cs | 16 + .../Composition/Utils/CubicBezier.cs | 302 +++++++ .../Rendering/Composition/Utils/MathExt.cs | 23 + .../Rendering/Composition/Visual.cs | 64 ++ .../Rendering/Composition/VisualCollection.cs | 64 ++ src/Avalonia.Base/Rendering/IRenderer.cs | 6 + .../SceneGraph/DeferredDrawingContextImpl.cs | 1 + src/Avalonia.Base/Size.cs | 11 + .../Threading/DispatcherPriority.cs | 14 +- src/Avalonia.Base/Visual.cs | 25 +- src/Avalonia.Base/composition-schema.xml | 70 ++ .../HeadlessPlatformRenderInterface.cs | 2 + .../CompositionRoslynGenerator.cs | 21 + .../CompositionGenerator/Config.cs | 116 +++ .../CompositionGenerator/Extensions.cs | 90 +++ .../Generator.KeyFrameAnimation.cs | 59 ++ .../Generator.ListProxy.cs | 121 +++ .../CompositionGenerator/Generator.Utils.cs | 66 ++ .../CompositionGenerator/Generator.cs | 504 ++++++++++++ src/Avalonia.X11/X11Platform.cs | 8 +- src/Avalonia.X11/X11Window.cs | 17 +- .../Media/DrawingContextImpl.cs | 7 + 89 files changed, 7106 insertions(+), 16 deletions(-) create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionObject.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Compositor.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Enums.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/Change.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Visual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/VisualCollection.cs create mode 100644 src/Avalonia.Base/composition-schema.xml create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Generator.Utils.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 4464413e63..2a0755b900 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -111,6 +111,7 @@ namespace ControlCatalog.NetCore EnableMultiTouch = true, UseDBusMenu = true, EnableIme = true, + UseCompositor = true }) .With(new Win32PlatformOptions { diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 8e4755b4b7..bcebdd504c 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -3,10 +3,13 @@ net6.0;netstandard2.0 Avalonia.Base Avalonia - True + True + true + $(BaseIntermediateOutputPath)\GeneratedFiles + diff --git a/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs index a6a5953827..cefbf642be 100644 --- a/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs +++ b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; diff --git a/src/Avalonia.Base/Collections/Pooled/PooledList.cs b/src/Avalonia.Base/Collections/Pooled/PooledList.cs index 200a52fb0d..ffda1bedca 100644 --- a/src/Avalonia.Base/Collections/Pooled/PooledList.cs +++ b/src/Avalonia.Base/Collections/Pooled/PooledList.cs @@ -1434,7 +1434,7 @@ namespace Avalonia.Collections.Pooled /// /// Returns the internal buffers to the ArrayPool. /// - public void Dispose() + public virtual void Dispose() { ReturnArray(); _size = 0; diff --git a/src/Avalonia.Base/Matrix.cs b/src/Avalonia.Base/Matrix.cs index b08a0eb98a..6f00b08d13 100644 --- a/src/Avalonia.Base/Matrix.cs +++ b/src/Avalonia.Base/Matrix.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Numerics; using Avalonia.Utilities; namespace Avalonia @@ -106,6 +107,36 @@ namespace Avalonia (value1._m31 * value2.M12) + (value1._m32 * value2.M22) + value2._m32); } + public static Matrix operator +(Matrix value1, Matrix value2) + { + return new Matrix(value1.M11 + value2.M11, + value1.M12 + value2.M12, + value1.M21 + value2.M21, + value1.M22 + value2.M22, + value1.M31 + value2.M31, + value1.M32 + value2.M32); + } + + public static Matrix operator -(Matrix value1, Matrix value2) + { + return new Matrix(value1.M11 - value2.M11, + value1.M12 - value2.M12, + value1.M21 - value2.M21, + value1.M22 - value2.M22, + value1.M31 - value2.M31, + value1.M32 - value2.M32); + } + + public static Matrix operator *(Matrix value1, double value2) + { + return new Matrix(value1.M11 * value2, + value1.M12 * value2, + value1.M21 * value2, + value1.M22 * value2, + value1.M31 * value2, + value1.M32 * value2); + } + /// /// Negates the given matrix by multiplying all values by -1. /// @@ -427,6 +458,14 @@ namespace Avalonia return true; } +#if !BUILDTASK + public static implicit operator Matrix4x4(Matrix m) + { + return new Matrix4x4(new Matrix3x2((float)m._m11, (float)m._m12, (float)m._m21, (float)m._m22, + (float)m._m31, (float)m._m32)); + } +#endif + public struct Decomposed { public Vector Translate; diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs new file mode 100644 index 0000000000..180c45022f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs @@ -0,0 +1,40 @@ +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Animations +{ + internal struct AnimatedValueStore where T : struct + { + private T _direct; + private IAnimationInstance _animation; + private T? _lastAnimated; + + public T Direct => _direct; + + public T GetAnimated(ServerCompositor compositor) + { + if (_animation == null) + return _direct; + var v = _animation.Evaluate(compositor.ServerNow, ExpressionVariant.Create(_direct)) + .CastOrDefault(); + _lastAnimated = v; + return v; + } + + private T LastAnimated => _animation != null ? _lastAnimated ?? _direct : _direct; + + public bool IsAnimation => _animation != null; + + public void SetAnimation(ChangeSet cs, IAnimationInstance animation) + { + _animation = animation; + _animation.Start(cs.Batch.CommitedAt, ExpressionVariant.Create(LastAnimated)); + } + + public static implicit operator AnimatedValueStore(T value) => new AnimatedValueStore() + { + _direct = value + }; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs new file mode 100644 index 0000000000..9375faaaae --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -0,0 +1,63 @@ +// ReSharper disable InconsistentNaming +// ReSharper disable CheckNamespace + +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Animations +{ + public abstract class CompositionAnimation : CompositionObject, ICompositionAnimationBase + { + private readonly CompositionPropertySet _propertySet; + internal CompositionAnimation(Compositor compositor) : base(compositor, null!) + { + _propertySet = new CompositionPropertySet(compositor); + } + + private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); + + public void ClearAllParameters() => _propertySet.ClearAll(); + + public void ClearParameter(string key) => _propertySet.Clear(key); + + void SetVariant(string key, ExpressionVariant value) => _propertySet.Set(key, value); + + public void SetColorParameter(string key, Avalonia.Media.Color value) => SetVariant(key, value); + + public void SetMatrix3x2Parameter(string key, Matrix3x2 value) => SetVariant(key, value); + + public void SetMatrix4x4Parameter(string key, Matrix4x4 value) => SetVariant(key, value); + + public void SetQuaternionParameter(string key, Quaternion value) => SetVariant(key, value); + + public void SetReferenceParameter(string key, CompositionObject compositionObject) => + _propertySet.Set(key, compositionObject); + + public void SetScalarParameter(string key, float value) => SetVariant(key, value); + + public void SetVector2Parameter(string key, Vector2 value) => SetVariant(key, value); + + public void SetVector3Parameter(string key, Vector3 value) => SetVariant(key, value); + + public void SetVector4Parameter(string key, Vector4 value) => SetVariant(key, value); + + // TODO: void SetExpressionReferenceParameter(string parameterName, IAnimationObject source) + + public string? Target { get; set; } + + internal abstract IAnimationInstance CreateInstance(ServerObject targetObject, + ExpressionVariant? finalValue); + + internal PropertySetSnapshot CreateSnapshot(bool server) + => _propertySet.Snapshot(server, 1); + + void ICompositionAnimationBase.InternalOnly() + { + + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs new file mode 100644 index 0000000000..833f7e498c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Transport; + + +namespace Avalonia.Rendering.Composition.Animations +{ + public class CompositionAnimationGroup : CompositionObject, ICompositionAnimationBase + { + internal List Animations { get; } = new List(); + void ICompositionAnimationBase.InternalOnly() + { + + } + + public void Add(CompositionAnimation value) => Animations.Add(value); + public void Remove(CompositionAnimation value) => Animations.Remove(value); + public void RemoveAll() => Animations.Clear(); + + public CompositionAnimationGroup(Compositor compositor) : base(compositor, null!) + { + } + + private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs new file mode 100644 index 0000000000..a6f24c2e35 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs @@ -0,0 +1,34 @@ +// ReSharper disable CheckNamespace +using System; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Animations +{ + public class ExpressionAnimation : CompositionAnimation + { + private string? _expression; + private Expression? _parsedExpression; + + internal ExpressionAnimation(Compositor compositor) : base(compositor) + { + } + + public string? Expression + { + get => _expression; + set + { + _expression = value; + _parsedExpression = null; + } + } + + private Expression ParsedExpression => _parsedExpression ??= ExpressionParser.Parse(_expression.AsSpan()); + + internal override IAnimationInstance CreateInstance( + ServerObject targetObject, ExpressionVariant? finalValue) + => new ExpressionAnimationInstance(ParsedExpression, + targetObject, finalValue, CreateSnapshot(true)); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs new file mode 100644 index 0000000000..47b947b2e9 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs @@ -0,0 +1,44 @@ +using System; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + internal class ExpressionAnimationInstance : IAnimationInstance + { + private readonly Expression _expression; + private readonly IExpressionObject _target; + private ExpressionVariant _startingValue; + private readonly ExpressionVariant? _finalValue; + private readonly PropertySetSnapshot _parameters; + + public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) + { + var ctx = new ExpressionEvaluationContext + { + Parameters = _parameters, + Target = _target, + ForeignFunctionInterface = BuiltInExpressionFfi.Instance, + StartingValue = _startingValue, + FinalValue = _finalValue ?? _startingValue, + CurrentValue = currentValue + }; + return _expression.Evaluate(ref ctx); + } + + public void Start(TimeSpan startedAt, ExpressionVariant startingValue) + { + _startingValue = startingValue; + } + + public ExpressionAnimationInstance(Expression expression, + IExpressionObject target, + ExpressionVariant? finalValue, + PropertySetSnapshot parameters) + { + _expression = expression; + _target = target; + _finalValue = finalValue; + _parameters = parameters; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs new file mode 100644 index 0000000000..a0b066ae0c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs @@ -0,0 +1,11 @@ +using System; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + internal interface IAnimationInstance + { + ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue); + void Start(TimeSpan startedAt, ExpressionVariant startingValue); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs new file mode 100644 index 0000000000..bf40fd3ad2 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs @@ -0,0 +1,12 @@ +// ReSharper disable CheckNamespace + +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Animations +{ + public interface ICompositionAnimationBase + { + internal void InternalOnly(); + } + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs new file mode 100644 index 0000000000..be91352527 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Animations +{ + public class ImplicitAnimationCollection : CompositionObject, IDictionary + { + private Dictionary _inner = new Dictionary(); + private IDictionary _innerface; + internal ImplicitAnimationCollection(Compositor compositor) : base(compositor, null!) + { + _innerface = _inner; + } + + private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); + + public IEnumerator> GetEnumerator() => _inner.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable) _inner).GetEnumerator(); + + void ICollection>.Add(KeyValuePair item) => _innerface.Add(item); + + public void Clear() => _inner.Clear(); + + bool ICollection>.Contains(KeyValuePair item) => _innerface.Contains(item); + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => _innerface.CopyTo(array, arrayIndex); + + bool ICollection>.Remove(KeyValuePair item) => _innerface.Remove(item); + + public int Count => _inner.Count; + + bool ICollection>.IsReadOnly => _innerface.IsReadOnly; + + public void Add(string key, ICompositionAnimationBase value) => _inner.Add(key, value); + + public bool ContainsKey(string key) => _inner.ContainsKey(key); + + public bool Remove(string key) => _inner.Remove(key); + + public bool TryGetValue(string key, [MaybeNullWhen(false)] out ICompositionAnimationBase value) => + _inner.TryGetValue(key, out value); + + public ICompositionAnimationBase this[string key] + { + get => _inner[key]; + set => _inner[key] = value; + } + + ICollection IDictionary.Keys => _innerface.Keys; + + ICollection IDictionary.Values => + _innerface.Values; + + // UWP compat + public uint Size => (uint) Count; + + public IReadOnlyDictionary GetView() => + new Dictionary(this); + + public bool HasKey(string key) => ContainsKey(key); + public void Insert(string key, ICompositionAnimationBase animation) => Add(key, animation); + + public ICompositionAnimationBase? Lookup(string key) + { + _inner.TryGetValue(key, out var rv); + return rv; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs b/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs new file mode 100644 index 0000000000..62b790701a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs @@ -0,0 +1,73 @@ +using System; +using System.Numerics; + +namespace Avalonia.Rendering.Composition.Animations +{ + internal interface IInterpolator + { + T Interpolate(T from, T to, float progress); + } + + class ScalarInterpolator : IInterpolator + { + public float Interpolate(float @from, float to, float progress) => @from + (to - @from) * progress; + + public static ScalarInterpolator Instance { get; } = new ScalarInterpolator(); + } + + class Vector2Interpolator : IInterpolator + { + public Vector2 Interpolate(Vector2 @from, Vector2 to, float progress) + => Vector2.Lerp(@from, to, progress); + + public static Vector2Interpolator Instance { get; } = new Vector2Interpolator(); + } + + class Vector3Interpolator : IInterpolator + { + public Vector3 Interpolate(Vector3 @from, Vector3 to, float progress) + => Vector3.Lerp(@from, to, progress); + + public static Vector3Interpolator Instance { get; } = new Vector3Interpolator(); + } + + class Vector4Interpolator : IInterpolator + { + public Vector4 Interpolate(Vector4 @from, Vector4 to, float progress) + => Vector4.Lerp(@from, to, progress); + + public static Vector4Interpolator Instance { get; } = new Vector4Interpolator(); + } + + class QuaternionInterpolator : IInterpolator + { + public Quaternion Interpolate(Quaternion @from, Quaternion to, float progress) + => Quaternion.Lerp(@from, to, progress); + + public static QuaternionInterpolator Instance { get; } = new QuaternionInterpolator(); + } + + class ColorInterpolator : IInterpolator + { + static byte Lerp(float a, float b, float p) => (byte) Math.Max(0, Math.Min(255, (p * (b - a) + a))); + + public static Avalonia.Media.Color + LerpRGB(Avalonia.Media.Color to, Avalonia.Media.Color from, float progress) => + new Avalonia.Media.Color(Lerp(to.A, @from.A, progress), + Lerp(to.R, @from.R, progress), + Lerp(to.G, @from.G, progress), + Lerp(to.B, @from.B, progress)); + + public Avalonia.Media.Color Interpolate(Avalonia.Media.Color @from, Avalonia.Media.Color to, float progress) + => LerpRGB(@from, to, progress); + + public static ColorInterpolator Instance { get; } = new ColorInterpolator(); + } + + class BooleanInterpolator : IInterpolator + { + public bool Interpolate(bool @from, bool to, float progress) => progress >= 1 ? to : @from; + + public static BooleanInterpolator Instance { get; } = new BooleanInterpolator(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs new file mode 100644 index 0000000000..065dfd7a8e --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs @@ -0,0 +1,53 @@ +namespace Avalonia.Rendering.Composition.Animations +{ + public abstract class KeyFrameAnimation : CompositionAnimation + { + internal KeyFrameAnimation(Compositor compositor) : base(compositor) + { + } + + public AnimationDelayBehavior DelayBehavior { get; set; } + public System.TimeSpan DelayTime { get; set; } + public AnimationDirection Direction { get; set; } + public System.TimeSpan Duration { get; set; } + public AnimationIterationBehavior IterationBehavior { get; set; } + public int IterationCount { get; set; } = 1; + public AnimationStopBehavior StopBehavior { get; set; } + + private protected abstract IKeyFrames KeyFrames { get; } + + public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, + CompositionEasingFunction easingFunction) => + KeyFrames.InsertExpressionKeyFrame(normalizedProgressKey, value, easingFunction); + + public void InsertExpressionKeyFrame(float normalizedProgressKey, string value) + => KeyFrames.InsertExpressionKeyFrame(normalizedProgressKey, value, new LinearEasingFunction(Compositor)); + } + + public enum AnimationDelayBehavior + { + SetInitialValueAfterDelay, + SetInitialValueBeforeDelay + } + + public enum AnimationDirection + { + Normal, + Reverse, + Alternate, + AlternateReverse + } + + public enum AnimationIterationBehavior + { + Count, + Forever + } + + public enum AnimationStopBehavior + { + LeaveCurrentValue, + SetToInitialValue, + SetToFinalValue + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs new file mode 100644 index 0000000000..b90a02148d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs @@ -0,0 +1,139 @@ +using System; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + class KeyFrameAnimationInstance : IAnimationInstance where T : struct + { + private readonly IInterpolator _interpolator; + private readonly ServerKeyFrame[] _keyFrames; + private readonly PropertySetSnapshot _snapshot; + private readonly ExpressionVariant? _finalValue; + private readonly IExpressionObject _target; + private readonly AnimationDelayBehavior _delayBehavior; + private readonly TimeSpan _delayTime; + private readonly AnimationDirection _direction; + private readonly TimeSpan _duration; + private readonly AnimationIterationBehavior _iterationBehavior; + private readonly int _iterationCount; + private readonly AnimationStopBehavior _stopBehavior; + private TimeSpan _startedAt; + private T _startingValue; + + public KeyFrameAnimationInstance( + IInterpolator interpolator, ServerKeyFrame[] keyFrames, + PropertySetSnapshot snapshot, ExpressionVariant? finalValue, + IExpressionObject target, + AnimationDelayBehavior delayBehavior, TimeSpan delayTime, + AnimationDirection direction, TimeSpan duration, + AnimationIterationBehavior iterationBehavior, + int iterationCount, AnimationStopBehavior stopBehavior) + { + _interpolator = interpolator; + _keyFrames = keyFrames; + _snapshot = snapshot; + _finalValue = finalValue; + _target = target; + _delayBehavior = delayBehavior; + _delayTime = delayTime; + _direction = direction; + _duration = duration; + _iterationBehavior = iterationBehavior; + _iterationCount = iterationCount; + _stopBehavior = stopBehavior; + if (_keyFrames.Length == 0) + throw new InvalidOperationException("Animation has no key frames"); + if(_duration.Ticks <= 0) + throw new InvalidOperationException("Invalid animation duration"); + } + + public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) + { + var elapsed = now - _startedAt; + var starting = ExpressionVariant.Create(_startingValue); + var ctx = new ExpressionEvaluationContext + { + Parameters = _snapshot, + Target = _target, + CurrentValue = currentValue, + FinalValue = _finalValue ?? starting, + StartingValue = starting, + ForeignFunctionInterface = BuiltInExpressionFfi.Instance + }; + + if (elapsed < _delayTime) + { + if (_delayBehavior == AnimationDelayBehavior.SetInitialValueBeforeDelay) + return ExpressionVariant.Create(GetKeyFrame(ref ctx, _keyFrames[0])); + return currentValue; + } + + elapsed -= _delayTime; + var iterationNumber = elapsed.Ticks / _duration.Ticks; + if (_iterationBehavior == AnimationIterationBehavior.Count + && iterationNumber >= _iterationCount) + return ExpressionVariant.Create(GetKeyFrame(ref ctx, _keyFrames[_keyFrames.Length - 1])); + + + var evenIterationNumber = iterationNumber % 2 == 0; + elapsed = TimeSpan.FromTicks(elapsed.Ticks % _duration.Ticks); + + var reverse = + _direction == AnimationDirection.Alternate + ? !evenIterationNumber + : _direction == AnimationDirection.AlternateReverse + ? evenIterationNumber + : _direction == AnimationDirection.Reverse; + + var iterationProgress = elapsed.TotalSeconds / _duration.TotalSeconds; + if (reverse) + iterationProgress = 1 - iterationProgress; + + var left = new ServerKeyFrame + { + Value = _startingValue + }; + var right = _keyFrames[_keyFrames.Length - 1]; + for (var c = 0; c < _keyFrames.Length; c++) + { + var kf = _keyFrames[c]; + if (kf.Key < iterationProgress) + { + // this is the last frame + if (c == _keyFrames.Length - 1) + return ExpressionVariant.Create(GetKeyFrame(ref ctx, kf)); + + left = kf; + right = _keyFrames[c + 1]; + break; + } + } + + var keyProgress = Math.Max(0, Math.Min(1, (iterationProgress - left.Key) / (right.Key - left.Key))); + + var easedKeyProgress = right.EasingFunction.Ease((float) keyProgress); + if (float.IsNaN(easedKeyProgress) || float.IsInfinity(easedKeyProgress)) + return currentValue; + + return ExpressionVariant.Create(_interpolator.Interpolate( + GetKeyFrame(ref ctx, left), + GetKeyFrame(ref ctx, right), + easedKeyProgress + )); + } + + T GetKeyFrame(ref ExpressionEvaluationContext ctx, ServerKeyFrame f) + { + if (f.Expression != null) + return f.Expression.Evaluate(ref ctx).CastOrDefault(); + else + return f.Value; + } + + public void Start(TimeSpan startedAt, ExpressionVariant startingValue) + { + _startedAt = startedAt; + _startingValue = startingValue.CastOrDefault(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs new file mode 100644 index 0000000000..d7f2504061 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + class KeyFrames : List>, IKeyFrames + { + void Validate(float key) + { + if (key < 0 || key > 1) + throw new ArgumentException("Key frame key"); + if (Count > 0 && this[Count - 1].NormalizedProgressKey > key) + throw new ArgumentException("Key frame key " + key + " is less than the previous one"); + } + + public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, + CompositionEasingFunction easingFunction) + { + Validate(normalizedProgressKey); + Add(new KeyFrame + { + NormalizedProgressKey = normalizedProgressKey, + Expression = Expression.Parse(value), + EasingFunction = easingFunction + }); + } + + public void Insert(float normalizedProgressKey, T value, CompositionEasingFunction easingFunction) + { + Validate(normalizedProgressKey); + Add(new KeyFrame + { + NormalizedProgressKey = normalizedProgressKey, + Value = value, + EasingFunction = easingFunction + }); + } + + public ServerKeyFrame[] Snapshot() + { + var frames = new ServerKeyFrame[Count]; + for (var c = 0; c < Count; c++) + { + var f = this[c]; + frames[c] = new ServerKeyFrame + { + Expression = f.Expression, + Value = f.Value, + EasingFunction = f.EasingFunction.Snapshot(), + Key = f.NormalizedProgressKey + }; + } + return frames; + } + } + + struct KeyFrame + { + public float NormalizedProgressKey; + public T Value; + public Expression Expression; + public CompositionEasingFunction EasingFunction; + } + + struct ServerKeyFrame + { + public T Value; + public Expression Expression; + public IEasingFunction EasingFunction; + public float Key; + } + + + + interface IKeyFrames + { + public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, CompositionEasingFunction easingFunction); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs b/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs new file mode 100644 index 0000000000..ca703dfc6f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + internal class PropertySetSnapshot : IExpressionParameterCollection, IExpressionObject + { + private readonly Dictionary _dic; + + public struct Value + { + public ExpressionVariant Variant; + public IExpressionObject Object; + + public Value(IExpressionObject o) + { + Object = o; + Variant = default; + } + + public static implicit operator Value(ExpressionVariant v) => new Value + { + Variant = v + }; + } + + public PropertySetSnapshot(Dictionary dic) + { + _dic = dic; + } + + public ExpressionVariant GetParameter(string name) + { + _dic.TryGetValue(name, out var v); + return v.Variant; + } + + public IExpressionObject GetObjectParameter(string name) + { + _dic.TryGetValue(name, out var v); + return v.Object; + } + + public ExpressionVariant GetProperty(string name) => GetParameter(name); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs new file mode 100644 index 0000000000..07ac54b634 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using Avalonia.Collections; +using Avalonia.Media; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering.Composition; + +public class CompositingRenderer : RendererBase, IRendererWithCompositor +{ + private readonly IRenderRoot _root; + private readonly Compositor _compositor; + private readonly IDeferredRendererLock? _rendererLock; + CompositionDrawingContext _recorder = new(); + DrawingContext _recordingContext; + private HashSet _dirty = new(); + private HashSet _recalculateChildren = new(); + private readonly CompositionTarget _target; + private bool _queuedUpdate; + private Action _update; + + public CompositingRenderer(IRenderRoot root, + Compositor compositor, + IDeferredRendererLock? rendererLock = null) + { + _root = root; + _compositor = compositor; + _recordingContext = new DrawingContext(_recorder); + _rendererLock = rendererLock ?? new ManagedDeferredRendererLock(); + _target = compositor.CreateCompositionTarget(root.CreateRenderTarget); + _target.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor); + _update = Update; + } + + public bool DrawFps { get; set; } + public bool DrawDirtyRects { get; set; } + public event EventHandler? SceneInvalidated; + + void QueueUpdate() + { + if(_queuedUpdate) + return; + _queuedUpdate = true; + Dispatcher.UIThread.Post(_update, DispatcherPriority.Composition); + } + public void AddDirty(IVisual visual) + { + _dirty.Add((Visual)visual); + QueueUpdate(); + } + + public IEnumerable HitTest(Point p, IVisual root, Func filter) + { + var res = _target.TryHitTest(new Vector2((float)p.X, (float)p.Y)); + if(res == null) + yield break; + for (var index = res.Count - 1; index >= 0; index--) + { + var v = res[index]; + if (v is CompositionDrawListVisual dv) + { + if (filter == null || filter(dv.Visual)) + yield return dv.Visual; + } + } + } + + public IVisual? HitTestFirst(Point p, IVisual root, Func filter) + { + // TODO: Optimize + return HitTest(p, root, filter).FirstOrDefault(); + } + + public void RecalculateChildren(IVisual visual) + { + _recalculateChildren.Add((Visual)visual); + QueueUpdate(); + } + + private void SyncChildren(Visual v) + { + //TODO: Optimize by moving that logic to Visual itself + if(v.CompositionVisual == null) + return; + var compositionChildren = v.CompositionVisual.Children; + var visualChildren = (AvaloniaList)v.GetVisualChildren(); + if (compositionChildren.Count == visualChildren.Count) + { + bool mismatch = false; + for(var c=0; c _target.IsEnabled = true; + + public void Stop() + { + _target.IsEnabled = true; + } + + public void Dispose() + { + Stop(); + _target.Dispose(); + // Wait for the composition batch to be applied and rendered to guarantee that + // render target is not used anymore and can be safely disposed + _compositor.RequestCommitAsync().Wait(); + } + + + public Compositor Compositor => _compositor; +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs new file mode 100644 index 0000000000..9f02055412 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -0,0 +1,44 @@ +using System.Numerics; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition; + +internal class CompositionDrawListVisual : CompositionContainerVisual +{ + public Visual Visual { get; } + + private new DrawListVisualChanges Changes => (DrawListVisualChanges)base.Changes; + private CompositionDrawList? _drawList; + public CompositionDrawList? DrawList + { + get => _drawList; + set + { + _drawList?.Dispose(); + _drawList = value; + Changes.DrawCommands = value?.Clone(); + } + } + + private protected override IChangeSetPool ChangeSetPool => DrawListVisualChanges.Pool; + + internal CompositionDrawListVisual(Compositor compositor, ServerCompositionContainerVisual server, Visual visual) : base(compositor, server) + { + Visual = visual; + } + + internal override bool HitTest(Vector2 point) + { + if (DrawList == null) + return false; + var pt = new Point(point.X, point.Y); + if (Visual is ICustomHitTest custom) + return custom.HitTest(pt); + foreach (var op in DrawList) + if (op.Item.HitTest(pt)) + return true; + return false; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs b/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs new file mode 100644 index 0000000000..73db243e93 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs @@ -0,0 +1,97 @@ +using System; +using System.Numerics; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Rendering.Composition.Utils; + +namespace Avalonia.Rendering.Composition +{ + public abstract class CompositionEasingFunction : CompositionObject + { + internal CompositionEasingFunction(Compositor compositor) : base(compositor, null!) + { + } + + private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); + + internal abstract IEasingFunction Snapshot(); + } + + internal interface IEasingFunction + { + float Ease(float progress); + } + + public sealed class DelegateCompositionEasingFunction : CompositionEasingFunction + { + private readonly Easing _func; + + public delegate float EasingDelegate(float progress); + + internal DelegateCompositionEasingFunction(Compositor compositor, EasingDelegate func) : base(compositor) + { + _func = new Easing(func); + } + + class Easing : IEasingFunction + { + private readonly EasingDelegate _func; + + public Easing(EasingDelegate func) + { + _func = func; + } + + public float Ease(float progress) => _func(progress); + } + + internal override IEasingFunction Snapshot() => _func; + } + + public class LinearEasingFunction : CompositionEasingFunction + { + public LinearEasingFunction(Compositor compositor) : base(compositor) + { + } + + class Linear : IEasingFunction + { + public float Ease(float progress) => progress; + } + + private static readonly Linear Instance = new Linear(); + internal override IEasingFunction Snapshot() => Instance; + } + + public class CubicBezierEasingFunction : CompositionEasingFunction + { + private CubicBezier _bezier; + public Vector2 ControlPoint1 { get; } + public Vector2 ControlPoint2 { get; } + //cubic-bezier(0.25, 0.1, 0.25, 1.0) + internal CubicBezierEasingFunction(Compositor compositor, Vector2 controlPoint1, Vector2 controlPoint2) : base(compositor) + { + ControlPoint1 = controlPoint1; + ControlPoint2 = controlPoint2; + if (controlPoint1.X < 0 || controlPoint1.X > 1 || controlPoint2.X < 0 || controlPoint2.X > 1) + throw new ArgumentException(); + _bezier = new CubicBezier(controlPoint1.X, controlPoint1.Y, controlPoint2.X, controlPoint2.Y); + } + + class EasingFunction : IEasingFunction + { + private readonly CubicBezier _bezier; + + public EasingFunction(CubicBezier bezier) + { + _bezier = bezier; + } + + public float Ease(float progress) => (float)_bezier.Solve(progress); + } + + internal static IEasingFunction Ease { get; } = new EasingFunction(new CubicBezier(0.25, 0.1, 0.25, 1)); + + internal override IEasingFunction Snapshot() => new EasingFunction(_bezier); + } + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs b/src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs new file mode 100644 index 0000000000..cf222550dd --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs @@ -0,0 +1,16 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition +{ + public partial class CompositionGradientBrush : CompositionBrush + { + internal CompositionGradientBrush(Compositor compositor, ServerCompositionGradientBrush server) : base(compositor, server) + { + ColorStops = new CompositionGradientStopCollection(compositor, server.Stops); + } + + public CompositionGradientStopCollection ColorStops { get; } + } + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs new file mode 100644 index 0000000000..2417ecaba8 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs @@ -0,0 +1,125 @@ +using System; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition +{ + public abstract class CompositionObject : IDisposable, IExpressionObject + { + public ImplicitAnimationCollection? ImplicitAnimations { get; set; } + internal CompositionObject(Compositor compositor, ServerObject server) + { + Compositor = compositor; + Server = server; + } + + public Compositor Compositor { get; } + internal ServerObject Server { get; } + public bool IsDisposed { get; private set; } + private ChangeSet? _changes; + + private static void ThrowInvalidOperation() => + throw new InvalidOperationException("There is no server-side counterpart for this object"); + + private protected ChangeSet Changes + { + get + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (Server == null) ThrowInvalidOperation(); + var currentBatch = Compositor.CurrentBatch; + if (_changes != null && _changes.Batch != currentBatch) + _changes = null; + if (_changes == null) + { + _changes = ChangeSetPool.Get(Server!, currentBatch); + currentBatch.Changes!.Add(_changes); + Compositor.QueueImplicitBatchCommit(); + } + + return _changes; + } + } + + private protected abstract IChangeSetPool ChangeSetPool { get; } + + public void Dispose() + { + Changes.Dispose = true; + IsDisposed = true; + } + + internal virtual ExpressionVariant GetPropertyForAnimation(string name) + { + return default; + } + + ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name); + + public void StartAnimation(string propertyName, CompositionAnimation animation) + => StartAnimation(propertyName, animation, null); + + internal virtual void StartAnimation(string propertyName, CompositionAnimation animation, ExpressionVariant? finalValue = null) + { + throw new ArgumentException("Unknown property " + propertyName); + } + + public void StartAnimationGroup(ICompositionAnimationBase grp) + { + if (grp is CompositionAnimation animation) + { + if(animation.Target == null) + throw new ArgumentException("Animation Target can't be null"); + StartAnimation(animation.Target, animation); + } + else if (grp is CompositionAnimationGroup group) + { + foreach (var a in group.Animations) + { + if (a.Target == null) + throw new ArgumentException("Animation Target can't be null"); + StartAnimation(a.Target, a); + } + } + } + + bool StartAnimationGroupPart(CompositionAnimation animation, string target, ExpressionVariant finalValue) + { + if(animation.Target == null) + throw new ArgumentException("Animation Target can't be null"); + if (animation.Target == target) + { + StartAnimation(animation.Target, animation, finalValue); + return true; + } + else + { + StartAnimation(animation.Target, animation); + return false; + } + } + + internal bool StartAnimationGroup(ICompositionAnimationBase grp, string target, ExpressionVariant finalValue) + { + if (grp is CompositionAnimation animation) + return StartAnimationGroupPart(animation, target, finalValue); + if (grp is CompositionAnimationGroup group) + { + var matched = false; + foreach (var a in group.Animations) + { + if (a.Target == null) + throw new ArgumentException("Animation Target can't be null"); + if (StartAnimationGroupPart(a, target, finalValue)) + matched = true; + } + + return matched; + } + + throw new ArgumentException(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs new file mode 100644 index 0000000000..004c2676ff --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition +{ + public class CompositionPropertySet : CompositionObject + { + private readonly Dictionary _variants = new Dictionary(); + private readonly Dictionary _objects = new Dictionary(); + + internal CompositionPropertySet(Compositor compositor) : base(compositor, null!) + { + } + + private protected override IChangeSetPool ChangeSetPool => throw new NotSupportedException(); + + internal void Set(string key, ExpressionVariant value) + { + _objects.Remove(key); + _variants[key] = value; + } + + internal void Set(string key, CompositionObject obj) + { + _objects[key] = obj ?? throw new ArgumentNullException(nameof(obj)); + _variants.Remove(key); + } + public void InsertColor(string propertyName, Avalonia.Media.Color value) => Set(propertyName, value); + + public void InsertMatrix3x2(string propertyName, Matrix3x2 value) => Set(propertyName, value); + + public void InsertMatrix4x4(string propertyName, Matrix4x4 value) => Set(propertyName, value); + + public void InsertQuaternion(string propertyName, Quaternion value) => Set(propertyName, value); + + public void InsertScalar(string propertyName, float value) => Set(propertyName, value); + public void InsertVector2(string propertyName, Vector2 value) => Set(propertyName, value); + + public void InsertVector3(string propertyName, Vector3 value) => Set(propertyName, value); + + public void InsertVector4(string propertyName, Vector4 value) => Set(propertyName, value); + + + CompositionGetValueStatus TryGetVariant(string key, out T value) where T : struct + { + value = default; + if (!_variants.TryGetValue(key, out var v)) + return _objects.ContainsKey(key) + ? CompositionGetValueStatus.TypeMismatch + : CompositionGetValueStatus.NotFound; + + return v.TryCast(out value) ? CompositionGetValueStatus.Succeeded : CompositionGetValueStatus.TypeMismatch; + } + + public CompositionGetValueStatus TryGetColor(string propertyName, out Avalonia.Media.Color value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetMatrix3x2(string propertyName, out Matrix3x2 value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetMatrix4x4(string propertyName, out Matrix4x4 value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetQuaternion(string propertyName, out Quaternion value) + => TryGetVariant(propertyName, out value); + + + public CompositionGetValueStatus TryGetScalar(string propertyName, out float value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetVector2(string propertyName, out Vector2 value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetVector3(string propertyName, out Vector3 value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetVector4(string propertyName, out Vector4 value) + => TryGetVariant(propertyName, out value); + + + public void InsertBoolean(string propertyName, bool value) => Set(propertyName, value); + + public CompositionGetValueStatus TryGetBoolean(string propertyName, out bool value) + => TryGetVariant(propertyName, out value); + + internal void ClearAll() + { + _objects.Clear(); + _variants.Clear(); + } + + internal void Clear(string key) + { + _objects.Remove(key); + _variants.Remove(key); + } + + internal PropertySetSnapshot Snapshot(bool server, int allowedNestingLevel) + { + var dic = new Dictionary(_objects.Count + _variants.Count); + foreach (var o in _objects) + { + if (o.Value is CompositionPropertySet ps) + { + if (allowedNestingLevel <= 0) + throw new InvalidOperationException("PropertySet depth limit reached"); + dic[o.Key] = new PropertySetSnapshot.Value(ps.Snapshot(server, allowedNestingLevel - 1)); + } + else if (o.Value.Server == null) + throw new InvalidOperationException($"Object of type {o.Value.GetType()} is not allowed"); + else + dic[o.Key] = new PropertySetSnapshot.Value(server ? (IExpressionObject) o.Value.Server : o.Value); + } + + foreach (var v in _variants) + dic[v.Key] = v.Value; + + return new PropertySetSnapshot(dic); + } + } + + public enum CompositionGetValueStatus + { + Succeeded, + TypeMismatch, + NotFound + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs new file mode 100644 index 0000000000..a8835ca668 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -0,0 +1,107 @@ +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Collections.Pooled; + +namespace Avalonia.Rendering.Composition +{ + public partial class CompositionTarget + { + partial void OnRootChanged() + { + if (Root != null) + Root.Root = this; + } + + partial void OnRootChanging() + { + if (Root != null) + Root.Root = null; + } + + public PooledList? TryHitTest(Vector2 point) + { + Server.Readback.NextRead(); + if (Root == null) + return null; + var res = new PooledList(); + HitTestCore(Root, point, res); + return res; + } + + public Vector2? TryTransformToVisual(CompositionVisual visual, Vector2 point) + { + if (visual.Root != this) + return null; + var v = visual; + var m = Matrix3x2.Identity; + while (v != null) + { + if (!TryGetInvertedTransform(v, out var cm)) + return null; + m = m * cm; + v = v.Parent; + } + + return Vector2.Transform(point, m); + } + + bool TryGetInvertedTransform(CompositionVisual visual, out Matrix3x2 matrix) + { + var m = visual.TryGetServerTransform(); + if (m == null) + { + matrix = default; + return false; + } + + // TODO: Use Matrix3x3 + var m32 = new Matrix3x2(m.Value.M11, m.Value.M12, m.Value.M21, m.Value.M22, m.Value.M41, m.Value.M42); + + return Matrix3x2.Invert(m32, out matrix); + } + + bool TryTransformTo(CompositionVisual visual, ref Vector2 v) + { + if (TryGetInvertedTransform(visual, out var m)) + { + v = Vector2.Transform(v, m); + return true; + } + + return false; + } + + bool HitTestCore(CompositionVisual visual, Vector2 point, PooledList result) + { + //TODO: Check readback too + if (visual.Visible == false) + return false; + if (!TryTransformTo(visual, ref point)) + return false; + if (point.X >= 0 && point.Y >= 0 && point.X <= visual.Size.X && point.Y <= visual.Size.Y) + { + bool success = false; + // Hit-test the current node + if (visual.HitTest(point)) + { + result.Add(visual); + success = true; + } + + // Inspect children too + if(visual is CompositionContainerVisual cv) + for (var c = cv.Children.Count - 1; c >= 0; c--) + { + var ch = cv.Children[c]; + var hit = HitTestCore(ch, point, result); + if (hit) + return true; + } + + return success; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs new file mode 100644 index 0000000000..217d8dd803 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using System.Threading.Tasks; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Threading; + + +namespace Avalonia.Rendering.Composition +{ + public partial class Compositor + { + private ServerCompositor _server; + private Batch _currentBatch; + private bool _implicitBatchCommitQueued; + private Action _implicitBatchCommit; + + internal Batch CurrentBatch => _currentBatch; + internal ServerCompositor Server => _server; + internal CompositionEasingFunction DefaultEasing { get; } + + private Compositor(ServerCompositor server) + { + _server = server; + _currentBatch = new Batch(); + _implicitBatchCommit = ImplicitBatchCommit; + DefaultEasing = new CubicBezierEasingFunction(this, + new Vector2(0.25f, 0.1f), new Vector2(0.25f, 1f)); + } + + public CompositionTarget CreateCompositionTarget(Func renderTargetFactory) + { + return new CompositionTarget(this, new ServerCompositionTarget(_server, renderTargetFactory)); + } + + public Task RequestCommitAsync() + { + var batch = CurrentBatch; + _currentBatch = new Batch(); + batch.CommitedAt = Server.Clock.Elapsed; + _server.EnqueueBatch(batch); + return batch.Completed; + } + + public static Compositor Create(IRenderLoop timer) + { + return new Compositor(new ServerCompositor(timer)); + } + + public void Dispose() + { + + } + + public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server)); + + public CompositionSolidColorVisual CreateSolidColorVisual() => new CompositionSolidColorVisual(this, + new ServerCompositionSolidColorVisual(_server)); + + public CompositionSolidColorVisual CreateSolidColorVisual(Avalonia.Media.Color color) + { + var v = new CompositionSolidColorVisual(this, new ServerCompositionSolidColorVisual(_server)); + v.Color = color; + return v; + } + + public CompositionSpriteVisual CreateSpriteVisual() => new CompositionSpriteVisual(this, new ServerCompositionSpriteVisual(_server)); + + public CompositionLinearGradientBrush CreateLinearGradientBrush() + => new CompositionLinearGradientBrush(this, new ServerCompositionLinearGradientBrush(_server)); + + public CompositionColorGradientStop CreateColorGradientStop() + => new CompositionColorGradientStop(this, new ServerCompositionColorGradientStop(_server)); + + public CompositionColorGradientStop CreateColorGradientStop(float offset, Avalonia.Media.Color color) + { + var stop = CreateColorGradientStop(); + stop.Offset = offset; + stop.Color = color; + return stop; + } + + // We want to make it 100% async later + /* + public CompositionBitmapSurface LoadBitmapSurface(Stream stream) + { + var bmp = _server.Backend.LoadCpuMemoryBitmap(stream); + return new CompositionBitmapSurface(this, bmp); + } + + public async Task LoadBitmapSurfaceAsync(Stream stream) + { + var bmp = await Task.Run(() => _server.Backend.LoadCpuMemoryBitmap(stream)); + return new CompositionBitmapSurface(this, bmp); + } + */ + public CompositionColorBrush CreateColorBrush(Avalonia.Media.Color color) => + new CompositionColorBrush(this, new ServerCompositionColorBrush(_server)) {Color = color}; + + public CompositionSurfaceBrush CreateSurfaceBrush() => + new CompositionSurfaceBrush(this, new ServerCompositionSurfaceBrush(_server)); + + /* + public CompositionGaussianBlurEffectBrush CreateGaussianBlurEffectBrush() => + new CompositionGaussianBlurEffectBrush(this, new ServerCompositionGaussianBlurEffectBrush(_server)); + + public CompositionBackdropBrush CreateBackdropBrush() => + new CompositionBackdropBrush(this, new ServerCompositionBackdropBrush(Server));*/ + + public ExpressionAnimation CreateExpressionAnimation() => new ExpressionAnimation(this); + + public ExpressionAnimation CreateExpressionAnimation(string expression) => new ExpressionAnimation(this) + { + Expression = expression + }; + + public ImplicitAnimationCollection CreateImplicitAnimationCollection() => new ImplicitAnimationCollection(this); + + public CompositionAnimationGroup CreateAnimationGroup() => new CompositionAnimationGroup(this); + + internal CustomDrawVisual CreateCustomDrawVisual(ICustomDrawVisualRenderer renderer, + ICustomDrawVisualHitTest? hitTest = null) where T : IEquatable => + new CustomDrawVisual(this, renderer, hitTest); + + public void QueueImplicitBatchCommit() + { + if(_implicitBatchCommitQueued) + return; + _implicitBatchCommitQueued = true; + Dispatcher.UIThread.Post(_implicitBatchCommit, DispatcherPriority.CompositionBatch); + } + + private void ImplicitBatchCommit() + { + _implicitBatchCommitQueued = false; + RequestCommitAsync(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs b/src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs new file mode 100644 index 0000000000..074c0a9ccf --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs @@ -0,0 +1,20 @@ +using System; + +namespace Avalonia.Rendering.Composition; + +partial class Compositor +{ + class CompositorRenderLoopTask : IRenderLoopTask + { + public bool NeedsUpdate { get; } + public void Update(TimeSpan time) + { + throw new NotImplementedException(); + } + + public void Render() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs new file mode 100644 index 0000000000..f650d3e995 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs @@ -0,0 +1,20 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition +{ + public class CompositionContainerVisual : CompositionVisual + { + public CompositionVisualCollection Children { get; } + internal CompositionContainerVisual(Compositor compositor, ServerCompositionContainerVisual server) : base(compositor, server) + { + Children = new CompositionVisualCollection(this, server.Children); + } + + private protected override void OnRootChanged() + { + foreach (var ch in Children) + ch.Root = Root; + base.OnRootChanged(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs b/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs new file mode 100644 index 0000000000..0505d6a46c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs @@ -0,0 +1,56 @@ +using System; +using System.Numerics; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition +{ + internal class CustomDrawVisual : CompositionContainerVisual where TData : IEquatable + { + private readonly ICustomDrawVisualHitTest? _hitTest; + + internal CustomDrawVisual(Compositor compositor, ICustomDrawVisualRenderer renderer, + ICustomDrawVisualHitTest? hitTest) : base(compositor, + new ServerCustomDrawVisual(compositor.Server, renderer)) + { + _hitTest = hitTest; + } + + private TData? _data; + + static bool Eq(TData? left, TData? right) + { + if (left == null && right == null) + return true; + if (left == null) + return false; + return left.Equals(right); + } + + public TData? Data + { + get => _data; + set + { + if (!Eq(_data, value)) + { + ((CustomDrawVisualChanges) Changes).Data.Value = value; + _data = value; + } + } + } + + private protected override IChangeSetPool ChangeSetPool => CustomDrawVisualChanges.Pool; + } + + public interface ICustomDrawVisualRenderer + { + void Render(IDrawingContextImpl canvas, TData? data); + } + + public interface ICustomDrawVisualHitTest + { + bool HitTest(TData data, Vector2 vector2); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs new file mode 100644 index 0000000000..aca8ef7c46 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs @@ -0,0 +1,81 @@ +using System; +using Avalonia.Collections.Pooled; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Drawing; + +internal class CompositionDrawList : PooledList> +{ + public CompositionDrawList() + { + + } + + public CompositionDrawList(int capacity) : base(capacity) + { + + } + + public override void Dispose() + { + foreach(var item in this) + item.Dispose(); + base.Dispose(); + } + + public CompositionDrawList Clone() + { + var clone = new CompositionDrawList(Count); + foreach (var r in this) + clone.Add(r.Clone()); + return clone; + } +} + +internal class CompositionDrawListBuilder +{ + private CompositionDrawList? _operations; + private bool _owns; + + public void Reset(CompositionDrawList? previousOperations) + { + _operations = previousOperations; + _owns = false; + } + + public CompositionDrawList DrawOperations => _operations ?? new CompositionDrawList(); + + void MakeWritable(int atIndex) + { + if(_owns) + return; + _owns = true; + var newOps = new CompositionDrawList(_operations?.Count ?? Math.Max(1, atIndex)); + if (_operations != null) + { + for (var c = 0; c < atIndex; c++) + newOps.Add(_operations[c].Clone()); + } + + _operations = newOps; + } + + public void ReplaceDrawOperation(int index, IDrawOperation node) + { + MakeWritable(index); + DrawOperations.Add(RefCountable.Create(node)); + } + + public void AddDrawOperation(IDrawOperation node) + { + MakeWritable(DrawOperations.Count); + DrawOperations.Add(RefCountable.Create(node)); + } + + public void TrimTo(int count) + { + if (count < DrawOperations.Count) + DrawOperations.RemoveRange(count, DrawOperations.Count - count); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs new file mode 100644 index 0000000000..c8e5d9e064 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -0,0 +1,383 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; +using Avalonia.VisualTree; +namespace Avalonia.Rendering.Composition; + +internal class CompositionDrawingContext : IDrawingContextImpl +{ + private CompositionDrawListBuilder _builder = new(); + private int _drawOperationIndex; + + /// + public Matrix Transform { get; set; } = Matrix.Identity; + + /// + public void Clear(Color color) + { + // Cannot clear a deferred scene. + } + + /// + public void Dispose() + { + // Nothing to do here since we allocate no unmanaged resources. + } + + public void BeginUpdate(CompositionDrawList list) + { + _builder.Reset(list); + _drawOperationIndex = 0; + } + + public CompositionDrawList EndUpdate() + { + _builder.TrimTo(_drawOperationIndex); + return _builder.DrawOperations!; + } + + /// + public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, brush, pen, geometry)) + { + Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush))); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode) + { + var next = NextDrawAs(); + + if (next == null || + !next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)) + { + Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) + { + // This method is currently only used to composite layers so shouldn't be called here. + throw new NotSupportedException(); + } + + /// + public void DrawLine(IPen pen, Point p1, Point p2) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) + { + Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush))); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, + BoxShadows boxShadows = default) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows)) + { + Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush))); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, material, rect)) + { + Add(new ExperimentalAcrylicNode(Transform, material, rect)); + } + else + { + ++_drawOperationIndex; + } + } + + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, brush, pen, rect)) + { + Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush))); + } + else + { + ++_drawOperationIndex; + } + } + + public void Custom(ICustomDrawOperation custom) + { + var next = NextDrawAs(); + if (next == null || !next.Item.Equals(Transform, custom)) + Add(new CustomDrawOperation(custom, Transform)); + else + ++_drawOperationIndex; + } + + /// + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) + { + Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground))); + } + + else + { + ++_drawOperationIndex; + } + } + + public IDrawingContextLayerImpl CreateLayer(Size size) + { + throw new NotSupportedException("Creating layers on a deferred drawing context not supported"); + } + + /// + public void PopClip() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new ClipNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PopGeometryClip() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new GeometryClipNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PopBitmapBlendMode() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new BitmapBlendModeNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PopOpacity() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new OpacityNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PopOpacityMask() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null, null)) + { + Add(new OpacityMaskNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushClip(Rect clip) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, clip)) + { + Add(new ClipNode(Transform, clip)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushClip(RoundedRect clip) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, clip)) + { + Add(new ClipNode(Transform, clip)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushGeometryClip(IGeometryImpl? clip) + { + if (clip is null) + return; + + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, clip)) + { + Add(new GeometryClipNode(Transform, clip)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushOpacity(double opacity) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(opacity)) + { + Add(new OpacityNode(opacity)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushOpacityMask(IBrush mask, Rect bounds) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(mask, bounds)) + { + Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask))); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(blendingMode)) + { + Add(new BitmapBlendModeNode(blendingMode)); + } + else + { + ++_drawOperationIndex; + } + } + + private void Add(T node) where T : class, IDrawOperation + { + if (_drawOperationIndex < _builder!.DrawOperations.Count) + { + _builder.ReplaceDrawOperation(_drawOperationIndex, node); + } + else + { + _builder.AddDrawOperation(node); + } + + ++_drawOperationIndex; + } + + private IRef? NextDrawAs() where T : class, IDrawOperation + { + return _drawOperationIndex < _builder!.DrawOperations.Count + ? _builder.DrawOperations[_drawOperationIndex] as IRef + : null; + } + + private IDictionary? CreateChildScene(IBrush? brush) + { + /* + var visualBrush = brush as VisualBrush; + + if (visualBrush != null) + { + var visual = visualBrush.Visual; + + if (visual != null) + { + (visual as IVisualBrushInitialize)?.EnsureInitialized(); + var scene = new Scene(visual); + _sceneBuilder.UpdateAll(scene); + return new Dictionary { { visualBrush.Visual, scene } }; + } + }*/ + + return null; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Enums.cs b/src/Avalonia.Base/Rendering/Composition/Enums.cs new file mode 100644 index 0000000000..e349845cbf --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Enums.cs @@ -0,0 +1,120 @@ +using System; + +namespace Avalonia.Rendering.Composition +{ + public enum CompositionBlendMode + { + /// No regions are enabled. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_clr.svg) + Clear, + + /// Only the source will be present. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src.svg) + Src, + + /// Only the destination will be present. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst.svg) + Dst, + + /// Source is placed over the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-over.svg) + SrcOver, + + /// Destination is placed over the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-over.svg) + DstOver, + + /// The source that overlaps the destination, replaces the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-in.svg) + SrcIn, + + /// Destination which overlaps the source, replaces the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-in.svg) + DstIn, + + /// Source is placed, where it falls outside of the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-out.svg) + SrcOut, + + /// Destination is placed, where it falls outside of the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-out.svg) + DstOut, + + /// Source which overlaps the destination, replaces the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-atop.svg) + SrcATop, + + /// Destination which overlaps the source replaces the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-atop.svg) + DstATop, + + /// The non-overlapping regions of source and destination are combined. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_xor.svg) + Xor, + + /// Display the sum of the source image and destination image. [Porter Duff Compositing Operators] + Plus, + + /// Multiplies all components (= alpha and color). [Separable Blend Modes] + Modulate, + + /// Multiplies the complements of the backdrop and source CompositionColorvalues, then complements the result. [Separable Blend Modes] + Screen, + + /// Multiplies or screens the colors, depending on the backdrop CompositionColorvalue. [Separable Blend Modes] + Overlay, + + /// Selects the darker of the backdrop and source colors. [Separable Blend Modes] + Darken, + + /// Selects the lighter of the backdrop and source colors. [Separable Blend Modes] + Lighten, + + /// Brightens the backdrop CompositionColorto reflect the source color. [Separable Blend Modes] + ColorDodge, + + /// Darkens the backdrop CompositionColorto reflect the source color. [Separable Blend Modes] + ColorBurn, + + /// Multiplies or screens the colors, depending on the source CompositionColorvalue. [Separable Blend Modes] + HardLight, + + /// Darkens or lightens the colors, depending on the source CompositionColorvalue. [Separable Blend Modes] + SoftLight, + + /// Subtracts the darker of the two constituent colors from the lighter color. [Separable Blend Modes] + Difference, + + /// Produces an effect similar to that of the Difference mode but lower in contrast. [Separable Blend Modes] + Exclusion, + + /// The source CompositionColoris multiplied by the destination CompositionColorand replaces the destination [Separable Blend Modes] + Multiply, + + /// Creates a CompositionColorwith the hue of the source CompositionColorand the saturation and luminosity of the backdrop color. [Non-Separable Blend Modes] + Hue, + + /// Creates a CompositionColorwith the saturation of the source CompositionColorand the hue and luminosity of the backdrop color. [Non-Separable Blend Modes] + Saturation, + + /// Creates a CompositionColorwith the hue and saturation of the source CompositionColorand the luminosity of the backdrop color. [Non-Separable Blend Modes] + Color, + + /// Creates a CompositionColorwith the luminosity of the source CompositionColorand the hue and saturation of the backdrop color. [Non-Separable Blend Modes] + Luminosity, + } + + public enum CompositionGradientExtendMode + { + Clamp, + Wrap, + Mirror + } + + [Flags] + public enum CompositionTileMode + { + None = 0, + TileX = 1, + TileY = 2, + FlipX = 4, + FlipY = 8, + Tile = TileX | TileY, + Flip = FlipX | FlipY + } + + public enum CompositionStretch + { + None = 0, + Fill = 1, + //TODO: Uniform, UniformToFill + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs new file mode 100644 index 0000000000..db9a26e301 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Utils; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal class BuiltInExpressionFfi : IExpressionForeignFunctionInterface + { + private readonly DelegateExpressionFfi _registry; + + static float Lerp(float a, float b, float p) => p * (b - a) + a; + + static Matrix3x2 Inverse(Matrix3x2 m) + { + Matrix3x2.Invert(m, out var r); + return r; + } + + static Matrix4x4 Inverse(Matrix4x4 m) + { + Matrix4x4.Invert(m, out var r); + return r; + } + + static float SmoothStep(float edge0, float edge1, float x) + { + var t = MathExt.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) + { + return new Vector2( + 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( + 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) + { + return new Vector4( + SmoothStep(edge0.X, edge1.X, x.X), + SmoothStep(edge0.Y, edge1.Y, x.Y), + SmoothStep(edge0.Z, edge1.Z, x.Z), + SmoothStep(edge0.W, edge1.W, x.W) + ); + } + + private BuiltInExpressionFfi() + { + _registry = new DelegateExpressionFfi + { + {"Abs", (float f) => Math.Abs(f)}, + {"Abs", (Vector2 v) => Vector2.Abs(v)}, + {"Abs", (Vector3 v) => Vector3.Abs(v)}, + {"Abs", (Vector4 v) => Vector4.Abs(v)}, + + {"ACos", (float f) => (float) Math.Acos(f)}, + {"ASin", (float f) => (float) Math.Asin(f)}, + {"ATan", (float f) => (float) Math.Atan(f)}, + {"Ceil", (float f) => (float) Math.Ceiling(f)}, + + {"Clamp", (float a1, float a2, float a3) => MathExt.Clamp(a1, a2, a3)}, + {"Clamp", (Vector2 a1, Vector2 a2, Vector2 a3) => Vector2.Clamp(a1, a2, a3)}, + {"Clamp", (Vector3 a1, Vector3 a2, Vector3 a3) => Vector3.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)}, + {"Cos", (float a) => (float) Math.Cos(a)}, + + /* + TODO: + ColorHsl(Float h, Float s, Float l) + ColorLerpHSL(Color colorTo, CompositionColorcolorFrom, Float progress) + */ + + { + "ColorLerp", (Avalonia.Media.Color to, Avalonia.Media.Color from, float progress) => + ColorInterpolator.LerpRGB(to, from, progress) + }, + { + "ColorLerpRGB", (Avalonia.Media.Color to, Avalonia.Media.Color from, float progress) => + ColorInterpolator.LerpRGB(to, from, progress) + }, + { + "ColorRGB", (float a, float r, float g, float b) => Avalonia.Media.Color.FromArgb( + (byte) MathExt.Clamp(a, 0, 255), + (byte) MathExt.Clamp(r, 0, 255), + (byte) MathExt.Clamp(g, 0, 255), + (byte) MathExt.Clamp(b, 0, 255) + ) + }, + + {"Distance", (Vector2 a1, Vector2 a2) => Vector2.Distance(a1, a2)}, + {"Distance", (Vector3 a1, Vector3 a2) => Vector3.Distance(a1, a2)}, + {"Distance", (Vector4 a1, Vector4 a2) => Vector4.Distance(a1, a2)}, + + {"DistanceSquared", (Vector2 a1, Vector2 a2) => Vector2.DistanceSquared(a1, a2)}, + {"DistanceSquared", (Vector3 a1, Vector3 a2) => Vector3.DistanceSquared(a1, a2)}, + {"DistanceSquared", (Vector4 a1, Vector4 a2) => Vector4.DistanceSquared(a1, a2)}, + + {"Floor", (float v) => (float) Math.Floor(v)}, + + {"Inverse", (Matrix3x2 v) => Inverse(v)}, + {"Inverse", (Matrix4x4 v) => Inverse(v)}, + + + {"Length", (Vector2 a1) => a1.Length()}, + {"Length", (Vector3 a1) => a1.Length()}, + {"Length", (Vector4 a1) => a1.Length()}, + {"Length", (Quaternion a1) => a1.Length()}, + + {"LengthSquared", (Vector2 a1) => a1.LengthSquared()}, + {"LengthSquared", (Vector3 a1) => a1.LengthSquared()}, + {"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", (Vector3 a1, Vector3 a2, float a3) => Vector3.Lerp(a1, a2, a3)}, + {"Lerp", (Vector4 a1, Vector4 a2, float a3) => Vector4.Lerp(a1, a2, a3)}, + + + {"Ln", (float f) => (float) Math.Log(f)}, + {"Log10", (float f) => (float) Math.Log10(f)}, + + {"Matrix3x2.CreateFromScale", (Vector2 v) => Matrix3x2.CreateScale(v)}, + {"Matrix3x2.CreateFromTranslation", (Vector2 v) => Matrix3x2.CreateTranslation(v)}, + {"Matrix3x2.CreateRotation", (float v) => Matrix3x2.CreateRotation(v)}, + {"Matrix3x2.CreateScale", (Vector2 v) => Matrix3x2.CreateScale(v)}, + {"Matrix3x2.CreateSkew", (float a1, float a2, Vector2 a3) => Matrix3x2.CreateSkew(a1, a2, a3)}, + {"Matrix3x2.CreateTranslation", (Vector2 v) => Matrix3x2.CreateScale(v)}, + { + "Matrix3x2", (float m11, float m12, float m21, float m22, float m31, float m32) => + new Matrix3x2(m11, m12, m21, m22, m31, m32) + }, + {"Matrix4x4.CreateFromAxisAngle", (Vector3 v, float angle) => Matrix4x4.CreateFromAxisAngle(v, angle)}, + {"Matrix4x4.CreateFromScale", (Vector3 v) => Matrix4x4.CreateScale(v)}, + {"Matrix4x4.CreateFromTranslation", (Vector3 v) => Matrix4x4.CreateTranslation(v)}, + {"Matrix4x4.CreateScale", (Vector3 v) => Matrix4x4.CreateScale(v)}, + {"Matrix4x4.CreateTranslation", (Vector3 v) => Matrix4x4.CreateScale(v)}, + {"Matrix4x4", (Matrix3x2 m) => new Matrix4x4(m)}, + { + "Matrix4x4", + (float m11, float m12, float m13, float m14, + float m21, float m22, float m23, float m24, + float m31, float m32, float m33, float m34, + float m41, float m42, float m43, float m44) => + new Matrix4x4( + m11, m12, m13, m14, + m21, m22, m23, m24, + m31, m32, m33, m34, + m41, m42, m43, m44) + }, + + + {"Max", (float a1, float a2) => Math.Max(a1, a2)}, + {"Max", (Vector2 a1, Vector2 a2) => Vector2.Max(a1, a2)}, + {"Max", (Vector3 a1, Vector3 a2) => Vector3.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", (Vector3 a1, Vector3 a2) => Vector3.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", (Vector3 a) => Vector3.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", (float a, float b, float c, float d) => new Quaternion(a, b, c, d)}, + + {"Round", (float a) => (float) Math.Round(a)}, + + {"Scale", (Matrix3x2 a, float b) => a * b}, + {"Scale", (Matrix4x4 a, float b) => a * b}, + {"Scale", (Vector2 a, float b) => a * b}, + {"Scale", (Vector3 a, float b) => 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", (Vector3 a1, Vector3 a2, Vector3 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 + {"Slerp", (float a1, float a2, float a3) => Lerp(a1, a2, a3)}, + {"Slerp", (Quaternion a1, Quaternion a2, float a3) => Quaternion.Slerp(a1, a2, a3)}, + + {"Sqrt", (float a) => (float) Math.Sqrt(a)}, + {"Square", (float a) => a * a}, + {"Tan", (float a) => (float) Math.Tan(a)}, + + {"ToRadians", (float a) => (float) (a * Math.PI / 180)}, + {"ToDegrees", (float a) => (float) (a * 180d / Math.PI)}, + + {"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)}, + {"Vector3", (Vector2 v2, float z) => new Vector3(v2, 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)}, + }; + } + + public bool Call(string name, IReadOnlyList arguments, out ExpressionVariant result) => + _registry.Call(name, arguments, out result); + + public static BuiltInExpressionFfi Instance { get; } = new BuiltInExpressionFfi(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs new file mode 100644 index 0000000000..002cf37522 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Avalonia.Media; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal class DelegateExpressionFfi : IExpressionForeignFunctionInterface, IEnumerable + { + struct FfiRecord + { + public VariantType[] Types; + public Func, ExpressionVariant> Delegate; + } + + private readonly Dictionary>> + _registry = new Dictionary>>(); + + public bool Call(string name, IReadOnlyList arguments, out ExpressionVariant result) + { + result = default; + if (!_registry.TryGetValue(name, out var nameGroup)) + return false; + if (!nameGroup.TryGetValue(arguments.Count, out var countGroup)) + return false; + foreach (var record in countGroup) + { + var match = true; + for (var c = 0; c < arguments.Count; c++) + { + if (record.Types[c] != arguments[c].Type) + { + match = false; + break; + } + } + + if (match) + { + result = record.Delegate(arguments); + return true; + } + } + + return false; + } + + // Stub for collection initializer + IEnumerator IEnumerable.GetEnumerator() => Array.Empty().GetEnumerator(); + + void Add(string name, Func, ExpressionVariant> cb, + params Type[] types) + { + if (!_registry.TryGetValue(name, out var nameGroup)) + _registry[name] = nameGroup = + new Dictionary>(); + if (!nameGroup.TryGetValue(types.Length, out var countGroup)) + nameGroup[types.Length] = countGroup = new List(); + + countGroup.Add(new FfiRecord + { + Types = types.Select(t => TypeMap[t]).ToArray(), + Delegate = cb + }); + } + + static readonly Dictionary TypeMap = new Dictionary + { + [typeof(bool)] = VariantType.Boolean, + [typeof(float)] = VariantType.Scalar, + [typeof(Vector2)] = VariantType.Vector2, + [typeof(Vector3)] = VariantType.Vector3, + [typeof(Vector4)] = VariantType.Vector4, + [typeof(Matrix3x2)] = VariantType.Matrix3x2, + [typeof(Matrix4x4)] = VariantType.Matrix4x4, + [typeof(Quaternion)] = VariantType.Quaternion, + [typeof(Color)] = VariantType.Color + }; + + public void Add(string name, Func cb) where T1 : struct + { + Add(name, args => cb(args[0].CastOrDefault()), typeof(T1)); + } + + public void Add(string name, Func cb) where T1 : struct where T2 : struct + { + Add(name, args => cb(args[0].CastOrDefault(), args[1].CastOrDefault()), typeof(T1), typeof(T2)); + } + + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct + { + Add(name, args => cb(args[0].CastOrDefault(), args[1].CastOrDefault(), args[2].CastOrDefault()), typeof(T1), typeof(T2), + typeof(T3)); + } + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct where T4 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault()), + typeof(T1), typeof(T2), typeof(T3), typeof(T4)); + } + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault(), + args[4].CastOrDefault()), + typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5)); + } + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct where T6 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault()), + typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6)); + } + + + public void Add(string name, + Func cb) + where T1 : struct + where T2 : struct + where T3 : struct + where T4 : struct + where T5 : struct + where T6 : struct + where T7 : struct + where T8 : struct + where T9 : struct + where T10 : struct + where T11 : struct + where T12 : struct + where T13 : struct + where T14 : struct + where T15 : struct + where T16 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault() + ), + typeof(T1), typeof(T2), typeof(T3), typeof(T4), + typeof(T5), typeof(T6), typeof(T7), typeof(T8), + typeof(T9), typeof(T10), typeof(T11), typeof(T12), + typeof(T13), typeof(T14), typeof(T15), typeof(T16) + ); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs new file mode 100644 index 0000000000..5577d2b52a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs @@ -0,0 +1,331 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal abstract class Expression + { + public abstract ExpressionType Type { get; } + public static Expression Parse(string expression) + { + return ExpressionParser.Parse(expression.AsSpan()); + } + + public abstract ExpressionVariant Evaluate(ref ExpressionEvaluationContext context); + + protected abstract string Print(); + public override string ToString() => Print(); + + internal static string OperatorName(ExpressionType t) + { + var attr = typeof(ExpressionType).GetMember(t.ToString())[0] + .GetCustomAttribute(); + if (attr != null) + return attr.Name; + return t.ToString(); + } + } + + internal class PrettyPrintStringAttribute : Attribute + { + public string Name { get; } + + public PrettyPrintStringAttribute(string name) + { + Name = name; + } + } + + internal enum ExpressionType + { + // Binary operators + [PrettyPrintString("+")] + Add, + [PrettyPrintString("-")] + Subtract, + [PrettyPrintString("/")] + Divide, + [PrettyPrintString("*")] + Multiply, + [PrettyPrintString(">")] + MoreThan, + [PrettyPrintString("<")] + LessThan, + [PrettyPrintString(">=")] + MoreThanOrEqual, + [PrettyPrintString("<=")] + LessThanOrEqual, + [PrettyPrintString("&&")] + LogicalAnd, + [PrettyPrintString("||")] + LogicalOr, + [PrettyPrintString("%")] + Remainder, + [PrettyPrintString("==")] + Equals, + [PrettyPrintString("!=")] + NotEquals, + // Unary operators + [PrettyPrintString("!")] + Not, + [PrettyPrintString("-")] + UnaryMinus, + // The rest + MemberAccess, + Parameter, + FunctionCall, + Keyword, + Constant, + ConditionalExpression + } + + internal enum ExpressionKeyword + { + StartingValue, + CurrentValue, + FinalValue, + Target, + Pi, + True, + False + } + + internal class ConditionalExpression : Expression + { + public Expression Condition { get; } + public Expression TruePart { get; } + public Expression FalsePart { get; } + public override ExpressionType Type => ExpressionType.ConditionalExpression; + + public ConditionalExpression(Expression condition, Expression truePart, Expression falsePart) + { + Condition = condition; + TruePart = truePart; + FalsePart = falsePart; + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + var cond = Condition.Evaluate(ref context); + if (cond.Type == VariantType.Boolean && cond.Boolean) + return TruePart.Evaluate(ref context); + return FalsePart.Evaluate(ref context); + } + + protected override string Print() => $"({Condition}) ? ({TruePart}) : ({FalsePart})"; + } + + internal class ConstantExpression : Expression + { + public float Constant { get; } + public override ExpressionType Type => ExpressionType.Constant; + + public ConstantExpression(float constant) + { + Constant = constant; + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) => Constant; + protected override string Print() => Constant.ToString(CultureInfo.InvariantCulture); + } + + internal class FunctionCallExpression : Expression + { + public string Name { get; } + public List Parameters { get; } + public override ExpressionType Type => ExpressionType.FunctionCall; + + public FunctionCallExpression(string name, List parameters) + { + Name = name; + Parameters = parameters; + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + if (context.ForeignFunctionInterface == null) + return default; + var args = new List(); + foreach (var expr in Parameters) + args.Add(expr.Evaluate(ref context)); + if (!context.ForeignFunctionInterface.Call(Name, args, out var res)) + return default; + return res; + } + + protected override string Print() + { + return Name + "( (" + string.Join("), (", Parameters) + ") )"; + } + } + + internal class MemberAccessExpression : Expression + { + public override ExpressionType Type => ExpressionType.MemberAccess; + public Expression Target { get; } + public string Member { get; } + + public MemberAccessExpression(Expression target, string member) + { + Target = target; + Member = string.Intern(member); + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + if (Target is KeywordExpression ke + && ke.Keyword == ExpressionKeyword.Target) + return context.Target.GetProperty(Member); + if (Target is ParameterExpression pe) + { + var obj = context.Parameters?.GetObjectParameter(pe.Name); + if (obj != null) + return obj.GetProperty(Member); + } + + return Target.Evaluate(ref context).GetProperty(Member); + } + + protected override string Print() + { + return "(" + Target.ToString() + ")." + Member; + } + } + + internal class ParameterExpression : Expression + { + public string Name { get; } + public override ExpressionType Type => ExpressionType.Parameter; + + public ParameterExpression(string name) + { + Name = name; + } + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + return context.Parameters?.GetParameter(Name) ?? default; + } + + protected override string Print() + { + return "{" + Name + "}"; + } + } + + internal class KeywordExpression : Expression + { + public override ExpressionType Type => ExpressionType.Keyword; + public ExpressionKeyword Keyword { get; } + + public KeywordExpression(ExpressionKeyword keyword) + { + Keyword = keyword; + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + if (Keyword == ExpressionKeyword.StartingValue) + return context.StartingValue; + if (Keyword == ExpressionKeyword.CurrentValue) + return context.CurrentValue; + if (Keyword == ExpressionKeyword.FinalValue) + return context.FinalValue; + if (Keyword == ExpressionKeyword.Target) + // should be handled by MemberAccess + return default; + if (Keyword == ExpressionKeyword.True) + return true; + if (Keyword == ExpressionKeyword.False) + return false; + if (Keyword == ExpressionKeyword.Pi) + return (float) Math.PI; + return default; + } + + protected override string Print() + { + return "[" + Keyword + "]"; + } + } + + internal class UnaryExpression : Expression + { + public Expression Parameter { get; } + public override ExpressionType Type { get; } + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + if (Type == ExpressionType.Not) + return !Parameter.Evaluate(ref context); + if (Type == ExpressionType.UnaryMinus) + return -Parameter.Evaluate(ref context); + return default; + } + + protected override string Print() + { + return OperatorName(Type) + Parameter; + } + + public UnaryExpression(Expression parameter, ExpressionType type) + { + Parameter = parameter; + Type = type; + } + } + + internal class BinaryExpression : Expression + { + public Expression Left { get; } + public Expression Right { get; } + public override ExpressionType Type { get; } + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + var left = Left.Evaluate(ref context); + var right = Right.Evaluate(ref context); + if (Type == ExpressionType.Add) + return left + right; + if (Type == ExpressionType.Subtract) + return left - right; + if (Type == ExpressionType.Multiply) + return left * right; + if (Type == ExpressionType.Divide) + return left / right; + if (Type == ExpressionType.Remainder) + return left % right; + if (Type == ExpressionType.MoreThan) + return left > right; + if (Type == ExpressionType.LessThan) + return left < right; + if (Type == ExpressionType.MoreThanOrEqual) + return left > right; + if (Type == ExpressionType.LessThanOrEqual) + return left < right; + if (Type == ExpressionType.LogicalAnd) + return left.And(right); + if (Type == ExpressionType.LogicalOr) + return left.Or(right); + if (Type == ExpressionType.Equals) + return left.EqualsTo(right); + if (Type == ExpressionType.NotEquals) + return left.NotEqualsTo(right); + return default; + } + + protected override string Print() + { + return "(" + Left + OperatorName(Type) + Right + ")"; + } + + public BinaryExpression(Expression left, Expression right, ExpressionType type) + { + Left = left; + Right = right; + Type = type; + } + } + + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs new file mode 100644 index 0000000000..a7ddabd70d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal struct ExpressionEvaluationContext + { + public ExpressionVariant StartingValue { get; set; } + public ExpressionVariant CurrentValue { get; set; } + public ExpressionVariant FinalValue { get; set; } + public IExpressionObject Target { get; set; } + public IExpressionParameterCollection Parameters { get; set; } + public IExpressionForeignFunctionInterface ForeignFunctionInterface { get; set; } + } + + internal interface IExpressionObject + { + ExpressionVariant GetProperty(string name); + } + + internal interface IExpressionParameterCollection + { + public ExpressionVariant GetParameter(string name); + + public IExpressionObject GetObjectParameter(string name); + } + + internal interface IExpressionForeignFunctionInterface + { + bool Call(string name, IReadOnlyList arguments, out ExpressionVariant result); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs new file mode 100644 index 0000000000..6a207a3bf7 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs @@ -0,0 +1,14 @@ +using System; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal class ExpressionParseException : Exception + { + public int Position { get; } + + public ExpressionParseException(string message, int position) : base(message) + { + Position = position; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs new file mode 100644 index 0000000000..5924bb8f1b --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +// ReSharper disable StringLiteralTypo + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal class ExpressionParser + { + public static Expression Parse(ReadOnlySpan s) + { + var p = new TokenParser(s); + var parsed = ParseTillTerminator(ref p, "", false, false, out _); + p.SkipWhitespace(); + if (p.Length != 0) + throw new ExpressionParseException("Unexpected data ", p.Position); + return parsed; + } + + private static ReadOnlySpan Dot => ".".AsSpan(); + static bool TryParseAtomic(ref TokenParser parser, + [MaybeNullWhen(returnValue: false)] out Expression expr) + { + // We can parse keywords, parameter names and constants + expr = null; + if (parser.TryParseKeywordLowerCase("this.startingvalue")) + expr = new KeywordExpression(ExpressionKeyword.StartingValue); + else if(parser.TryParseKeywordLowerCase("this.currentvalue")) + expr = new KeywordExpression(ExpressionKeyword.CurrentValue); + else if(parser.TryParseKeywordLowerCase("this.finalvalue")) + expr = new KeywordExpression(ExpressionKeyword.FinalValue); + else if(parser.TryParseKeywordLowerCase("pi")) + expr = new KeywordExpression(ExpressionKeyword.Pi); + else if(parser.TryParseKeywordLowerCase("true")) + expr = new KeywordExpression(ExpressionKeyword.True); + else if(parser.TryParseKeywordLowerCase("false")) + expr = new KeywordExpression(ExpressionKeyword.False); + else if (parser.TryParseKeywordLowerCase("this.target")) + expr = new KeywordExpression(ExpressionKeyword.Target); + + if (expr != null) + return true; + + if (parser.TryParseIdentifier(out var identifier)) + { + expr = new ParameterExpression(identifier.ToString()); + return true; + } + + if(parser.TryParseFloat(out var scalar)) + { + expr = new ConstantExpression(scalar); + return true; + } + + return false; + + } + + static bool TryParseOperator(ref TokenParser parser, out ExpressionType op) + { + op = (ExpressionType) (-1); + if (parser.TryConsume("||")) + op = ExpressionType.LogicalOr; + else if (parser.TryConsume("&&")) + op = ExpressionType.LogicalAnd; + else if (parser.TryConsume(">=")) + op = ExpressionType.MoreThanOrEqual; + else if (parser.TryConsume("<=")) + op = ExpressionType.LessThanOrEqual; + else if (parser.TryConsume("==")) + op = ExpressionType.Equals; + else if (parser.TryConsume("!=")) + op = ExpressionType.NotEquals; + else if (parser.TryConsumeAny("+-/*><%".AsSpan(), out var sop)) + { +#pragma warning disable CS8509 + op = sop switch +#pragma warning restore CS8509 + { + '+' => ExpressionType.Add, + '-' => ExpressionType.Subtract, + '/' => ExpressionType.Divide, + '*' => ExpressionType.Multiply, + '<' => ExpressionType.LessThan, + '>' => ExpressionType.MoreThan, + '%' => ExpressionType.Remainder + }; + } + else + return false; + + return true; + } + + + struct ExpressionOperatorGroup + { + private List _expressions; + private List _operators; + private Expression? _first; + + public bool NotEmpty => !Empty; + public bool Empty => _expressions == null && _first == null; + + public void AppendFirst(Expression expr) + { + if (NotEmpty) + throw new InvalidOperationException(); + _first = expr; + } + + public void AppendWithOperator(Expression expr, ExpressionType op) + { + if (_expressions == null) + { + if (_first == null) + throw new InvalidOperationException(); + _expressions = new List(); + _expressions.Add(_first); + _first = null; + _operators = new List(); + } + _expressions.Add(expr); + _operators.Add(op); + } + + // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/ + private static readonly ExpressionType[][] OperatorPrecedenceGroups = new[] + { + // multiplicative + new[] {ExpressionType.Multiply, ExpressionType.Divide, ExpressionType.Remainder}, + // additive + new[] {ExpressionType.Add, ExpressionType.Subtract}, + // relational + new[] {ExpressionType.MoreThan, ExpressionType.MoreThanOrEqual, ExpressionType.LessThan, ExpressionType.LessThanOrEqual}, + // equality + new[] {ExpressionType.Equals, ExpressionType.NotEquals}, + // conditional AND + new[] {ExpressionType.LogicalAnd}, + // conditional OR + new[]{ ExpressionType.LogicalOr}, + }; + + private static readonly ExpressionType[][] OperatorPrecedenceGroupsReversed = + OperatorPrecedenceGroups.Reverse().ToArray(); + + // a*b+c [a,b,c] [*,+], call with (0, 2) + // ToExpression(a*b) + ToExpression(c) + // a+b*c -> ToExpression(a) + ToExpression(b*c) + Expression ToExpression(int from, int to) + { + if (to - from == 0) + return _expressions[from]; + + if (to - from == 1) + return new BinaryExpression(_expressions[from], _expressions[to], _operators[from]); + + foreach (var grp in OperatorPrecedenceGroupsReversed) + { + for (var c = from; c < to; c++) + { + var currentOperator = _operators[c]; + foreach(var operatorFromGroup in grp) + if (currentOperator == operatorFromGroup) + { + // We are dividing the expression right here + var left = ToExpression(from, c); + var right = ToExpression(c + 1, to); + return new BinaryExpression(left, right, currentOperator); + } + } + } + + // We shouldn't ever get here, if we are, there is something wrong in the code + throw new ExpressionParseException("Expression parsing algorithm bug in ToExpression", 0); + } + + public Expression ToExpression() + { + if (_expressions == null) + return _first ?? throw new InvalidOperationException(); + return ToExpression(0, _expressions.Count - 1); + } + } + + static Expression ParseTillTerminator(ref TokenParser parser, string terminatorChars, + bool throwOnTerminator, + bool throwOnEnd, + out char? token) + { + ExpressionOperatorGroup left = default; + token = null; + while (true) + { + if (parser.TryConsumeAny(terminatorChars.AsSpan(), out var consumedToken)) + { + if (throwOnTerminator || left.Empty) + throw new ExpressionParseException($"Unexpected '{token}'", parser.Position - 1); + token = consumedToken; + return left.ToExpression(); + } + parser.SkipWhitespace(); + if (parser.Length == 0) + { + if (throwOnEnd || left.Empty) + throw new ExpressionParseException("Unexpected end of expression", parser.Position); + return left.ToExpression(); + } + + ExpressionType? op = null; + if (left.NotEmpty) + { + if (parser.TryConsume('?')) + { + var truePart = ParseTillTerminator(ref parser, ":", + false, true, out _); + // pass through the current parsing rules to consume the rest + var falsePart = ParseTillTerminator(ref parser, terminatorChars, throwOnTerminator, throwOnEnd, + out token); + + return new ConditionalExpression(left.ToExpression(), truePart, falsePart); + } + + // We expect a binary operator here + if (!TryParseOperator(ref parser, out var sop)) + throw new ExpressionParseException("Unexpected token", parser.Position); + op = sop; + } + + // We expect an expression to be parsed (either due to expecting a binary operator or parsing the first part + var applyNegation = false; + while (parser.TryConsume('!')) + applyNegation = !applyNegation; + + var applyUnaryMinus = false; + while (parser.TryConsume('-')) + applyUnaryMinus = !applyUnaryMinus; + + Expression? parsed; + + if (parser.TryConsume('(')) + parsed = ParseTillTerminator(ref parser, ")", false, true, out _); + else if (parser.TryParseCall(out var functionName)) + { + var parameterList = new List(); + while (true) + { + parameterList.Add(ParseTillTerminator(ref parser, ",)", false, true, out var closingToken)); + if (closingToken == ')') + break; + if (closingToken != ',') + throw new ExpressionParseException("Unexpected end of the expression", parser.Position); + } + + parsed = new FunctionCallExpression(functionName.ToString(), parameterList); + } + else if (TryParseAtomic(ref parser, out parsed)) + { + // do nothing + } + else + throw new ExpressionParseException("Unexpected token", parser.Position); + + + // Parse any following member accesses + while (parser.TryConsume('.')) + { + if(!parser.TryParseIdentifier(out var memberName)) + throw new ExpressionParseException("Unexpected token", parser.Position); + + parsed = new MemberAccessExpression(parsed, memberName.ToString()); + } + + // Apply ! operator + if (applyNegation) + parsed = new UnaryExpression(parsed, ExpressionType.Not); + + if (applyUnaryMinus) + { + if(parsed is ConstantExpression constexpr) + parsed = new ConstantExpression(-constexpr.Constant); + else parsed = new UnaryExpression(parsed, ExpressionType.UnaryMinus); + } + + if (left.Empty) + left.AppendFirst(parsed); + else + left.AppendWithOperator(parsed, op!.Value); + } + + + + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs new file mode 100644 index 0000000000..8c6af5cb0c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs @@ -0,0 +1,739 @@ +using System; +using System.Globalization; +using System.Numerics; +using System.Runtime.InteropServices; +using Avalonia.Media; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal enum VariantType + { + Invalid, + Boolean, + Scalar, + Double, + Vector2, + Vector3, + Vector4, + AvaloniaMatrix, + Matrix3x2, + Matrix4x4, + Quaternion, + Color + } + + [StructLayout(LayoutKind.Explicit)] + internal struct ExpressionVariant + { + [FieldOffset(0)] public VariantType Type; + + [FieldOffset(4)] public bool Boolean; + [FieldOffset(4)] public float Scalar; + [FieldOffset(4)] public double Double; + [FieldOffset(4)] public Vector2 Vector2; + [FieldOffset(4)] public Vector3 Vector3; + [FieldOffset(4)] public Vector4 Vector4; + [FieldOffset(4)] public Matrix AvaloniaMatrix; + [FieldOffset(4)] public Matrix3x2 Matrix3x2; + [FieldOffset(4)] public Matrix4x4 Matrix4x4; + [FieldOffset(4)] public Quaternion Quaternion; + [FieldOffset(4)] public Color Color; + + + public ExpressionVariant GetProperty(string property) + { + if (Type == VariantType.Vector2) + { + if (ReferenceEquals(property, "X")) + return Vector2.X; + if (ReferenceEquals(property, "Y")) + return Vector2.Y; + return default; + } + + if (Type == VariantType.Vector3) + { + if (ReferenceEquals(property, "X")) + return Vector3.X; + if (ReferenceEquals(property, "Y")) + return Vector3.Y; + if (ReferenceEquals(property, "Z")) + return Vector3.Z; + if(ReferenceEquals(property, "XY")) + return new Vector2(Vector3.X, Vector3.Y); + if(ReferenceEquals(property, "YX")) + return new Vector2(Vector3.Y, Vector3.X); + if(ReferenceEquals(property, "XZ")) + return new Vector2(Vector3.X, Vector3.Z); + if(ReferenceEquals(property, "ZX")) + return new Vector2(Vector3.Z, Vector3.X); + if(ReferenceEquals(property, "YZ")) + return new Vector2(Vector3.Y, Vector3.Z); + if(ReferenceEquals(property, "ZY")) + return new Vector2(Vector3.Z, Vector3.Y); + return default; + } + + if (Type == VariantType.Vector4) + { + if (ReferenceEquals(property, "X")) + return Vector4.X; + if (ReferenceEquals(property, "Y")) + return Vector4.Y; + if (ReferenceEquals(property, "Z")) + return Vector4.Z; + if (ReferenceEquals(property, "W")) + return Vector4.W; + return default; + } + + if (Type == VariantType.Matrix3x2) + { + if (ReferenceEquals(property, "M11")) + return Matrix3x2.M11; + if (ReferenceEquals(property, "M12")) + return Matrix3x2.M12; + if (ReferenceEquals(property, "M21")) + return Matrix3x2.M21; + if (ReferenceEquals(property, "M22")) + return Matrix3x2.M22; + if (ReferenceEquals(property, "M31")) + return Matrix3x2.M31; + if (ReferenceEquals(property, "M32")) + return Matrix3x2.M32; + return default; + } + + if (Type == VariantType.AvaloniaMatrix) + { + if (ReferenceEquals(property, "M11")) + return AvaloniaMatrix.M11; + if (ReferenceEquals(property, "M12")) + return AvaloniaMatrix.M12; + if (ReferenceEquals(property, "M21")) + return AvaloniaMatrix.M21; + if (ReferenceEquals(property, "M22")) + return AvaloniaMatrix.M22; + if (ReferenceEquals(property, "M31")) + return AvaloniaMatrix.M31; + if (ReferenceEquals(property, "M32")) + return AvaloniaMatrix.M32; + return default; + } + + if (Type == VariantType.Matrix4x4) + { + if (ReferenceEquals(property, "M11")) + return Matrix4x4.M11; + if (ReferenceEquals(property, "M12")) + return Matrix4x4.M12; + if (ReferenceEquals(property, "M13")) + return Matrix4x4.M13; + if (ReferenceEquals(property, "M14")) + return Matrix4x4.M14; + if (ReferenceEquals(property, "M21")) + return Matrix4x4.M21; + if (ReferenceEquals(property, "M22")) + return Matrix4x4.M22; + if (ReferenceEquals(property, "M23")) + return Matrix4x4.M23; + if (ReferenceEquals(property, "M24")) + return Matrix4x4.M24; + if (ReferenceEquals(property, "M31")) + return Matrix4x4.M31; + if (ReferenceEquals(property, "M32")) + return Matrix4x4.M32; + if (ReferenceEquals(property, "M33")) + return Matrix4x4.M33; + if (ReferenceEquals(property, "M34")) + return Matrix4x4.M34; + if (ReferenceEquals(property, "M41")) + return Matrix4x4.M41; + if (ReferenceEquals(property, "M42")) + return Matrix4x4.M42; + if (ReferenceEquals(property, "M43")) + return Matrix4x4.M43; + if (ReferenceEquals(property, "M44")) + return Matrix4x4.M44; + return default; + } + + if (Type == VariantType.Quaternion) + { + if (ReferenceEquals(property, "X")) + return Quaternion.X; + if (ReferenceEquals(property, "Y")) + return Quaternion.Y; + if (ReferenceEquals(property, "Z")) + return Quaternion.Z; + if (ReferenceEquals(property, "W")) + return Quaternion.W; + return default; + } + + if (Type == VariantType.Color) + { + if (ReferenceEquals(property, "A")) + return Color.A; + if (ReferenceEquals(property, "R")) + return Color.R; + if (ReferenceEquals(property, "G")) + return Color.G; + if (ReferenceEquals(property, "B")) + return Color.B; + return default; + } + + return default; + } + + public static implicit operator ExpressionVariant(bool value) => + new ExpressionVariant + { + Type = VariantType.Boolean, + Boolean = value + }; + + public static implicit operator ExpressionVariant(float scalar) => + new ExpressionVariant + { + Type = VariantType.Scalar, + Scalar = scalar + }; + + public static implicit operator ExpressionVariant(double d) => + new ExpressionVariant + { + Type = VariantType.Double, + Double = d + }; + + + public static implicit operator ExpressionVariant(Vector2 value) => + new ExpressionVariant + { + Type = VariantType.Vector2, + Vector2 = value + }; + + + public static implicit operator ExpressionVariant(Vector3 value) => + new ExpressionVariant + { + Type = VariantType.Vector3, + Vector3 = value + }; + + + public static implicit operator ExpressionVariant(Vector4 value) => + new ExpressionVariant + { + Type = VariantType.Vector4, + Vector4 = value + }; + + public static implicit operator ExpressionVariant(Matrix3x2 value) => + new ExpressionVariant + { + Type = VariantType.Matrix3x2, + Matrix3x2 = value + }; + + public static implicit operator ExpressionVariant(Matrix value) => + new ExpressionVariant + { + Type = VariantType.Matrix3x2, + AvaloniaMatrix = value + }; + + public static implicit operator ExpressionVariant(Matrix4x4 value) => + new ExpressionVariant + { + Type = VariantType.Matrix4x4, + Matrix4x4 = value + }; + + public static implicit operator ExpressionVariant(Quaternion value) => + new ExpressionVariant + { + Type = VariantType.Quaternion, + Quaternion = value + }; + + public static implicit operator ExpressionVariant(Avalonia.Media.Color value) => + new ExpressionVariant + { + Type = VariantType.Color, + Color = value + }; + + public static ExpressionVariant operator +(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type != right.Type || left.Type == VariantType.Invalid) + return default; + + if (left.Type == VariantType.Scalar) + return left.Scalar + right.Scalar; + + if (left.Type == VariantType.Double) + return left.Double + right.Double; + + if (left.Type == VariantType.Vector2) + return left.Vector2 + right.Vector2; + + if (left.Type == VariantType.Vector3) + return left.Vector3 + right.Vector3; + + if (left.Type == VariantType.Vector4) + return left.Vector4 + right.Vector4; + + if (left.Type == VariantType.Matrix3x2) + return left.Matrix3x2 + right.Matrix3x2; + + if (left.Type == VariantType.AvaloniaMatrix) + return left.AvaloniaMatrix + right.AvaloniaMatrix; + + if (left.Type == VariantType.Matrix4x4) + return left.Matrix4x4 + right.Matrix4x4; + + if (left.Type == VariantType.Quaternion) + return left.Quaternion + right.Quaternion; + + return default; + } + + public static ExpressionVariant operator -(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type != right.Type || left.Type == VariantType.Invalid) + return default; + + if (left.Type == VariantType.Scalar) + return left.Scalar - right.Scalar; + + if (left.Type == VariantType.Double) + return left.Double - right.Double; + + if (left.Type == VariantType.Vector2) + return left.Vector2 - right.Vector2; + + if (left.Type == VariantType.Vector3) + return left.Vector3 - right.Vector3; + + if (left.Type == VariantType.Vector4) + return left.Vector4 - right.Vector4; + + if (left.Type == VariantType.Matrix3x2) + return left.Matrix3x2 - right.Matrix3x2; + + if (left.Type == VariantType.AvaloniaMatrix) + return left.AvaloniaMatrix - right.AvaloniaMatrix; + + if (left.Type == VariantType.Matrix4x4) + return left.Matrix4x4 - right.Matrix4x4; + + if (left.Type == VariantType.Quaternion) + return left.Quaternion - right.Quaternion; + + return default; + } + + public static ExpressionVariant operator -(ExpressionVariant left) + { + + if (left.Type == VariantType.Scalar) + return -left.Scalar; + + if (left.Type == VariantType.Double) + return -left.Double; + + if (left.Type == VariantType.Vector2) + return -left.Vector2; + + if (left.Type == VariantType.Vector3) + return -left.Vector3; + + if (left.Type == VariantType.Vector4) + return -left.Vector4; + + if (left.Type == VariantType.Matrix3x2) + return -left.Matrix3x2; + + if (left.Type == VariantType.AvaloniaMatrix) + return -left.AvaloniaMatrix; + + if (left.Type == VariantType.Matrix4x4) + return -left.Matrix4x4; + + if (left.Type == VariantType.Quaternion) + return -left.Quaternion; + + return default; + } + + public static ExpressionVariant operator *(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Invalid || right.Type == VariantType.Invalid) + return default; + + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar * right.Scalar; + + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double * right.Double; + + if (left.Type == VariantType.Vector2 && right.Type == VariantType.Vector2) + return left.Vector2 * right.Vector2; + + if (left.Type == VariantType.Vector2 && right.Type == VariantType.Scalar) + return left.Vector2 * right.Scalar; + + if (left.Type == VariantType.Vector3 && right.Type == VariantType.Vector3) + return left.Vector3 * right.Vector3; + + if (left.Type == VariantType.Vector3 && right.Type == VariantType.Scalar) + return left.Vector3 * right.Scalar; + + if (left.Type == VariantType.Vector4 && right.Type == VariantType.Vector4) + return left.Vector4 * right.Vector4; + + if (left.Type == VariantType.Vector4 && right.Type == VariantType.Scalar) + return left.Vector4 * right.Scalar; + + if (left.Type == VariantType.Matrix3x2 && right.Type == VariantType.Matrix3x2) + return left.Matrix3x2 * right.Matrix3x2; + + if (left.Type == VariantType.Matrix3x2 && right.Type == VariantType.Scalar) + return left.Matrix3x2 * right.Scalar; + + if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.AvaloniaMatrix) + return left.AvaloniaMatrix * right.AvaloniaMatrix; + + if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.Scalar) + return left.AvaloniaMatrix * (double)right.Scalar; + + if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.Double) + return left.AvaloniaMatrix * right.Double; + + if (left.Type == VariantType.Matrix4x4 && right.Type == VariantType.Matrix4x4) + return left.Matrix4x4 * right.Matrix4x4; + + if (left.Type == VariantType.Matrix4x4 && right.Type == VariantType.Scalar) + return left.Matrix4x4 * right.Scalar; + + if (left.Type == VariantType.Quaternion && right.Type == VariantType.Quaternion) + return left.Quaternion * right.Quaternion; + + if (left.Type == VariantType.Quaternion && right.Type == VariantType.Scalar) + return left.Quaternion * right.Scalar; + + return default; + } + + public static ExpressionVariant operator /(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Invalid || right.Type == VariantType.Invalid) + return default; + + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar / right.Scalar; + + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double / right.Double; + + if (left.Type == VariantType.Vector2 && right.Type == VariantType.Vector2) + return left.Vector2 / right.Vector2; + + if (left.Type == VariantType.Vector2 && right.Type == VariantType.Scalar) + return left.Vector2 / right.Scalar; + + if (left.Type == VariantType.Vector3 && right.Type == VariantType.Vector3) + return left.Vector3 / right.Vector3; + + if (left.Type == VariantType.Vector3 && right.Type == VariantType.Scalar) + return left.Vector3 / right.Scalar; + + if (left.Type == VariantType.Vector4 && right.Type == VariantType.Vector4) + return left.Vector4 / right.Vector4; + + if (left.Type == VariantType.Vector4 && right.Type == VariantType.Scalar) + return left.Vector4 / right.Scalar; + + if (left.Type == VariantType.Quaternion && right.Type == VariantType.Quaternion) + return left.Quaternion / right.Quaternion; + + return default; + } + + public ExpressionVariant EqualsTo(ExpressionVariant right) + { + if (Type != right.Type || Type == VariantType.Invalid) + return default; + + if (Type == VariantType.Scalar) + return Scalar == right.Scalar; + + + if (Type == VariantType.Double) + return Double == right.Double; + + if (Type == VariantType.Vector2) + return Vector2 == right.Vector2; + + if (Type == VariantType.Vector3) + return Vector3 == right.Vector3; + + if (Type == VariantType.Vector4) + return Vector4 == right.Vector4; + + if (Type == VariantType.Boolean) + return Boolean == right.Boolean; + + if (Type == VariantType.Matrix3x2) + return Matrix3x2 == right.Matrix3x2; + + if (Type == VariantType.AvaloniaMatrix) + return AvaloniaMatrix == right.AvaloniaMatrix; + + if (Type == VariantType.Matrix4x4) + return Matrix4x4 == right.Matrix4x4; + + if (Type == VariantType.Quaternion) + return Quaternion == right.Quaternion; + + return default; + } + + public ExpressionVariant NotEqualsTo(ExpressionVariant right) + { + var r = EqualsTo(right); + if (r.Type == VariantType.Boolean) + return !r.Boolean; + return default; + } + + public static ExpressionVariant operator !(ExpressionVariant v) + { + if (v.Type == VariantType.Boolean) + return !v.Boolean; + return default; + } + + public static ExpressionVariant operator %(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar % right.Scalar; + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double % right.Double; + return default; + } + + public static ExpressionVariant operator <(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar < right.Scalar; + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double < right.Double; + return default; + } + + public static ExpressionVariant operator >(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar > right.Scalar; + + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double > right.Double; + return default; + } + + public ExpressionVariant And(ExpressionVariant right) + { + if (Type == VariantType.Boolean && right.Type == VariantType.Boolean) + return Boolean && right.Boolean; + return default; + } + + public ExpressionVariant Or(ExpressionVariant right) + { + if (Type == VariantType.Boolean && right.Type == VariantType.Boolean) + return Boolean && right.Boolean; + return default; + } + + public bool TryCast(out T res) where T : struct + { + if (typeof(T) == typeof(bool)) + { + if (Type == VariantType.Boolean) + { + res = (T) (object) Boolean; + return true; + } + } + + if (typeof(T) == typeof(float)) + { + if (Type == VariantType.Scalar) + { + res = (T) (object) Scalar; + return true; + } + } + + if (typeof(T) == typeof(double)) + { + if (Type == VariantType.Double) + { + res = (T) (object) Double; + return true; + } + } + + if (typeof(T) == typeof(Vector2)) + { + if (Type == VariantType.Vector2) + { + res = (T) (object) Vector2; + return true; + } + } + + if (typeof(T) == typeof(Vector3)) + { + if (Type == VariantType.Vector3) + { + res = (T) (object) Vector3; + return true; + } + } + + if (typeof(T) == typeof(Vector4)) + { + if (Type == VariantType.Vector4) + { + res = (T) (object) Vector4; + return true; + } + } + + if (typeof(T) == typeof(Matrix3x2)) + { + if (Type == VariantType.Matrix3x2) + { + res = (T) (object) Matrix3x2; + return true; + } + } + + if (typeof(T) == typeof(Matrix)) + { + if (Type == VariantType.AvaloniaMatrix) + { + res = (T) (object) Matrix3x2; + return true; + } + } + + if (typeof(T) == typeof(Matrix4x4)) + { + if (Type == VariantType.Matrix4x4) + { + res = (T) (object) Matrix4x4; + return true; + } + } + + if (typeof(T) == typeof(Quaternion)) + { + if (Type == VariantType.Quaternion) + { + res = (T) (object) Quaternion; + return true; + } + } + + if (typeof(T) == typeof(Avalonia.Media.Color)) + { + if (Type == VariantType.Color) + { + res = (T) (object) Color; + return true; + } + } + + res = default(T); + return false; + } + + public static ExpressionVariant Create(T v) where T : struct + { + if (typeof(T) == typeof(bool)) + return (bool) (object) v; + + if (typeof(T) == typeof(float)) + return (float) (object) v; + + if (typeof(T) == typeof(Vector2)) + return (Vector2) (object) v; + + if (typeof(T) == typeof(Vector3)) + return (Vector3) (object) v; + + if (typeof(T) == typeof(Vector4)) + return (Vector4) (object) v; + + if (typeof(T) == typeof(Matrix3x2)) + return (Matrix3x2) (object) v; + + if (typeof(T) == typeof(Matrix)) + return (Matrix) (object) v; + + if (typeof(T) == typeof(Matrix4x4)) + return (Matrix4x4) (object) v; + + if (typeof(T) == typeof(Quaternion)) + return (Quaternion) (object) v; + + if (typeof(T) == typeof(Avalonia.Media.Color)) + return (Avalonia.Media.Color) (object) v; + + throw new ArgumentException("Invalid variant type: " + typeof(T)); + } + + public T CastOrDefault() where T : struct + { + TryCast(out var r); + return r; + } + + public override string ToString() + { + if (Type == VariantType.Boolean) + return Boolean.ToString(); + if (Type == VariantType.Scalar) + return Scalar.ToString(CultureInfo.InvariantCulture); + if (Type == VariantType.Double) + return Double.ToString(CultureInfo.InvariantCulture); + if (Type == VariantType.Vector2) + return Vector2.ToString(); + if (Type == VariantType.Vector3) + return Vector3.ToString(); + if (Type == VariantType.Vector4) + return Vector4.ToString(); + if (Type == VariantType.Quaternion) + return Quaternion.ToString(); + if (Type == VariantType.Matrix3x2) + return Matrix3x2.ToString(); + if (Type == VariantType.AvaloniaMatrix) + return AvaloniaMatrix.ToString(); + if (Type == VariantType.Matrix4x4) + return Matrix4x4.ToString(); + if (Type == VariantType.Color) + return Color.ToString(); + if (Type == VariantType.Invalid) + return "Invalid"; + return "Unknown"; + } + } + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs new file mode 100644 index 0000000000..1050c7274c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs @@ -0,0 +1,256 @@ +using System; +using System.Globalization; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal ref struct TokenParser + { + private ReadOnlySpan _s; + public int Position { get; private set; } + public TokenParser(ReadOnlySpan s) + { + _s = s; + Position = 0; + } + + public void SkipWhitespace() + { + while (true) + { + if (_s.Length > 0 && char.IsWhiteSpace(_s[0])) + Advance(1); + else + return; + } + } + + static bool IsAlphaNumeric(char ch) => (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z'); + + public bool TryConsume(char c) + { + SkipWhitespace(); + if (_s.Length == 0 || _s[0] != c) + return false; + + Advance(1); + return true; + } + public bool TryConsume(string s) + { + SkipWhitespace(); + if (_s.Length < s.Length) + return false; + for (var c = 0; c < s.Length; c++) + { + if (_s[c] != s[c]) + return false; + } + + Advance(s.Length); + return true; + } + + public bool TryConsumeAny(ReadOnlySpan chars, out char token) + { + SkipWhitespace(); + token = default; + if (_s.Length == 0) + return false; + + foreach (var c in chars) + { + if (c == _s[0]) + { + token = c; + Advance(1); + return true; + } + } + + return false; + } + + + public bool TryParseKeyword(string keyword) + { + SkipWhitespace(); + if (keyword.Length > _s.Length) + return false; + for(var c=0; c keyword.Length && IsAlphaNumeric(_s[keyword.Length])) + return false; + + Advance(keyword.Length); + return true; + } + + public bool TryParseKeywordLowerCase(string keywordInLowerCase) + { + SkipWhitespace(); + if (keywordInLowerCase.Length > _s.Length) + return false; + for(var c=0; c keywordInLowerCase.Length && IsAlphaNumeric(_s[keywordInLowerCase.Length])) + return false; + + Advance(keywordInLowerCase.Length); + return true; + } + + public void Advance(int c) + { + _s = _s.Slice(c); + Position += c; + } + + public int Length => _s.Length; + + public bool TryParseIdentifier(ReadOnlySpan extraValidChars, out ReadOnlySpan res) + { + res = ReadOnlySpan.Empty; + SkipWhitespace(); + if (_s.Length == 0) + return false; + var first = _s[0]; + if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z'))) + return false; + int len = 1; + for (var c = 1; c < _s.Length; c++) + { + var ch = _s[c]; + if (IsAlphaNumeric(ch)) + len++; + else + { + var found = false; + foreach(var vc in extraValidChars) + if (vc == ch) + { + found = true; + break; + } + + if (found) + len++; + else + break; + } + } + + res = _s.Slice(0, len); + Advance(len); + return true; + } + + public bool TryParseIdentifier(out ReadOnlySpan res) + { + res = ReadOnlySpan.Empty; + SkipWhitespace(); + if (_s.Length == 0) + return false; + var first = _s[0]; + if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z'))) + return false; + int len = 1; + for (var c = 1; c < _s.Length; c++) + { + var ch = _s[c]; + if (IsAlphaNumeric(ch)) + len++; + else + break; + } + + res = _s.Slice(0, len); + Advance(len); + return true; + } + + public bool TryParseCall(out ReadOnlySpan res) + { + res = ReadOnlySpan.Empty; + SkipWhitespace(); + if (_s.Length == 0) + return false; + var first = _s[0]; + if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z'))) + return false; + int len = 1; + for (var c = 1; c < _s.Length; c++) + { + var ch = _s[c]; + if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch<= 'Z') || ch == '.') + len++; + else + break; + } + + res = _s.Slice(0, len); + + // Find '(' + for (var c = len; c < _s.Length; c++) + { + if(char.IsWhiteSpace(_s[c])) + continue; + if(_s[c]=='(') + { + Advance(c + 1); + return true; + } + + return false; + + } + + return false; + + } + + + public bool TryParseFloat(out float res) + { + res = 0; + SkipWhitespace(); + if (_s.Length == 0) + return false; + + var len = 0; + var dotCount = 0; + for (var c = 0; c < _s.Length; c++) + { + var ch = _s[c]; + if (ch >= '0' && ch <= '9') + len = c + 1; + else if (ch == '.' && dotCount == 0) + { + len = c + 1; + dotCount++; + } + else + break; + } + + var span = _s.Slice(0, len); + +#if NETSTANDARD2_0 + if (!float.TryParse(span.ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, out res)) + return false; +#else + if (!float.TryParse(span, NumberStyles.Number, CultureInfo.InvariantCulture, out res)) + return false; +#endif + Advance(len); + return true; + } + + public override string ToString() => _s.ToString(); + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs b/src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs new file mode 100644 index 0000000000..9ef31c30e0 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs @@ -0,0 +1,9 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition +{ + public interface ICompositionSurface + { + internal ServerCompositionSurface Server { get; } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs new file mode 100644 index 0000000000..5e91bcb3d4 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs @@ -0,0 +1,46 @@ +using System.Numerics; + +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) + { + // 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); + + if (!transformMatrix.IsIdentity) + mat = transformMatrix * mat; + + + if (scale != new Vector3(1, 1, 1)) + mat *= Matrix4x4.CreateScale(scale, center); + + //TODO: RotationAxis support + if (rotationAngle != 0) + mat *= Matrix4x4.CreateRotationZ(rotationAngle, center); + + if (orientation != Quaternion.Identity) + { + if (centerPoint != default) + { + mat *= Matrix4x4.CreateTranslation(-center) + * Matrix4x4.CreateFromQuaternion(orientation) + * Matrix4x4.CreateTranslation(center); + } + else + mat *= Matrix4x4.CreateFromQuaternion(orientation); + } + + if (offset != default) + mat *= Matrix4x4.CreateTranslation(offset); + + return mat; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs new file mode 100644 index 0000000000..f5dfa92897 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -0,0 +1,142 @@ +using System.Numerics; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal class CompositorDrawingContextProxy : IDrawingContextImpl +{ + private IDrawingContextImpl _impl; + + public CompositorDrawingContextProxy(IDrawingContextImpl impl) + { + _impl = impl; + } + + public Matrix PreTransform { get; set; } = Matrix.Identity; + + public void Dispose() + { + _impl.Dispose(); + } + + Matrix _transform; + public Matrix Transform + { + get => _transform; + set => _impl.Transform = PreTransform * (_transform = value); + } + + public void Clear(Color color) + { + _impl.Clear(color); + } + + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + { + _impl.DrawBitmap(source, opacity, sourceRect, destRect, bitmapInterpolationMode); + } + + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + { + _impl.DrawBitmap(source, opacityMask, opacityMaskRect, destRect); + } + + public void DrawLine(IPen pen, Point p1, Point p2) + { + _impl.DrawLine(pen, p1, p2); + } + + public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + { + _impl.DrawGeometry(brush, pen, geometry); + } + + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default) + { + _impl.DrawRectangle(brush, pen, rect, boxShadows); + } + + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + { + _impl.DrawEllipse(brush, pen, rect); + } + + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + { + _impl.DrawGlyphRun(foreground, glyphRun); + } + + public IDrawingContextLayerImpl CreateLayer(Size size) + { + return _impl.CreateLayer(size); + } + + public void PushClip(Rect clip) + { + _impl.PushClip(clip); + } + + public void PushClip(RoundedRect clip) + { + _impl.PushClip(clip); + } + + public void PopClip() + { + _impl.PopClip(); + } + + public void PushOpacity(double opacity) + { + _impl.PushOpacity(opacity); + } + + public void PopOpacity() + { + _impl.PopOpacity(); + } + + public void PushOpacityMask(IBrush mask, Rect bounds) + { + _impl.PushOpacityMask(mask, bounds); + } + + public void PopOpacityMask() + { + _impl.PopOpacityMask(); + } + + public void PushGeometryClip(IGeometryImpl clip) + { + _impl.PushGeometryClip(clip); + } + + public void PopGeometryClip() + { + _impl.PopGeometryClip(); + } + + public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + { + _impl.PushBitmapBlendMode(blendingMode); + } + + public void PopBitmapBlendMode() + { + _impl.PopBitmapBlendMode(); + } + + public void Custom(ICustomDrawOperation custom) + { + _impl.Custom(custom); + } + + public Matrix CutTransform(Matrix4x4 transform) => new Matrix(transform.M11, transform.M12, transform.M21, + transform.M22, transform.M41, + transform.M42); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs new file mode 100644 index 0000000000..372fa4d9ce --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs @@ -0,0 +1,46 @@ +namespace Avalonia.Rendering.Composition.Server +{ + internal class ReadbackIndices + { + private readonly object _lock = new object(); + public int ReadIndex { get; private set; } = 0; + public int WriteIndex { get; private set; } = -1; + public ulong ReadRevision { get; private set; } + public ulong WriteRevision { get; private set; } + private ulong[] _revisions = new ulong[3]; + + + public void NextRead() + { + lock (_lock) + { + for (var c = 0; c < 3; c++) + { + if (c != WriteIndex && c != ReadIndex && _revisions[c] > ReadRevision) + { + ReadIndex = c; + ReadRevision = _revisions[c]; + return; + } + } + } + } + + public void NextWrite(ulong revision) + { + lock (_lock) + { + for (var c = 0; c < 3; c++) + { + if (c != WriteIndex && c != ReadIndex) + { + WriteIndex = c; + WriteRevision = revision; + _revisions[c] = revision; + return; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs new file mode 100644 index 0000000000..eb041aaf88 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs @@ -0,0 +1,7 @@ +namespace Avalonia.Rendering.Composition.Server +{ + internal abstract partial class ServerCompositionBrush : ServerObject + { + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs new file mode 100644 index 0000000000..397c968d04 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -0,0 +1,41 @@ +using System.Numerics; +using Avalonia.Collections.Pooled; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisual +{ + private CompositionDrawList? _renderCommands; + + public ServerCompositionDrawListVisual(ServerCompositor compositor) : base(compositor) + { + } + + protected override void ApplyCore(ChangeSet changes) + { + var ch = (DrawListVisualChanges)changes; + if (ch.DrawCommandsIsSet) + { + _renderCommands?.Dispose(); + _renderCommands = ch.AcquireDrawCommands(); + } + base.ApplyCore(changes); + } + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + if (_renderCommands != null) + { + foreach (var cmd in _renderCommands) + { + cmd.Item.Render(canvas); + } + } + base.RenderCore(canvas, transform); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs new file mode 100644 index 0000000000..0948b9692f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs @@ -0,0 +1,15 @@ +using System; + +namespace Avalonia.Rendering.Composition.Server +{ + internal abstract partial class ServerCompositionGradientBrush : ServerCompositionBrush + { + public ServerCompositionGradientStopCollection Stops { get; } + public ServerCompositionGradientBrush(ServerCompositor compositor) : base(compositor) + { + Stops = new ServerCompositionGradientStopCollection(compositor); + } + + public override long LastChangedBy => Math.Max(base.LastChangedBy, (long)Stops.LastChangedBy); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs new file mode 100644 index 0000000000..c421cdcfb0 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs @@ -0,0 +1,22 @@ +using Avalonia.Media; + +namespace Avalonia.Rendering.Composition.Server +{ + internal partial class ServerCompositionLinearGradientBrush + { + /* + protected override void UpdateBackendBrush(ICbBrush brush) + { + var stopColors = new Color[Stops.List.Count]; + var offsets = new float[Stops.List.Count]; + for (var c = 0; c < Stops.List.Count; c++) + { + stopColors[c] = Stops.List[c].Color; + offsets[c] = Stops.List[c].Offset; + } + + ((ICbLinearGradientBrush) brush).Update(StartPoint, EndPoint, stopColors, offsets, ExtendMode); + }*/ + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs new file mode 100644 index 0000000000..462a193a86 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Rendering.Composition.Server +{ + internal abstract class ServerCompositionSurface : ServerObject + { + protected ServerCompositionSurface(ServerCompositor compositor) : base(compositor) + { + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs new file mode 100644 index 0000000000..493529e111 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -0,0 +1,53 @@ +using System; +using System.Numerics; +using System.Threading; +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + internal partial class ServerCompositionTarget + { + private readonly ServerCompositor _compositor; + private readonly Func _renderTargetFactory; + private static long s_nextId = 1; + public long Id { get; } + private ulong _frame = 1; + private IRenderTarget? _renderTarget; + + public ReadbackIndices Readback { get; } = new(); + + public ServerCompositionTarget(ServerCompositor compositor, Func renderTargetFactory) : + base(compositor) + { + _compositor = compositor; + _renderTargetFactory = renderTargetFactory; + Id = Interlocked.Increment(ref s_nextId); + } + + partial void OnIsEnabledChanged() + { + if (IsEnabled) + _compositor.AddCompositionTarget(this); + else + _compositor.RemoveCompositionTarget(this); + } + + public void Render() + { + if (Root == null) + return; + _renderTarget ??= _renderTargetFactory(); + + Compositor.UpdateServerTime(); + using (var context = _renderTarget.CreateDrawingContext(null)) + { + context.Clear(Colors.Transparent); + Root.Render(new CompositorDrawingContextProxy(context), Root.CombinedTransformMatrix); + } + + Readback.NextWrite(_frame); + _frame++; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs new file mode 100644 index 0000000000..e56f85acdf --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + internal class ServerCompositor : IRenderLoopTask + { + private readonly IRenderLoop _renderLoop; + private readonly Queue _batches = new Queue(); + public long LastBatchId { get; private set; } + public Stopwatch Clock { get; } = Stopwatch.StartNew(); + public TimeSpan ServerNow { get; private set; } + private List _activeTargets = new(); + + public ServerCompositor(IRenderLoop renderLoop) + { + _renderLoop = renderLoop; + _renderLoop.Add(this); + } + + public void EnqueueBatch(Batch batch) + { + lock (_batches) + _batches.Enqueue(batch); + } + + internal void UpdateServerTime() => ServerNow = Clock.Elapsed; + + List _reusableToCompleteList = new(); + void ApplyPendingBatches() + { + while (true) + { + Batch batch; + lock (_batches) + { + if(_batches.Count == 0) + break; + batch = _batches.Dequeue(); + } + + foreach (var change in batch.Changes) + { + if (change.Dispose) + { + //TODO + } + change.Target!.Apply(change); + change.Reset(); + } + + _reusableToCompleteList.Add(batch); + LastBatchId = batch.SequenceId; + } + } + + void CompletePendingBatches() + { + foreach(var batch in _reusableToCompleteList) + batch.Complete(); + _reusableToCompleteList.Clear(); + } + + bool IRenderLoopTask.NeedsUpdate => false; + void IRenderLoopTask.Update(TimeSpan time) + { + } + + void IRenderLoopTask.Render() + { + ApplyPendingBatches(); + foreach (var t in _activeTargets) + t.Render(); + + CompletePendingBatches(); + } + + public void AddCompositionTarget(ServerCompositionTarget target) + { + _activeTargets.Add(target); + } + + public void RemoveCompositionTarget(ServerCompositionTarget target) + { + _activeTargets.Remove(target); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs new file mode 100644 index 0000000000..ac112b846f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs @@ -0,0 +1,29 @@ +using System.Numerics; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + internal class ServerCompositionContainerVisual : ServerCompositionVisual + { + public ServerCompositionVisualCollection Children { get; } + + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + base.RenderCore(canvas, transform); + + foreach (var ch in Children) + { + var t = transform; + + t = ch.CombinedTransformMatrix * t; + ch.Render(canvas, t); + } + } + + public ServerCompositionContainerVisual(ServerCompositor compositor) : base(compositor) + { + Children = new ServerCompositionVisualCollection(compositor); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs new file mode 100644 index 0000000000..5f3eb051a4 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs @@ -0,0 +1,31 @@ +using System.Numerics; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + class ServerCustomDrawVisual : ServerCompositionContainerVisual + { + private readonly ICustomDrawVisualRenderer _renderer; + private TData? _data; + public ServerCustomDrawVisual(ServerCompositor compositor, ICustomDrawVisualRenderer renderer) : base(compositor) + { + _renderer = renderer; + } + + protected override void ApplyCore(ChangeSet changes) + { + var c = (CustomDrawVisualChanges) changes; + if (c.Data.IsSet) + _data = c.Data.Value; + + base.ApplyCore(changes); + } + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + _renderer.Render(canvas, _data); + base.RenderCore(canvas, transform); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs new file mode 100644 index 0000000000..09ef119e6b --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + class ServerList : ServerObject where T : ServerObject + { + public List List { get; } = new List(); + protected override void ApplyCore(ChangeSet changes) + { + var c = (ListChangeSet) changes; + if (c.HasListChanges) + { + foreach (var lc in c.ListChanges) + { + if(lc.Action == ListChangeAction.Clear) + List.Clear(); + if(lc.Action == ListChangeAction.RemoveAt) + List.RemoveAt(lc.Index); + if(lc.Action == ListChangeAction.InsertAt) + List.Insert(lc.Index, lc.Added!); + if (lc.Action == ListChangeAction.ReplaceAt) + List[lc.Index] = lc.Added!; + } + } + } + + public override long LastChangedBy + { + get + { + var seq = base.LastChangedBy; + foreach (var i in List) + seq = Math.Max(i.LastChangedBy, seq); + return seq; + } + } + + public List.Enumerator GetEnumerator() => List.GetEnumerator(); + + public ServerList(ServerCompositor compositor) : base(compositor) + { + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs new file mode 100644 index 0000000000..072377cd7e --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -0,0 +1,36 @@ +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + internal abstract class ServerObject : IExpressionObject + { + public ServerCompositor Compositor { get; } + + public virtual long LastChangedBy => ItselfLastChangedBy; + public long ItselfLastChangedBy { get; private set; } + + public ServerObject(ServerCompositor compositor) + { + Compositor = compositor; + } + + protected virtual void ApplyCore(ChangeSet changes) + { + + } + + public void Apply(ChangeSet changes) + { + ApplyCore(changes); + ItselfLastChangedBy = changes.Batch!.SequenceId; + } + + public virtual ExpressionVariant GetPropertyForAnimation(string name) + { + return default; + } + + ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs new file mode 100644 index 0000000000..60569867de --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs @@ -0,0 +1,16 @@ +using System.Numerics; +using Avalonia.Media.Immutable; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + internal partial class ServerCompositionSolidColorVisual + { + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + canvas.Transform = canvas.CutTransform(transform); + canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new RoundedRect(new Rect(new Size(Size)))); + base.RenderCore(canvas, transform); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs new file mode 100644 index 0000000000..c658dc8ae3 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs @@ -0,0 +1,20 @@ +using System.Numerics; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + internal partial class ServerCompositionSpriteVisual + { + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + if (Brush != null) + { + //SetTransform(canvas, transform); + //canvas.FillRect((Vector2)Size, (ICbBrush)Brush.Brush!); + } + + base.RenderCore(canvas, transform); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs new file mode 100644 index 0000000000..37e188fb47 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -0,0 +1,92 @@ +using System.Numerics; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + unsafe partial class ServerCompositionVisual : ServerObject + { + protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + + } + + public void Render(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + if(Visible == false) + return; + if(Opacity == 0) + return; + canvas.PreTransform = canvas.CutTransform(transform); + canvas.Transform = Matrix.Identity; + if (Opacity != 1) + canvas.PushOpacity(Opacity); + if(ClipToBounds) + canvas.PushClip(new Rect(new Size(Size.X, Size.Y))); + if (Clip != null) + canvas.PushGeometryClip(Clip); + + RenderCore(canvas, transform); + + if (Clip != null) + canvas.PopGeometryClip(); + if (ClipToBounds) + canvas.PopClip(); + if(Opacity != 1) + canvas.PopOpacity(); + } + + private ReadbackData _readback0, _readback1, _readback2; + + + public ref ReadbackData GetReadback(int idx) + { + if (idx == 0) + return ref _readback0; + if (idx == 1) + return ref _readback1; + return ref _readback2; + } + + public Matrix4x4 CombinedTransformMatrix + { + get + { + if (Root == null) + return default; + + var res = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, + Scale, RotationAngle, Orientation, Offset); + var i = Root.Readback; + ref var readback = ref GetReadback(i.WriteIndex); + readback.Revision = i.WriteRevision; + readback.Matrix = res; + readback.TargetId = Root.Id; + + return res; + } + } + + public struct ReadbackData + { + public Matrix4x4 Matrix; + public bool Visible; + public ulong Revision; + public long TargetId; + } + + partial void ApplyChangesExtra(CompositionVisualChanges c) + { + if (c.Parent.IsSet) + Parent = c.Parent.Value; + if (c.Root.IsSet) + Root = c.Root.Value; + } + + public ServerCompositionTarget? Root { get; private set; } + + public ServerCompositionVisual? Parent { get; private set; } + } + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs new file mode 100644 index 0000000000..7b64c01d09 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Avalonia.Rendering.Composition.Transport +{ + internal class Batch + { + private static long _nextSequenceId = 1; + private static ConcurrentBag> _pool = new ConcurrentBag>(); + public long SequenceId { get; } + + public Batch() + { + SequenceId = Interlocked.Increment(ref _nextSequenceId); + if (!_pool.TryTake(out var lst)) + lst = new List(); + Changes = lst; + } + private TaskCompletionSource _tcs = new TaskCompletionSource(); + public List Changes { get; private set; } + public TimeSpan CommitedAt { get; set; } + + public void Complete() + { + Changes.Clear(); + _pool.Add(Changes); + Changes = null!; + + _tcs.TrySetResult(0); + } + + public Task Completed => _tcs.Task; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs new file mode 100644 index 0000000000..cbee350ab3 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs @@ -0,0 +1,82 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Rendering.Composition.Animations; + +namespace Avalonia.Rendering.Composition.Transport +{ + struct Change + { + private T? _value; + + public bool IsSet { get; private set; } + + public T? Value + { + get + { + if(!IsSet) + throw new InvalidOperationException(); + return _value; + } + set + { + IsSet = true; + _value = value; + } + } + + public void Reset() + { + _value = default; + IsSet = false; + } + } + + struct AnimatedChange + { + private T? _value; + private IAnimationInstance? _animation; + + public bool IsValue { get; private set; } + public bool IsAnimation { get; private set; } + + public T Value + { + get + { + if(!IsValue) + throw new InvalidOperationException(); + return _value!; + } + set + { + IsAnimation = false; + _animation = null; + IsValue = true; + _value = value; + } + } + + public IAnimationInstance Animation + { + get + { + if(!IsAnimation) + throw new InvalidOperationException(); + return _animation!; + } + set + { + IsValue = false; + _value = default; + IsAnimation = true; + _animation = value; + } + } + + public void Reset() + { + this = default; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs new file mode 100644 index 0000000000..898885dce6 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs @@ -0,0 +1,36 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + internal abstract class ChangeSet + { + private readonly IChangeSetPool _pool; + public Batch Batch = null!; + public ServerObject? Target; + public bool Dispose; + + public ChangeSet(IChangeSetPool pool) + { + _pool = pool; + } + + public virtual void Reset() + { + Batch = null!; + Target = null; + Dispose = false; + } + + public void Return() + { + _pool.Return(this); + } + } + + internal class CompositionObjectChanges : ChangeSet + { + public CompositionObjectChanges(IChangeSetPool pool) : base(pool) + { + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs new file mode 100644 index 0000000000..ea97cd7d44 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Concurrent; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + interface IChangeSetPool + { + void Return(ChangeSet changes); + ChangeSet Get(ServerObject target, Batch batch); + } + + class ChangeSetPool : IChangeSetPool where T : ChangeSet + { + private readonly Func _factory; + private readonly ConcurrentBag _pool = new ConcurrentBag(); + + public ChangeSetPool(Func factory) + { + _factory = factory; + } + + public void Return(T changes) + { + changes.Reset(); + _pool.Add(changes); + } + + void IChangeSetPool.Return(ChangeSet changes) => Return((T) changes); + ChangeSet IChangeSetPool.Get(ServerObject target, Batch batch) => Get(target, batch); + + public T Get(ServerObject target, Batch batch) + { + if (!_pool.TryTake(out var res)) + res = _factory(this); + res.Target = target; + res.Batch = batch; + res.Dispose = false; + return res; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs new file mode 100644 index 0000000000..aed041b62e --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs @@ -0,0 +1,20 @@ +namespace Avalonia.Rendering.Composition.Transport +{ + class CustomDrawVisualChanges : CompositionVisualChanges + { + public CustomDrawVisualChanges(IChangeSetPool pool) : base(pool) + { + } + + public Change Data; + + public override void Reset() + { + Data.Reset(); + base.Reset(); + } + + public new static ChangeSetPool> Pool { get; } = + new ChangeSetPool>(pool => new CustomDrawVisualChanges(pool)); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs new file mode 100644 index 0000000000..215c03b229 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using Avalonia.Collections.Pooled; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Transport; + +internal class DrawListVisualChanges : CompositionVisualChanges +{ + private CompositionDrawList? _drawCommands; + + public DrawListVisualChanges(IChangeSetPool pool) : base(pool) + { + } + + public CompositionDrawList? DrawCommands + { + get => _drawCommands; + set + { + _drawCommands?.Dispose(); + _drawCommands = value; + DrawCommandsIsSet = true; + } + } + + public bool DrawCommandsIsSet { get; private set; } + + public CompositionDrawList? AcquireDrawCommands() + { + var rv = _drawCommands; + _drawCommands = null; + DrawCommandsIsSet = false; + return rv; + } + + public override void Reset() + { + _drawCommands?.Dispose(); + _drawCommands = null; + DrawCommandsIsSet = false; + base.Reset(); + } + + public new static ChangeSetPool Pool { get; } = + new ChangeSetPool(pool => new(pool)); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs new file mode 100644 index 0000000000..ee6e4231f8 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs @@ -0,0 +1,19 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + internal class ListChange where T : ServerObject + { + public int Index; + public ListChangeAction Action; + public T? Added; + } + + internal enum ListChangeAction + { + InsertAt, + RemoveAt, + Clear, + ReplaceAt + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs new file mode 100644 index 0000000000..9bb101a080 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + class ListChangeSet : ChangeSet where T : ServerObject + { + private List>? _listChanges; + public List> ListChanges => _listChanges ??= new List>(); + public bool HasListChanges => _listChanges != null; + + public override void Reset() + { + _listChanges?.Clear(); + base.Reset(); + } + + public ListChangeSet(IChangeSetPool pool) : base(pool) + { + } + + public static readonly ChangeSetPool> Pool = + new ChangeSetPool>(pool => new ListChangeSet(pool)); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs new file mode 100644 index 0000000000..1add3aa990 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs @@ -0,0 +1,92 @@ +using System.Collections; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + class ServerListProxyHelper : IList + where TServer : ServerObject + where TClient : CompositionObject + { + private readonly IGetChanges _parent; + private readonly List _list = new List(); + + public interface IGetChanges + { + ListChangeSet GetChanges(); + } + + public ServerListProxyHelper(IGetChanges parent) + { + _parent = parent; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public List.Enumerator GetEnumerator() => _list.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Add(TClient item) => Insert(_list.Count, item); + + public void Clear() + { + _list.Clear(); + _parent.GetChanges().ListChanges.Add(new ListChange + { + Action = ListChangeAction.Clear + }); + } + + public bool Contains(TClient item) => _list.Contains(item); + + public void CopyTo(TClient[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); + + public bool Remove(TClient item) + { + var idx = _list.IndexOf(item); + if (idx == -1) + return false; + RemoveAt(idx); + return true; + } + + public int Count => _list.Count; + public bool IsReadOnly => false; + public int IndexOf(TClient item) => _list.IndexOf(item); + + public void Insert(int index, TClient item) + { + _list.Insert(index, item); + _parent.GetChanges().ListChanges.Add(new ListChange + { + Action = ListChangeAction.InsertAt, + Index = index, + Added = (TServer) item.Server + }); + } + + public void RemoveAt(int index) + { + _list.RemoveAt(index); + _parent.GetChanges().ListChanges.Add(new ListChange + { + Action = ListChangeAction.RemoveAt, + Index = index + }); + } + + public TClient this[int index] + { + get => _list[index]; + set + { + _list[index] = value; + _parent.GetChanges().ListChanges.Add(new ListChange + { + Action = ListChangeAction.ReplaceAt, + Index = index, + Added = (TServer) value.Server + }); + } + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs new file mode 100644 index 0000000000..c87fb96967 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs @@ -0,0 +1,16 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + partial class CompositionVisualChanges + { + public Change Parent; + public Change Root; + + partial void ResetExtra() + { + Parent.Reset(); + Root.Reset(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs b/src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs new file mode 100644 index 0000000000..8c85d7978b --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs @@ -0,0 +1,302 @@ +// ReSharper disable InconsistentNaming +// Ported from Chromium project https://github.com/chromium/chromium/blob/374d31b7704475fa59f7b2cb836b3b68afdc3d79/ui/gfx/geometry/cubic_bezier.cc + +using System; + +// ReSharper disable CompareOfFloatsByEqualityOperator +// ReSharper disable CommentTypo +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable TooWideLocalVariableScope +// ReSharper disable UnusedMember.Global +#pragma warning disable 649 + +namespace Avalonia.Rendering.Composition.Utils +{ + internal unsafe struct CubicBezier + { + const int CUBIC_BEZIER_SPLINE_SAMPLES = 11; + double ax_; + double bx_; + double cx_; + + double ay_; + double by_; + double cy_; + + double start_gradient_; + double end_gradient_; + + double range_min_; + double range_max_; + private bool monotonically_increasing_; + + fixed double spline_samples_[CUBIC_BEZIER_SPLINE_SAMPLES]; + + public CubicBezier(double p1x, double p1y, double p2x, double p2y) : this() + { + InitCoefficients(p1x, p1y, p2x, p2y); + InitGradients(p1x, p1y, p2x, p2y); + InitRange(p1y, p2y); + InitSpline(); + } + + public readonly double SampleCurveX(double t) + { + // `ax t^3 + bx t^2 + cx t' expanded using Horner's rule. + return ((ax_ * t + bx_) * t + cx_) * t; + } + + readonly double SampleCurveY(double t) + { + return ((ay_ * t + by_) * t + cy_) * t; + } + + readonly double SampleCurveDerivativeX(double t) + { + return (3.0 * ax_ * t + 2.0 * bx_) * t + cx_; + } + + readonly double SampleCurveDerivativeY(double t) + { + return (3.0 * ay_ * t + 2.0 * by_) * t + cy_; + } + + public readonly double SolveWithEpsilon(double x, double epsilon) + { + if (x < 0.0) + return 0.0 + start_gradient_ * x; + if (x > 1.0) + return 1.0 + end_gradient_ * (x - 1.0); + return SampleCurveY(SolveCurveX(x, epsilon)); + } + + void InitCoefficients(double p1x, + double p1y, + double p2x, + double p2y) + { + // Calculate the polynomial coefficients, implicit first and last control + // points are (0,0) and (1,1). + cx_ = 3.0 * p1x; + bx_ = 3.0 * (p2x - p1x) - cx_; + ax_ = 1.0 - cx_ - bx_; + + cy_ = 3.0 * p1y; + by_ = 3.0 * (p2y - p1y) - cy_; + ay_ = 1.0 - cy_ - by_; + +#if DEBUG + // Bezier curves with x-coordinates outside the range [0,1] for internal + // control points may have multiple values for t for a given value of x. + // In this case, calls to SolveCurveX may produce ambiguous results. + monotonically_increasing_ = p1x >= 0 && p1x <= 1 && p2x >= 0 && p2x <= 1; +#endif + } + + void InitGradients(double p1x, + double p1y, + double p2x, + double p2y) + { + // End-point gradients are used to calculate timing function results + // outside the range [0, 1]. + // + // There are four possibilities for the gradient at each end: + // (1) the closest control point is not horizontally coincident with regard to + // (0, 0) or (1, 1). In this case the line between the end point and + // the control point is tangent to the bezier at the end point. + // (2) the closest control point is coincident with the end point. In + // this case the line between the end point and the far control + // point is tangent to the bezier at the end point. + // (3) both internal control points are coincident with an endpoint. There + // are two special case that fall into this category: + // CubicBezier(0, 0, 0, 0) and CubicBezier(1, 1, 1, 1). Both are + // equivalent to linear. + // (4) the closest control point is horizontally coincident with the end + // point, but vertically distinct. In this case the gradient at the + // end point is Infinite. However, this causes issues when + // interpolating. As a result, we break down to a simple case of + // 0 gradient under these conditions. + + if (p1x > 0) + start_gradient_ = p1y / p1x; + else if (p1y == 0 && p2x > 0) + start_gradient_ = p2y / p2x; + else if (p1y == 0 && p2y == 0) + start_gradient_ = 1; + else + start_gradient_ = 0; + + if (p2x < 1) + end_gradient_ = (p2y - 1) / (p2x - 1); + else if (p2y == 1 && p1x < 1) + end_gradient_ = (p1y - 1) / (p1x - 1); + else if (p2y == 1 && p1y == 1) + end_gradient_ = 1; + else + end_gradient_ = 0; + } + + const double kBezierEpsilon = 1e-7; + + void InitRange(double p1y, double p2y) + { + range_min_ = 0; + range_max_ = 1; + if (0 <= p1y && p1y < 1 && 0 <= p2y && p2y <= 1) + return; + + double epsilon = kBezierEpsilon; + + // Represent the function's derivative in the form at^2 + bt + c + // as in sampleCurveDerivativeY. + // (Technically this is (dy/dt)*(1/3), which is suitable for finding zeros + // but does not actually give the slope of the curve.) + double a = 3.0 * ay_; + double b = 2.0 * by_; + double c = cy_; + + // Check if the derivative is constant. + if (Math.Abs(a) < epsilon && Math.Abs(b) < epsilon) + return; + + // Zeros of the function's derivative. + double t1; + double t2 = 0; + + if (Math.Abs(a) < epsilon) + { + // The function's derivative is linear. + t1 = -c / b; + } + else + { + // The function's derivative is a quadratic. We find the zeros of this + // quadratic using the quadratic formula. + double discriminant = b * b - 4 * a * c; + if (discriminant < 0) + return; + double discriminant_sqrt = Math.Sqrt(discriminant); + t1 = (-b + discriminant_sqrt) / (2 * a); + t2 = (-b - discriminant_sqrt) / (2 * a); + } + + double sol1 = 0; + double sol2 = 0; + + // If the solution is in the range [0,1] then we include it, otherwise we + // ignore it. + + // An interesting fact about these beziers is that they are only + // actually evaluated in [0,1]. After that we take the tangent at that point + // and linearly project it out. + if (0 < t1 && t1 < 1) + sol1 = SampleCurveY(t1); + + if (0 < t2 && t2 < 1) + sol2 = SampleCurveY(t2); + + range_min_ = Math.Min(Math.Min(range_min_, sol1), sol2); + range_max_ = Math.Max(Math.Max(range_max_, sol1), sol2); + } + + void InitSpline() + { + double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1); + for (int i = 0; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++) + { + spline_samples_[i] = SampleCurveX(i * delta_t); + } + } + + const int kMaxNewtonIterations = 4; + + + public readonly double SolveCurveX(double x, double epsilon) + { + if (x < 0 || x > 1) + throw new ArgumentException(); + + double t0 = 0; + double t1 = 0; + double t2 = x; + double x2 = 0; + double d2; + int i; + +#if DEBUG + if (!monotonically_increasing_) + throw new InvalidOperationException(); +#endif + + // Linear interpolation of spline curve for initial guess. + double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1); + for (i = 1; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++) + { + if (x <= spline_samples_[i]) + { + t1 = delta_t * i; + t0 = t1 - delta_t; + t2 = t0 + (t1 - t0) * (x - spline_samples_[i - 1]) / + (spline_samples_[i] - spline_samples_[i - 1]); + break; + } + } + + // Perform a few iterations of Newton's method -- normally very fast. + // See https://en.wikipedia.org/wiki/Newton%27s_method. + double newton_epsilon = Math.Min(kBezierEpsilon, epsilon); + for (i = 0; i < kMaxNewtonIterations; i++) + { + x2 = SampleCurveX(t2) - x; + if (Math.Abs(x2) < newton_epsilon) + return t2; + d2 = SampleCurveDerivativeX(t2); + if (Math.Abs(d2) < kBezierEpsilon) + break; + t2 = t2 - x2 / d2; + } + + if (Math.Abs(x2) < epsilon) + return t2; + + // Fall back to the bisection method for reliability. + while (t0 < t1) + { + x2 = SampleCurveX(t2); + if (Math.Abs(x2 - x) < epsilon) + return t2; + if (x > x2) + t0 = t2; + else + t1 = t2; + t2 = (t1 + t0) * .5; + } + + // Failure. + return t2; + } + + public readonly double Solve(double x) + { + return SolveWithEpsilon(x, kBezierEpsilon); + } + + public readonly double SlopeWithEpsilon(double x, double epsilon) + { + x = MathExt.Clamp(x, 0.0, 1.0); + double t = SolveCurveX(x, epsilon); + double dx = SampleCurveDerivativeX(t); + double dy = SampleCurveDerivativeY(t); + return dy / dx; + } + + public readonly double Slope(double x) + { + return SlopeWithEpsilon(x, kBezierEpsilon); + } + + public readonly double RangeMin => range_min_; + public readonly double RangeMax => range_max_; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs b/src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs new file mode 100644 index 0000000000..0be19a8e9d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs @@ -0,0 +1,23 @@ +using System; + +namespace Avalonia.Rendering.Composition.Utils +{ + static class MathExt + { + public static float Clamp(float value, float min, float max) + { + var amax = Math.Max(min, max); + var amin = Math.Min(min, max); + return Math.Min(Math.Max(value, amin), amax); + } + + public static double Clamp(double value, double min, double max) + { + var amax = Math.Max(min, max); + var amin = Math.Min(min, max); + return Math.Min(Math.Max(value, amin), amax); + } + + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs new file mode 100644 index 0000000000..1e6d7f8abb --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -0,0 +1,64 @@ +using System.Numerics; + +namespace Avalonia.Rendering.Composition +{ + public abstract partial class CompositionVisual + { + private CompositionVisual? _parent; + private CompositionTarget? _root; + + public CompositionVisual? Parent + { + get => _parent; + internal set + { + if (_parent == value) + return; + _parent = value; + Changes.Parent.Value = value?.Server; + Root = _parent?.Root; + } + } + + // TODO: hide behind private-ish API + public CompositionTarget? Root + { + get => _root; + internal set + { + var changed = _root != value; + _root = value; + Changes.Root.Value = value?.Server; + if (changed) + OnRootChanged(); + } + } + + private protected virtual void OnRootChanged() + { + } + + + internal Matrix4x4? TryGetServerTransform() + { + if (Root == null) + return null; + var i = Root.Server.Readback; + ref var readback = ref Server.GetReadback(i.ReadIndex); + + // CompositionVisual wasn't visible + if (readback.Revision < i.ReadRevision) + return null; + + // CompositionVisual was reparented (potential race here) + if (readback.TargetId != Root.Server.Id) + return null; + + return readback.Matrix; + } + + internal object? Tag { get; set; } + + internal virtual bool HitTest(Vector2 point) => true; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs new file mode 100644 index 0000000000..35f33c3b38 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs @@ -0,0 +1,64 @@ +using System; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition +{ + public partial class CompositionVisualCollection : CompositionObject + { + private CompositionVisual _owner; + internal CompositionVisualCollection(CompositionVisual parent, ServerCompositionVisualCollection server) : base(parent.Compositor, server) + { + _owner = parent; + InitializeDefaults(); + } + + public void InsertAbove(CompositionVisual newChild, CompositionVisual sibling) + { + var idx = _list.IndexOf(sibling); + if (idx == -1) + throw new InvalidOperationException(); + + Insert(idx + 1, newChild); + } + + public void InsertBelow(CompositionVisual newChild, CompositionVisual sibling) + { + var idx = _list.IndexOf(sibling); + if (idx == -1) + throw new InvalidOperationException(); + Insert(idx, newChild); + } + + public void InsertAtTop(CompositionVisual newChild) => Insert(_list.Count, newChild); + + public void InsertAtBottom(CompositionVisual newChild) => Insert(0, newChild); + + public void RemoveAll() => Clear(); + + partial void OnAdded(CompositionVisual item) => item.Parent = _owner; + + partial void OnBeforeReplace(CompositionVisual oldItem, CompositionVisual newItem) + { + if (oldItem != newItem) + OnBeforeAdded(newItem); + } + + partial void OnReplace(CompositionVisual oldItem, CompositionVisual newItem) + { + if (oldItem != newItem) + { + OnRemoved(oldItem); + OnAdded(newItem); + } + } + + partial void OnRemoved(CompositionVisual item) => item.Parent = null; + + + partial void OnBeforeAdded(CompositionVisual item) + { + if (item.Parent != null) + throw new InvalidOperationException("Visual already has a parent"); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/IRenderer.cs b/src/Avalonia.Base/Rendering/IRenderer.cs index e998f78d5c..8d6aabf440 100644 --- a/src/Avalonia.Base/Rendering/IRenderer.cs +++ b/src/Avalonia.Base/Rendering/IRenderer.cs @@ -1,6 +1,7 @@ using System; using Avalonia.VisualTree; using System.Collections.Generic; +using Avalonia.Rendering.Composition; namespace Avalonia.Rendering { @@ -87,4 +88,9 @@ namespace Avalonia.Rendering /// void Stop(); } + + public interface IRendererWithCompositor : IRenderer + { + Compositor Compositor { get; } + } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 5225b85020..3f495c619c 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Numerics; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Utilities; diff --git a/src/Avalonia.Base/Size.cs b/src/Avalonia.Base/Size.cs index 69c3ae7319..5f20206200 100644 --- a/src/Avalonia.Base/Size.cs +++ b/src/Avalonia.Base/Size.cs @@ -52,6 +52,17 @@ namespace Avalonia _width = width; _height = height; } + +#if !BUILDTASK + /// + /// Initializes a new instance of the structure. + /// + /// The vector to take values from. + public Size(System.Numerics.Vector2 vector2) : this(vector2.X, vector2.Y) + { + + } +#endif /// /// Gets the aspect ratio of the size. diff --git a/src/Avalonia.Base/Threading/DispatcherPriority.cs b/src/Avalonia.Base/Threading/DispatcherPriority.cs index a93e4f406d..b4bf603f74 100644 --- a/src/Avalonia.Base/Threading/DispatcherPriority.cs +++ b/src/Avalonia.Base/Threading/DispatcherPriority.cs @@ -62,10 +62,20 @@ namespace Avalonia.Threading /// public static readonly DispatcherPriority Render = new(5); + /// + /// The job will be processed with the same priority as composition batch commit. + /// + public static readonly DispatcherPriority CompositionBatch = new(6); + + /// + /// The job will be processed with the same priority as composition updates. + /// + public static readonly DispatcherPriority Composition = new(7); + /// /// The job will be processed with the same priority as render. /// - public static readonly DispatcherPriority Layout = new(6); + public static readonly DispatcherPriority Layout = new(8); /// /// The job will be processed with the same priority as data binding. @@ -75,7 +85,7 @@ namespace Avalonia.Threading /// /// The job will be processed before other asynchronous operations. /// - public static readonly DispatcherPriority Send = new(7); + public static readonly DispatcherPriority Send = new(9); /// /// Maximum possible priority diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 4fd21f02f9..64341be0c7 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -1,3 +1,7 @@ + + +#nullable enable + using System; using System.Collections; using System.Collections.Specialized; @@ -8,11 +12,10 @@ using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Server; using Avalonia.Utilities; using Avalonia.VisualTree; - -#nullable enable - namespace Avalonia { /// @@ -288,6 +291,8 @@ namespace Avalonia /// protected IRenderRoot? VisualRoot => _visualRoot ?? (this as IRenderRoot); + internal CompositionDrawListVisual? CompositionVisual { get; private set; } + /// /// Gets a value indicating whether this control is attached to a visual root. /// @@ -432,6 +437,10 @@ namespace Avalonia } EnableTransitions(); + if (_visualRoot.Renderer is IRendererWithCompositor compositingRenderer) + { + AttachToCompositor(compositingRenderer.Compositor); + } OnAttachedToVisualTree(e); AttachedToVisualTree?.Invoke(this, e); InvalidateVisual(); @@ -452,6 +461,14 @@ namespace Avalonia } } + internal CompositionVisual AttachToCompositor(Compositor compositor) + { + if (CompositionVisual == null || CompositionVisual.Compositor != compositor) + CompositionVisual = new CompositionDrawListVisual(compositor, + new ServerCompositionDrawListVisual(compositor.Server), this); + return CompositionVisual; + } + /// /// Calls the method /// for this control and all of its visual descendants. @@ -564,7 +581,7 @@ namespace Avalonia { newValue.Changed += sender.RenderTransformChanged; } - + sender.InvalidateVisual(); } } diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml new file mode 100644 index 0000000000..cf864dac2d --- /dev/null +++ b/src/Avalonia.Base/composition-schema.xml @@ -0,0 +1,70 @@ + + + System.Numerics + Avalonia.Rendering.Composition.Server + Avalonia.Rendering.Composition.Transport + Avalonia.Rendering.Composition.Animations + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index addc248d58..984f76adb6 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Numerics; using System.Runtime.InteropServices; using Avalonia.Media; using Avalonia.Platform; @@ -344,6 +345,7 @@ namespace Avalonia.Headless } public Matrix Transform { get; set; } + public void Clear(Color color) { diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs new file mode 100644 index 0000000000..f079a339df --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs @@ -0,0 +1,21 @@ +using System.IO; +using System.Xml.Serialization; +using Microsoft.CodeAnalysis; + +namespace Avalonia.SourceGenerator.CompositionGenerator; + +[Generator(LanguageNames.CSharp)] +public class CompositionRoslynGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var schema = context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith("composition-schema.xml")); + var configs = schema.Select((t, _) => + (GConfig)new XmlSerializer(typeof(GConfig)).Deserialize(new StringReader(t.GetText().ToString()))); + context.RegisterSourceOutput(configs, (spc, config) => + { + var generator = new Generator(spc, config); + generator.Generate(); + }); + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs new file mode 100644 index 0000000000..8b6aca33cd --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs @@ -0,0 +1,116 @@ +#nullable disable +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + + + [XmlRoot("NComposition")] + public class GConfig + { + [XmlElement("Using")] + public List Usings { get; set; } = new List(); + + [XmlElement(typeof(GManualClass), ElementName = "Manual")] + public List ManualClasses { get; set; } = new List(); + + [XmlElement(typeof(GClass), ElementName = "Object")] + [XmlElement(typeof(GBrush), ElementName = "Brush")] + [XmlElement(typeof(GList), ElementName = "List")] + public List Classes { get; set; } = new List(); + + [XmlElement(typeof(GAnimationType), ElementName = "KeyFrameAnimation")] + public List KeyFrameAnimations { get; set; } = new List(); + } + + public class GUsing + { + [XmlText] + public string Name { get; set; } + } + + public class GManualClass + { + [XmlAttribute] + public string Name { get; set; } + + [XmlAttribute] + public string ServerName { get; set; } + } + + public class GImplements + { + [XmlAttribute] + public string Name { get; set; } + [XmlAttribute] + public string ServerName { get; set; } + } + + public class GClass + { + [XmlAttribute] + public string Name { get; set; } + + [XmlAttribute] + public string Inherits { get; set; } + + [XmlAttribute] + public string ChangesBase { get; set; } + + [XmlAttribute] + public string ServerBase { get; set; } + + [XmlAttribute] + public bool CustomCtor { get; set; } + + [XmlAttribute] + public bool CustomServerCtor { get; set; } + + [XmlElement(typeof(GImplements), ElementName = "Implements")] + public List Implements { get; set; } = new List(); + + [XmlAttribute] + public bool Abstract { get; set; } + + [XmlElement(typeof(GProperty), ElementName = "Property")] + public List Properties { get; set; } = new List(); + } + + public class GBrush : GClass + { + [XmlAttribute] + public bool CustomUpdate { get; set; } + + public GBrush() + { + Inherits = "CompositionBrush"; + } + } + + public class GList : GClass + { + [XmlAttribute] + public string ItemType { get; set; } + } + + public class GProperty + { + [XmlAttribute] + public string Name { get; set; } + [XmlAttribute] + public string Type { get; set; } + [XmlAttribute] + public string DefaultValue { get; set; } + [XmlAttribute] + public bool Animated { get; set; } + } + + public class GAnimationType + { + [XmlAttribute] + public string Name { get; set; } + [XmlAttribute] + public string Type { get; set; } + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs new file mode 100644 index 0000000000..43a4a4afa7 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs @@ -0,0 +1,90 @@ +using System; +using System.Linq; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public static class Extensions + { + public static ClassDeclarationSyntax AddModifiers(this ClassDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static MethodDeclarationSyntax AddModifiers(this MethodDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static PropertyDeclarationSyntax AddModifiers(this PropertyDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static ConstructorDeclarationSyntax AddModifiers(this ConstructorDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static AccessorDeclarationSyntax AddModifiers(this AccessorDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static string WithLowerFirst(this string s) + { + if (string.IsNullOrEmpty(s)) + return s; + return char.ToLowerInvariant(s[0]) + s.Substring(1); + } + + public static ExpressionSyntax MemberAccess(params string[] identifiers) + { + if (identifiers == null || identifiers.Length == 0) + throw new ArgumentException(); + var expr = (ExpressionSyntax)IdentifierName(identifiers[0]); + for (var c = 1; c < identifiers.Length; c++) + expr = MemberAccess(expr, identifiers[c]); + return expr; + } + + public static ExpressionSyntax MemberAccess(ExpressionSyntax expr, params string[] identifiers) + { + foreach (var i in identifiers) + expr = MemberAccess(expr, i); + return expr; + } + + public static MemberAccessExpressionSyntax MemberAccess(ExpressionSyntax expr, string identifier) => + MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, expr, IdentifierName(identifier)); + + public static ExpressionSyntax ConditionalMemberAccess(ExpressionSyntax expr, string member, bool checkNull) + { + if (checkNull) + return ConditionalAccessExpression(expr, MemberBindingExpression(IdentifierName(member))); + return MemberAccess(expr, member); + } + + public static ClassDeclarationSyntax WithBaseType(this ClassDeclarationSyntax cl, string bt) + { + return cl.AddBaseListTypes(SimpleBaseType(SyntaxFactory.ParseTypeName(bt))); + } + + public static string StripPrefix(this string s, string prefix) => string.IsNullOrEmpty(s) + ? s + : s.StartsWith(prefix) + ? s.Substring(prefix.Length) + : s; + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs new file mode 100644 index 0000000000..7d5146c5f5 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs @@ -0,0 +1,59 @@ +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public partial class Generator + { + void GenerateAnimations() + { + var code = $@"using System.Numerics; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition +{{ +"; + + foreach (var a in _config.KeyFrameAnimations) + { + var name = a.Name ?? a.Type; + + code += $@" + public class {name}KeyFrameAnimation : KeyFrameAnimation + {{ + public {name}KeyFrameAnimation(Compositor compositor) : base(compositor) + {{ + }} + + internal override IAnimationInstance CreateInstance(Avalonia.Rendering.Composition.Server.ServerObject targetObject, ExpressionVariant? finalValue) + {{ + return new KeyFrameAnimationInstance<{a.Type}>({name}Interpolator.Instance, _keyFrames.Snapshot(), CreateSnapshot(true), + finalValue?.CastOrDefault<{a.Type}>(), targetObject, + DelayBehavior, DelayTime, Direction, Duration, IterationBehavior, + IterationCount, StopBehavior); + }} + + private KeyFrames<{a.Type}> _keyFrames = new KeyFrames<{a.Type}>(); + private protected override IKeyFrames KeyFrames => _keyFrames; + + public void InsertKeyFrame(float normalizedProgressKey, {a.Type} value, CompositionEasingFunction easingFunction) + {{ + _keyFrames.Insert(normalizedProgressKey, value, easingFunction); + }} + + public void InsertKeyFrame(float normalizedProgressKey, {a.Type} value) + {{ + _keyFrames.Insert(normalizedProgressKey, value, Compositor.DefaultEasing); + }} + }} + + public partial class Compositor + {{ + public {name}KeyFrameAnimation Create{name}KeyFrameAnimation() => new {name}KeyFrameAnimation(this); + }} +"; + } + + code += "}"; + _output.AddSource("CompositionAnimations.cs", code); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs new file mode 100644 index 0000000000..593386f713 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs @@ -0,0 +1,121 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public partial class Generator + { + private const string ListProxyTemplate = @" +class Template +{ + private ServerListProxyHelper _list = null!; + + ListChangeSet + ServerListProxyHelper.IGetChanges. + GetChanges() => Changes; + + public List.Enumerator GetEnumerator() => _list.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable) _list).GetEnumerator(); + + public void Add(ItemTypeName item) + { + OnBeforeAdded(item); + _list.Add(item); + OnAdded(item); + } + + public void Clear() + { + OnBeforeClear(); + _list.Clear(); + OnClear(); + } + + public bool Contains(ItemTypeName item) => _list.Contains(item); + + public void CopyTo(ItemTypeName[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); + + public bool Remove(ItemTypeName item) + { + var removed = _list.Remove(item); + if(removed) + OnRemoved(item); + return removed; + } + + public int Count => _list.Count; + + public bool IsReadOnly => _list.IsReadOnly; + + public int IndexOf(ItemTypeName item) => _list.IndexOf(item); + + public void Insert(int index, ItemTypeName item) + { + OnBeforeAdded(item); + _list.Insert(index, item); + OnAdded(item); + } + + public void RemoveAt(int index) + { + var item = _list[index]; + _list.RemoveAt(index); + OnRemoved(item); + } + + public ItemTypeName this[int index] + { + get => _list[index]; + set + { + var old = _list[index]; + OnBeforeReplace(old, value); + _list[index] = value; + OnReplace(old, value); + } + } + + partial void OnBeforeAdded(ItemTypeName item); + partial void OnAdded(ItemTypeName item); + partial void OnRemoved(ItemTypeName item); + partial void OnBeforeClear(); + partial void OnBeforeReplace(ItemTypeName oldItem, ItemTypeName newItem); + partial void OnReplace(ItemTypeName oldItem, ItemTypeName newItem); + partial void OnClear(); +} +"; + + private ClassDeclarationSyntax AppendListProxy(GList list, ClassDeclarationSyntax cl) + { + + var itemType = list.ItemType; + var serverItemType = ServerName(itemType); + + cl = cl.AddBaseListTypes(SimpleBaseType( + ParseTypeName("ServerListProxyHelper<" + itemType + ", " + serverItemType + ">.IGetChanges")), + SimpleBaseType(ParseTypeName("IList<" + itemType + ">")) + ); + var code = ListProxyTemplate.Replace("ListTypeName", list.Name) + .Replace("ItemTypeName", itemType); + + var parsed = ParseCompilationUnit(code); + var parsedClass = (ClassDeclarationSyntax)parsed.Members.First(); + + cl = cl.AddMembers(parsedClass.Members.ToArray()); + + var defs = cl.Members.OfType().First(m => m.Identifier.Text == "InitializeDefaults"); + + cl = cl.ReplaceNode(defs.Body, defs.Body.AddStatements( + + ParseStatement($"_list = new ServerListProxyHelper<{itemType}, {serverItemType}>(this);"))); + + return cl; + } + + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.Utils.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.Utils.cs new file mode 100644 index 0000000000..b53c247991 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.Utils.cs @@ -0,0 +1,66 @@ +using System.IO; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public partial class Generator + { + static void CleanDirectory(string path) + { + Directory.CreateDirectory(path); + Directory.Delete(path, true); + Directory.CreateDirectory(path); + } + + CompilationUnitSyntax Unit() + => CompilationUnit().WithUsings(List(new[] + { + "System", + "System.Text", + "System.Collections", + "System.Collections.Generic" + } + .Concat(_config.Usings + .Select(x => x.Name)).Select(u => UsingDirective(IdentifierName(u))))); + + void SaveTo(CompilationUnitSyntax unit, params string[] path) + { + var text = @" +#nullable enable +#pragma warning disable CS0108, CS0114 + +" + + + unit.NormalizeWhitespace().ToFullString(); + _output.AddSource(string.Join("_", path), text); + } + + + SyntaxToken Semicolon() => Token(SyntaxKind.SemicolonToken); + + + FieldDeclarationSyntax DeclareConstant(string type, string name, LiteralExpressionSyntax value) + => FieldDeclaration( + VariableDeclaration(ParseTypeName(type), + SingletonSeparatedList( + VariableDeclarator(name).WithInitializer(EqualsValueClause(value)) + )) + ).WithSemicolonToken(Semicolon()) + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.ConstKeyword))); + + FieldDeclarationSyntax DeclareField(string type, string name, params SyntaxKind[] modifiers) => + DeclareField(type, name, null, modifiers); + + FieldDeclarationSyntax DeclareField(string type, string name, EqualsValueClauseSyntax initializer, + params SyntaxKind[] modifiers) => + FieldDeclaration( + VariableDeclaration(ParseTypeName(type), + SingletonSeparatedList( + VariableDeclarator(name).WithInitializer(initializer)))) + .WithSemicolonToken(Semicolon()) + .WithModifiers(TokenList(modifiers.Select(x => Token(x)))); + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs new file mode 100644 index 0000000000..43ef4a96e8 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs @@ -0,0 +1,504 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Avalonia.SourceGenerator.CompositionGenerator.Extensions; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + partial class Generator + { + private readonly SourceProductionContext _output; + private readonly GConfig _config; + private readonly HashSet _objects; + private readonly HashSet _brushes; + private readonly Dictionary _manuals; + public Generator(SourceProductionContext output, GConfig config) + { + _output = output; + _config = config; + _manuals = _config.ManualClasses.ToDictionary(x => x.Name); + _objects = new HashSet(_config.ManualClasses.Select(x => x.Name) + .Concat(_config.Classes.Select(x => x.Name))); + _brushes = new HashSet(_config.Classes.OfType().Select(x => x.Name)) {"CompositionBrush"}; + } + + + + public void Generate() + { + foreach (var cl in _config.Classes) + GenerateClass(cl); + + GenerateAnimations(); + } + + + + string ServerName(string c) => c != null ? ("Server" + c) : "ServerObject"; + string ChangesName(string c) => c != null ? (c + "Changes") : "ChangeSet"; + + void GenerateClass(GClass cl) + { + var list = cl as GList; + + var unit = Unit(); + + var clientNs = NamespaceDeclaration(IdentifierName("Avalonia.Rendering.Composition")); + var serverNs = NamespaceDeclaration(IdentifierName("Avalonia.Rendering.Composition.Server")); + var transportNs = NamespaceDeclaration(IdentifierName("Avalonia.Rendering.Composition.Transport")); + + var inherits = cl.Inherits ?? "CompositionObject"; + var abstractModifier = cl.Abstract ? new[] {SyntaxKind.AbstractKeyword} : null; + + var client = ClassDeclaration(cl.Name) + .AddModifiers(abstractModifier) + .AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) + .WithBaseType(inherits); + + var serverName = ServerName(cl.Name); + var serverBase = cl.ServerBase ?? ServerName(cl.Inherits); + if (list != null) + serverBase = "ServerList<" + ServerName(list.ItemType) + ">"; + + var server = ClassDeclaration(serverName) + .AddModifiers(abstractModifier) + .AddModifiers(SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) + .WithBaseType(serverBase); + + string changesName = ChangesName(cl.Name); + var changesBase = ChangesName(cl.ChangesBase ?? cl.Inherits); + + if (list != null) + changesBase = "ListChangeSet<" + ServerName(list.ItemType) + ">"; + + var changeSetPoolType = "ChangeSetPool<" + changesName + ">"; + var transport = ClassDeclaration(changesName) + .AddModifiers(SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) + .WithBaseType(changesBase) + .AddMembers(DeclareField(changeSetPoolType, "Pool", + EqualsValueClause( + ParseExpression($"new {changeSetPoolType}(pool => new {changesName}(pool))") + ), + SyntaxKind.PublicKeyword, + SyntaxKind.StaticKeyword, SyntaxKind.ReadOnlyKeyword)) + .AddMembers(ParseMemberDeclaration($"public {changesName}(IChangeSetPool pool) : base(pool){{}}")); + + client = client + .AddMembers( + PropertyDeclaration(ParseTypeName("IChangeSetPool"), "ChangeSetPool") + .AddModifiers(SyntaxKind.PrivateKeyword, SyntaxKind.ProtectedKeyword, + SyntaxKind.OverrideKeyword) + .WithExpressionBody( + ArrowExpressionClause(MemberAccess(changesName, "Pool"))) + .WithSemicolonToken(Semicolon())) + .AddMembers(PropertyDeclaration(ParseTypeName(changesName), "Changes") + .AddModifiers(SyntaxKind.PrivateKeyword, SyntaxKind.NewKeyword) + .WithExpressionBody(ArrowExpressionClause(CastExpression(ParseTypeName(changesName), + MemberAccess(BaseExpression(), "Changes")))) + .WithSemicolonToken(Semicolon())); + + if (!cl.CustomCtor) + { + client = client.AddMembers(PropertyDeclaration(ParseTypeName(serverName), "Server") + .AddModifiers(SyntaxKind.InternalKeyword, SyntaxKind.NewKeyword) + .AddAccessorListAccessors(AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(Semicolon()))); + client = client.AddMembers( + ConstructorDeclaration(cl.Name) + .AddModifiers(SyntaxKind.InternalKeyword) + .WithParameterList(ParameterList(SeparatedList(new[] + { + Parameter(Identifier("compositor")).WithType(ParseTypeName("Compositor")), + Parameter(Identifier("server")).WithType(ParseTypeName(serverName)), + }))) + .WithInitializer(ConstructorInitializer(SyntaxKind.BaseConstructorInitializer, + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("compositor")), + Argument(IdentifierName("server")), + })))).WithBody(Block( + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName("Server"), + CastExpression(ParseTypeName(serverName), IdentifierName("server")))), + ExpressionStatement(InvocationExpression(IdentifierName("InitializeDefaults"))) + ))); + } + + if (!cl.CustomServerCtor) + { + server = server.AddMembers( + ConstructorDeclaration(serverName) + .AddModifiers(SyntaxKind.InternalKeyword) + .WithParameterList(ParameterList(SeparatedList(new[] + { + Parameter(Identifier("compositor")).WithType(ParseTypeName("ServerCompositor")), + }))) + .WithInitializer(ConstructorInitializer(SyntaxKind.BaseConstructorInitializer, + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("compositor")), + })))).WithBody(Block())); + } + + + var changesVarName = "c"; + var changesVar = IdentifierName(changesVarName); + + server = server.AddMembers( + MethodDeclaration(ParseTypeName("void"), "ApplyChangesExtra") + .AddParameterListParameters(Parameter(Identifier("c")).WithType(ParseTypeName(changesName))) + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + transport = transport.AddMembers( + MethodDeclaration(ParseTypeName("void"), "ResetExtra") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + var applyMethodBody = Block( + ExpressionStatement(InvocationExpression(MemberAccess(IdentifierName("base"), "ApplyCore"), + ArgumentList(SeparatedList(new[] {Argument(IdentifierName("changes"))})))), + LocalDeclarationStatement(VariableDeclaration(ParseTypeName("var")) + .WithVariables(SingletonSeparatedList( + VariableDeclarator(changesVarName) + .WithInitializer(EqualsValueClause(CastExpression(ParseTypeName(changesName), + IdentifierName("changes"))))))), + ExpressionStatement(InvocationExpression(IdentifierName("ApplyChangesExtra")) + .AddArgumentListArguments(Argument(IdentifierName("c")))) + ); + + var resetBody = Block(); + var startAnimationBody = Block(); + var getPropertyBody = Block(); + var serverGetPropertyBody = Block(); + + var defaultsMethodBody = Block(); + + foreach (var prop in cl.Properties) + { + var fieldName = "_" + prop.Name.WithLowerFirst(); + var propType = ParseTypeName(prop.Type); + var filteredPropertyType = prop.Type.TrimEnd('?'); + var isObject = _objects.Contains(filteredPropertyType); + var isNullable = prop.Type.EndsWith("?"); + + + + + client = client + .AddMembers(DeclareField(prop.Type, fieldName)) + .AddMembers(PropertyDeclaration(propType, prop.Name) + .AddModifiers(SyntaxKind.PublicKeyword) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, + Block(ReturnStatement(IdentifierName(fieldName)))), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, + Block( + ParseStatement("var changed = false;"), + IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, + IdentifierName(fieldName), + IdentifierName("value")), + Block( + ParseStatement("On" + prop.Name + "Changing();"), + ParseStatement("changed = true;"), + GeneratePropertySetterAssignment(prop, fieldName, isObject, isNullable)) + ), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldName), IdentifierName("value"))), + ParseStatement($"if(changed) On" + prop.Name + "Changed();") + )) + )) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + + var animatedServer = prop.Animated; + + var serverPropertyType = ((isObject ? "Server" : "") + prop.Type); + if (_manuals.TryGetValue(filteredPropertyType, out var manual) && manual.ServerName != null) + serverPropertyType = manual.ServerName + (isNullable ? "?" : ""); + + + transport = transport + .AddMembers(DeclareField((animatedServer ? "Animated" : "") + "Change<" + serverPropertyType + ">", + prop.Name, SyntaxKind.PublicKeyword)); + + if (animatedServer) + server = server.AddMembers( + DeclareField("AnimatedValueStore<" + serverPropertyType + ">", fieldName), + PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) + .AddModifiers(SyntaxKind.PublicKeyword) + .WithExpressionBody(ArrowExpressionClause( + InvocationExpression(MemberAccess(fieldName, "GetAnimated"), + ArgumentList(SingletonSeparatedList(Argument(IdentifierName("Compositor"))))))) + .WithSemicolonToken(Semicolon()) + ); + else + { + server = server + .AddMembers(DeclareField(serverPropertyType, fieldName)) + .AddMembers(PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) + .AddModifiers(SyntaxKind.PublicKeyword) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, + Block(ReturnStatement(IdentifierName(fieldName)))), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, + Block( + ParseStatement("var changed = false;"), + IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, + IdentifierName(fieldName), + IdentifierName("value")), + Block( + ParseStatement("On" + prop.Name + "Changing();"), + ParseStatement($"changed = true;")) + ), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldName), IdentifierName("value"))), + ParseStatement($"if(changed) On" + prop.Name + "Changed();") + )) + )) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + } + + if (animatedServer) + applyMethodBody = applyMethodBody.AddStatements( + IfStatement(MemberAccess(changesVar, prop.Name, "IsValue"), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldName), MemberAccess(changesVar, prop.Name, "Value")))), + IfStatement(MemberAccess(changesVar, prop.Name, "IsAnimation"), + ExpressionStatement( + InvocationExpression(MemberAccess(fieldName, "SetAnimation"), + ArgumentList(SeparatedList(new[] + { + Argument(changesVar), + Argument(MemberAccess(changesVar, prop.Name, "Animation")) + }))))) + ); + else + applyMethodBody = applyMethodBody.AddStatements( + IfStatement(MemberAccess(changesVar, prop.Name, "IsSet"), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(prop.Name), MemberAccess(changesVar, prop.Name, "Value")))) + + ); + + + resetBody = resetBody.AddStatements( + ExpressionStatement(InvocationExpression(MemberAccess(prop.Name, "Reset")))); + + if (animatedServer) + startAnimationBody = ApplyStartAnimation(startAnimationBody, prop, fieldName); + + getPropertyBody = ApplyGetProperty(getPropertyBody, prop); + serverGetPropertyBody = ApplyGetProperty(getPropertyBody, prop); + + if (prop.DefaultValue != null) + { + defaultsMethodBody = defaultsMethodBody.AddStatements( + ExpressionStatement( + AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(prop.Name), ParseExpression(prop.DefaultValue)))); + } + } + + if (cl is GBrush brush && !cl.Abstract) + { + var brushName = brush.Name.StripPrefix("Composition"); + /* + server = server.AddMembers( + MethodDeclaration(ParseTypeName("ICbBrush"), "CreateBackendBrush") + .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) + .WithExpressionBody(ArrowExpressionClause( + InvocationExpression(MemberAccess("Compositor", "Backend", "Create" + brushName)) + )).WithSemicolonToken(Semicolon()) + ); + if (!brush.CustomUpdate) + server = server.AddMembers( + MethodDeclaration(ParseTypeName("void"), "UpdateBackendBrush") + .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) + .AddParameterListParameters(Parameter(Identifier("brush")) + .WithType(ParseTypeName("ICbBrush"))) + .AddBodyStatements( + ExpressionStatement( + InvocationExpression( + MemberAccess( + ParenthesizedExpression( + CastExpression(ParseTypeName("ICb" + brushName), IdentifierName("brush"))), "Update"), + ArgumentList(SeparatedList(cl.Properties.Select(x => + { + if(x.Type.TrimEnd('?') == "ICompositionSurface") + return Argument( + ConditionalAccessExpression(IdentifierName(x.Name), + MemberBindingExpression(IdentifierName("BackendSurface"))) + ); + if (_brushes.Contains(x.Type)) + return Argument( + ConditionalAccessExpression(IdentifierName(x.Name), + MemberBindingExpression(IdentifierName("Brush"))) + ); + return Argument(IdentifierName(x.Name)); + })))) + ))); + +*/ + } + + server = server.AddMembers( + MethodDeclaration(ParseTypeName("void"), "ApplyCore") + .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) + .AddParameterListParameters( + Parameter(Identifier("changes")).WithType(ParseTypeName("ChangeSet"))) + .WithBody(applyMethodBody)); + + client = client.AddMembers( + MethodDeclaration(ParseTypeName("void"), "InitializeDefaults").WithBody(defaultsMethodBody)); + + transport = transport.AddMembers(MethodDeclaration(ParseTypeName("void"), "Reset") + .AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.OverrideKeyword) + .WithBody(resetBody.AddStatements( + ExpressionStatement(InvocationExpression(IdentifierName("ResetExtra"))), + ExpressionStatement(InvocationExpression(MemberAccess("base", "Reset")))))); + + if (list != null) + client = AppendListProxy(list, client); + + if (startAnimationBody.Statements.Count != 0) + client = WithStartAnimation(client, startAnimationBody); + + client = WithGetProperty(client, getPropertyBody, false); + server = WithGetProperty(server, serverGetPropertyBody, true); + + if(cl.Implements.Count > 0) + foreach (var impl in cl.Implements) + { + client = client.WithBaseList(client.BaseList.AddTypes(SimpleBaseType(ParseTypeName(impl.Name)))); + if (impl.ServerName != null) + server = server.WithBaseList( + server.BaseList.AddTypes(SimpleBaseType(ParseTypeName(impl.ServerName)))); + + client = client.AddMembers( + ParseMemberDeclaration($"{impl.ServerName} {impl.Name}.Server => Server;")); + } + + + SaveTo(unit.AddMembers(clientNs.AddMembers(client)), + cl.Name + ".generated.cs"); + SaveTo(unit.AddMembers(serverNs.AddMembers(server)), + "Server", "Server" + cl.Name + ".generated.cs"); + SaveTo(unit.AddMembers(transportNs.AddMembers(transport)), + "Transport", cl.Name + "Changes.generated.cs"); + } + + StatementSyntax GeneratePropertySetterAssignment(GProperty prop, string fieldName, bool isObject, bool isNullable) + { + var normalChangesAssignment = (StatementSyntax)ExpressionStatement(AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + MemberAccess((ExpressionSyntax) IdentifierName("Changes"), prop.Name, + "Value"), + isObject + ? + ConditionalMemberAccess(IdentifierName("value"), "Server", isNullable) + : IdentifierName("value"))); + if (!prop.Animated) + return normalChangesAssignment; + + var code = $@" +{{ + if(animation is CompositionAnimation a) + Changes.{prop.Name}.Animation = a.CreateInstance(this.Server, value); + else + {{ + var saved = Changes.{prop.Name}; + if(!StartAnimationGroup(animation, ""{prop.Name}"", value)) + Changes.{prop.Name}.Value = value; + }} +}} + +"; + + return IfStatement( + ParseExpression( + $"ImplicitAnimations != null && ImplicitAnimations.TryGetValue(\"{prop.Name}\", out var animation) == true"), + ParseStatement(code), + ElseClause(normalChangesAssignment) + ); + } + + BlockSyntax ApplyStartAnimation(BlockSyntax body, GProperty prop, string fieldName) + { + var code = $@" +if (propertyName == ""{prop.Name}"") +{{ +var current = {fieldName}; +var server = animation.CreateInstance(this.Server, finalValue); +Changes.{prop.Name}.Animation = server; +return; +}} +"; + return body.AddStatements(ParseStatement(code)); + } + + private static HashSet VariantPropertyTypes = new HashSet + { + "bool", + "float", + "Vector2", + "Vector3", + "Vector4", + "Matrix3x2", + "Matrix4x4", + "Quaternion", + "CompositionColor" + }; + + BlockSyntax ApplyGetProperty(BlockSyntax body, GProperty prop) + { + if (VariantPropertyTypes.Contains(prop.Type)) + return body.AddStatements( + ParseStatement($"if(name == \"{prop.Name}\")\n return {prop.Name};\n") + ); + + return body; + } + + ClassDeclarationSyntax WithGetProperty(ClassDeclarationSyntax cl, BlockSyntax body, bool server) + { + if (body.Statements.Count == 0) + return cl; + body = body.AddStatements( + ParseStatement("return base.GetPropertyForAnimation(name);")); + var method = ((MethodDeclarationSyntax) ParseMemberDeclaration( + $"{(server ? "public" : "internal")} override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}")) + .WithBody(body); + + return cl.AddMembers(method); + } + + ClassDeclarationSyntax WithStartAnimation(ClassDeclarationSyntax cl, BlockSyntax body) + { + body = body.AddStatements( + ExpressionStatement(InvocationExpression(MemberAccess("base", "StartAnimation"), + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("propertyName")), + Argument(IdentifierName("animation")), + Argument(IdentifierName("finalValue")), + })))) + ); + return cl.AddMembers( + ((MethodDeclarationSyntax) ParseMemberDeclaration( + "internal override void StartAnimation(string propertyName, CompositionAnimation animation, Avalonia.Rendering.Composition.Expressions.ExpressionVariant? finalValue){}")) + .WithBody(body)); + + + } + + } +} \ No newline at end of file diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index ec3f29c806..7a64b39575 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -13,6 +13,7 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.X11; using Avalonia.X11.Glx; using Avalonia.X11.NativeDialogs; @@ -29,6 +30,7 @@ namespace Avalonia.X11 public XI2Manager XI2; public X11Info Info { get; private set; } public IX11Screens X11Screens { get; private set; } + public Compositor Compositor { get; private set; } public IScreenImpl Screens { get; private set; } public X11PlatformOptions Options { get; private set; } public IntPtr OrphanedWindow { get; private set; } @@ -101,7 +103,9 @@ namespace Avalonia.X11 GlxPlatformOpenGlInterface.TryInitialize(Info, Options.GlProfiles); } - + if (options.UseCompositor) + Compositor = Compositor.Create(AvaloniaLocator.Current.GetService()!); + } public IntPtr DeferredDisplay { get; set; } @@ -222,6 +226,8 @@ namespace Avalonia /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. /// public bool UseDeferredRendering { get; set; } = true; + + public bool UseCompositor { get; set; } /// /// Determines whether to use IME. diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 066156a652..37cfeb0624 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -18,6 +18,7 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.X11.Glx; using static Avalonia.X11.XLib; @@ -360,13 +361,15 @@ namespace Avalonia.X11 if (customRendererFactory != null) return customRendererFactory.Create(root, loop); - - return _platform.Options.UseDeferredRendering ? - new DeferredRenderer(root, loop) - { - RenderOnlyOnRenderThread = true - } : - (IRenderer)new X11ImmediateRendererProxy(root, loop); + + return _platform.Options.UseDeferredRendering + ? _platform.Options.UseCompositor + ? new CompositingRenderer(root, this._platform.Compositor) + : new DeferredRenderer(root, loop) + { + RenderOnlyOnRenderThread = true + } + : (IRenderer)new X11ImmediateRendererProxy(root, loop); } void OnEvent(ref XEvent ev) diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index a259d8fab9..1243c90214 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Numerics; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; @@ -73,6 +74,12 @@ namespace Avalonia.Direct2D1.Media set { _deviceContext.Transform = value.ToDirect2D(); } } + public Matrix4x4 Transform4x4 + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + /// public void Clear(Color color) { From 4274e78a321b37412c77915638535e1d12304c6e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 9 May 2022 21:43:22 +0300 Subject: [PATCH 002/171] Win32 --- samples/ControlCatalog.NetCore/Program.cs | 3 ++- .../Rendering/Composition/CompositingRenderer.cs | 4 ++-- src/Windows/Avalonia.Win32/Win32Platform.cs | 8 ++++++++ src/Windows/Avalonia.Win32/WindowImpl.cs | 4 ++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 2a0755b900..6196aac153 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -115,7 +115,8 @@ namespace ControlCatalog.NetCore }) .With(new Win32PlatformOptions { - EnableMultitouch = true + EnableMultitouch = true, + UseCompositor = true }) .UseSkia() .AfterSetup(builder => diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 07ac54b634..65ed0d17ad 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -177,7 +177,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor public void Stop() { - _target.IsEnabled = true; + _target.IsEnabled = false; } public void Dispose() @@ -191,4 +191,4 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor public Compositor Compositor => _compositor; -} \ No newline at end of file +} diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index dc5e5324c4..32705a2cc6 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -14,6 +14,7 @@ using Avalonia.Input.Platform; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.Win32.Input; @@ -48,6 +49,8 @@ namespace Avalonia /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. /// public bool UseDeferredRendering { get; set; } = true; + + public bool UseCompositor { get; set; } /// /// Enables ANGLE for Windows. For every Windows version that is above Windows 7, the default is true otherwise it's false. @@ -132,6 +135,8 @@ namespace Avalonia.Win32 public static bool UseDeferredRendering => Options.UseDeferredRendering; internal static bool UseOverlayPopups => Options.OverlayPopups; public static Win32PlatformOptions Options { get; private set; } + + internal static Compositor Compositor { get; private set; } public Size DoubleClickSize => new Size( UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CXDOUBLECLK), @@ -181,6 +186,9 @@ namespace Avalonia.Win32 if (OleContext.Current != null) AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); + + if (Options.UseCompositor) + Compositor = Compositor.Create(AvaloniaLocator.Current.GetRequiredService()); } public bool HasMessages() diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index f0036236ec..e61121f23e 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -14,6 +14,7 @@ using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Win32.Automation; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; @@ -500,6 +501,9 @@ namespace Avalonia.Win32 if (customRendererFactory != null) return customRendererFactory.Create(root, loop); + if (Win32Platform.Compositor != null) + return new CompositingRenderer(root, Win32Platform.Compositor); + return Win32Platform.UseDeferredRendering ? _isUsingComposition ? new DeferredRenderer(root, loop) From 7a3b5f051f02e2e0689df3232ffac5b76f6d986b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 9 May 2022 23:20:39 +0300 Subject: [PATCH 003/171] Make ThreadSafeObjectPool actually thread safe --- src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs index c6845485dc..05995f2069 100644 --- a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs +++ b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs @@ -5,7 +5,6 @@ namespace Avalonia.Threading public class ThreadSafeObjectPool where T : class, new() { private Stack _stack = new Stack(); - private object _lock = new object(); public static ThreadSafeObjectPool Default { get; } = new ThreadSafeObjectPool(); public T Get() @@ -20,7 +19,7 @@ namespace Avalonia.Threading public void Return(T obj) { - lock (_stack) + lock (_lock) { _stack.Push(obj); } From 6d46006d075e05a4de377e8f85b1b93199e348f3 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 9 May 2022 23:30:02 +0300 Subject: [PATCH 004/171] Removed some matrix operations --- .../Composition/Expressions/ExpressionVariant.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs index 8c6af5cb0c..086c8ce276 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs @@ -290,9 +290,6 @@ namespace Avalonia.Rendering.Composition.Expressions if (left.Type == VariantType.Matrix3x2) return left.Matrix3x2 + right.Matrix3x2; - if (left.Type == VariantType.AvaloniaMatrix) - return left.AvaloniaMatrix + right.AvaloniaMatrix; - if (left.Type == VariantType.Matrix4x4) return left.Matrix4x4 + right.Matrix4x4; @@ -325,9 +322,6 @@ namespace Avalonia.Rendering.Composition.Expressions if (left.Type == VariantType.Matrix3x2) return left.Matrix3x2 - right.Matrix3x2; - if (left.Type == VariantType.AvaloniaMatrix) - return left.AvaloniaMatrix - right.AvaloniaMatrix; - if (left.Type == VariantType.Matrix4x4) return left.Matrix4x4 - right.Matrix4x4; @@ -407,12 +401,6 @@ namespace Avalonia.Rendering.Composition.Expressions if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.AvaloniaMatrix) return left.AvaloniaMatrix * right.AvaloniaMatrix; - - if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.Scalar) - return left.AvaloniaMatrix * (double)right.Scalar; - - if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.Double) - return left.AvaloniaMatrix * right.Double; if (left.Type == VariantType.Matrix4x4 && right.Type == VariantType.Matrix4x4) return left.Matrix4x4 * right.Matrix4x4; From 16dde385d0d8c6b9f7711a801b67a8f4440b3a57 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 11 May 2022 00:59:00 +0300 Subject: [PATCH 005/171] WIP: Matrix3x3, some invalidation support, dirty rects drawing --- samples/RenderDemo/App.xaml.cs | 4 + .../Composition/CompositingRenderer.cs | 26 ++++-- .../Composition/CompositionDrawListVisual.cs | 3 +- .../Composition/CompositionTarget.cs | 24 +++--- .../Rendering/Composition/Compositor.cs | 1 + .../Rendering/Composition/MatrixUtils.cs | 20 +++++ .../Composition/Server/DrawingContextProxy.cs | 4 - .../Composition/Server/FpsCounter.cs | 73 ++++++++++++++++ .../Server/ServerCompositionDrawListVisual.cs | 20 +++++ .../Server/ServerCompositionTarget.cs | 83 ++++++++++++++++++- .../Server/ServerContainerVisual.cs | 7 ++ .../Server/ServerSolidColorVisual.cs | 2 +- .../Composition/Server/ServerVisual.cs | 52 ++++++++---- .../Transport/CompositionTargetChanges.cs | 11 +++ .../Rendering/Composition/Visual.cs | 2 +- .../Rendering/Composition/VisualCollection.cs | 5 ++ .../Threading/ThreadSafeObjectPool.cs | 4 +- src/Avalonia.Base/composition-schema.xml | 4 + 18 files changed, 297 insertions(+), 48 deletions(-) create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs diff --git a/samples/RenderDemo/App.xaml.cs b/samples/RenderDemo/App.xaml.cs index 8054b06964..8f4e02df01 100644 --- a/samples/RenderDemo/App.xaml.cs +++ b/samples/RenderDemo/App.xaml.cs @@ -29,6 +29,10 @@ namespace RenderDemo .With(new Win32PlatformOptions { OverlayPopups = true, + }) + .With(new X11PlatformOptions + { + UseCompositor = true }) .UsePlatformDetect() .LogToTrace(); diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 07ac54b634..22fc6a77d4 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -37,9 +37,19 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor _target.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor); _update = Update; } - - public bool DrawFps { get; set; } - public bool DrawDirtyRects { get; set; } + + public bool DrawFps + { + get => _target.DrawFps; + set => _target.DrawFps = value; + } + + public bool DrawDirtyRects + { + get => _target.DrawDirtyRects; + set => _target.DrawDirtyRects = value; + } + public event EventHandler? SceneInvalidated; void QueueUpdate() @@ -57,7 +67,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor public IEnumerable HitTest(Point p, IVisual root, Func filter) { - var res = _target.TryHitTest(new Vector2((float)p.X, (float)p.Y)); + var res = _target.TryHitTest(p); if(res == null) yield break; for (var index = res.Count - 1; index >= 0; index--) @@ -146,7 +156,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor renderTransform *= mirrorMatrix; } - comp.TransformMatrix = renderTransform; + comp.TransformMatrix = MatrixUtils.ToMatrix4x4(renderTransform); _recorder.BeginUpdate(comp.DrawList ?? new CompositionDrawList()); visual.Render(_recordingContext); @@ -159,6 +169,8 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor SyncChildren(v); _dirty.Clear(); _recalculateChildren.Clear(); + _target.Size = _root.ClientSize; + _target.Scaling = _root.RenderScaling; } public void Resized(Size size) @@ -169,7 +181,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor { // We render only on the render thread for now Update(); - + _target.RequestRedraw(); Compositor.RequestCommitAsync().Wait(); } @@ -177,7 +189,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor public void Stop() { - _target.IsEnabled = true; + _target.IsEnabled = false; } public void Dispose() diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index 9f02055412..b19c311663 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -29,11 +29,10 @@ internal class CompositionDrawListVisual : CompositionContainerVisual Visual = visual; } - internal override bool HitTest(Vector2 point) + internal override bool HitTest(Point pt) { if (DrawList == null) return false; - var pt = new Point(point.X, point.Y); if (Visual is ICustomHitTest custom) return custom.HitTest(pt); foreach (var op in DrawList) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index a8835ca668..8d052389c2 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -18,7 +18,7 @@ namespace Avalonia.Rendering.Composition Root.Root = null; } - public PooledList? TryHitTest(Vector2 point) + public PooledList? TryHitTest(Point point) { Server.Readback.NextRead(); if (Root == null) @@ -28,12 +28,12 @@ namespace Avalonia.Rendering.Composition return res; } - public Vector2? TryTransformToVisual(CompositionVisual visual, Vector2 point) + public Point? TryTransformToVisual(CompositionVisual visual, Point point) { if (visual.Root != this) return null; var v = visual; - var m = Matrix3x2.Identity; + var m = Matrix.Identity; while (v != null) { if (!TryGetInvertedTransform(v, out var cm)) @@ -42,10 +42,10 @@ namespace Avalonia.Rendering.Composition v = v.Parent; } - return Vector2.Transform(point, m); + return point * m; } - bool TryGetInvertedTransform(CompositionVisual visual, out Matrix3x2 matrix) + bool TryGetInvertedTransform(CompositionVisual visual, out Matrix matrix) { var m = visual.TryGetServerTransform(); if (m == null) @@ -54,24 +54,22 @@ namespace Avalonia.Rendering.Composition return false; } - // TODO: Use Matrix3x3 - var m32 = new Matrix3x2(m.Value.M11, m.Value.M12, m.Value.M21, m.Value.M22, m.Value.M41, m.Value.M42); - - return Matrix3x2.Invert(m32, out matrix); + var m33 = MatrixUtils.ToMatrix(m.Value); + return m33.TryInvert(out matrix); } - bool TryTransformTo(CompositionVisual visual, ref Vector2 v) + bool TryTransformTo(CompositionVisual visual, ref Point v) { if (TryGetInvertedTransform(visual, out var m)) { - v = Vector2.Transform(v, m); + v = v * m; return true; } return false; } - bool HitTestCore(CompositionVisual visual, Vector2 point, PooledList result) + bool HitTestCore(CompositionVisual visual, Point point, PooledList result) { //TODO: Check readback too if (visual.Visible == false) @@ -103,5 +101,7 @@ namespace Avalonia.Rendering.Composition return false; } + + public void RequestRedraw() => Changes.RedrawRequested.Value = true; } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 217d8dd803..14d779dbc4 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Numerics; using System.Threading.Tasks; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Server; diff --git a/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs index 5e91bcb3d4..2cb500cae4 100644 --- a/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs +++ b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs @@ -42,5 +42,25 @@ namespace Avalonia.Rendering.Composition return mat; } + + public static Matrix4x4 ToMatrix4x4(Matrix matrix) => + new Matrix4x4( + (float)matrix.M11, (float)matrix.M12, 0, (float)matrix.M13, + (float)matrix.M21, (float)matrix.M22, 0, (float)matrix.M23, + 0, 0, 1, 0, + (float)matrix.M31, (float)matrix.M32, 0, (float)matrix.M33 + ); + + public static Matrix ToMatrix(Matrix4x4 matrix44) => + new Matrix( + matrix44.M11, + matrix44.M12, + matrix44.M14, + matrix44.M21, + matrix44.M22, + matrix44.M24, + matrix44.M41, + matrix44.M42, + matrix44.M44); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index f5dfa92897..7d061e86a9 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -135,8 +135,4 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl { _impl.Custom(custom); } - - public Matrix CutTransform(Matrix4x4 transform) => new Matrix(transform.M11, transform.M12, transform.M21, - transform.M22, transform.M41, - transform.M42); } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs new file mode 100644 index 0000000000..8c2e6e774a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -0,0 +1,73 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal class FpsCounter +{ + private readonly GlyphTypeface _typeface; + private readonly bool _useManualFpsCounting; + private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); + private int _framesThisSecond; + private int _fps; + private TimeSpan _lastFpsUpdate; + private GlyphRun[] _runs = new GlyphRun[10]; + + public FpsCounter(GlyphTypeface typeface, bool useManualFpsCounting = false) + { + for (var c = 0; c <= 9; c++) + { + var s = c.ToString(); + var glyph = typeface.GetGlyph((uint)(s[0])); + _runs[c] = new GlyphRun(typeface, 18, new ReadOnlySlice(s.AsMemory()), new ushort[] { glyph }); + } + _typeface = typeface; + _useManualFpsCounting = useManualFpsCounting; + } + + public void FpsTick() => _framesThisSecond++; + + public void RenderFps(IDrawingContextImpl context) + { + var now = _stopwatch.Elapsed; + var elapsed = now - _lastFpsUpdate; + + if (!_useManualFpsCounting) + ++_framesThisSecond; + + if (elapsed.TotalSeconds > 1) + { + _fps = (int)(_framesThisSecond / elapsed.TotalSeconds); + _framesThisSecond = 0; + _lastFpsUpdate = now; + } + + var fpsLine = _fps.ToString("000"); + double width = 0; + double height = 0; + foreach (var ch in fpsLine) + { + var run = _runs[ch - '0']; + width += run.Size.Width; + height = Math.Max(height, run.Size.Height); + } + + var rect = new Rect(0, 0, width + 3, height + 3); + + context.DrawRectangle(Brushes.Black, null, rect); + + double offset = 0; + foreach (var ch in fpsLine) + { + var run = _runs[ch - '0']; + context.Transform = Matrix.CreateTranslation(offset, 0); + context.DrawGlyphRun(Brushes.White, run); + offset += run.Size.Width; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index 397c968d04..f0384c36fc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -16,6 +16,25 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua { } + Rect? _contentBounds; + + public override Rect ContentBounds + { + get + { + if (_contentBounds == null) + { + var rect = Rect.Empty; + if(_renderCommands!=null) + foreach (var cmd in _renderCommands) + rect = rect.Union(cmd.Item.Bounds); + _contentBounds = rect; + } + + return _contentBounds.Value; + } + } + protected override void ApplyCore(ChangeSet changes) { var ch = (DrawListVisualChanges)changes; @@ -23,6 +42,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua { _renderCommands?.Dispose(); _renderCommands = ch.AcquireDrawCommands(); + _contentBounds = null; } base.ApplyCore(changes); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 493529e111..04ec711455 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -2,7 +2,10 @@ using System; using System.Numerics; using System.Threading; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Platform; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Server { @@ -14,6 +17,13 @@ namespace Avalonia.Rendering.Composition.Server public long Id { get; } private ulong _frame = 1; private IRenderTarget? _renderTarget; + private FpsCounter _fpsCounter = new FpsCounter(Typeface.Default.GlyphTypeface); + private Rect _dirtyRect; + private Random _random = new(); + private Size _layerSize; + private IDrawingContextLayerImpl? _layer; + private bool _redrawRequested; + public ReadbackIndices Readback { get; } = new(); @@ -33,6 +43,11 @@ namespace Avalonia.Rendering.Composition.Server _compositor.RemoveCompositionTarget(this); } + partial void ApplyChangesExtra(CompositionTargetChanges c) + { + _redrawRequested = true; + } + public void Render() { if (Root == null) @@ -40,14 +55,76 @@ namespace Avalonia.Rendering.Composition.Server _renderTarget ??= _renderTargetFactory(); Compositor.UpdateServerTime(); - using (var context = _renderTarget.CreateDrawingContext(null)) + + Root.Update(this, Matrix4x4.Identity); + + if(_dirtyRect.IsEmpty && !_redrawRequested) + return; + _redrawRequested = false; + using (var targetContext = _renderTarget.CreateDrawingContext(null)) { - context.Clear(Colors.Transparent); - Root.Render(new CompositorDrawingContextProxy(context), Root.CombinedTransformMatrix); + var layerSize = Size * Scaling; + if (layerSize != _layerSize || _layer == null) + { + _layer?.Dispose(); + _layer = null; + _layer = targetContext.CreateLayer(layerSize); + _layerSize = layerSize; + } + + if (!_dirtyRect.IsEmpty) + { + using (var context = _layer.CreateDrawingContext(null)) + { + context.PushClip(_dirtyRect); + context.Clear(Colors.Transparent); + Root.Render(new CompositorDrawingContextProxy(context), Root.CombinedTransformMatrix); + context.PopClip(); + } + } + + targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1, new Rect(_layerSize), + new Rect(_layerSize)); + + + if (DrawDirtyRects) + { + targetContext.DrawRectangle(new ImmutableSolidColorBrush( + new Color(30, (byte)_random.Next(255), (byte)_random.Next(255), + (byte)_random.Next(255))) + , null, _dirtyRect); + } + + if(DrawFps) + _fpsCounter.RenderFps(targetContext); + _dirtyRect = Rect.Empty; + } Readback.NextWrite(_frame); _frame++; } + + private static Rect SnapToDevicePixels(Rect rect, double scale) + { + return new Rect( + new Point( + Math.Floor(rect.X * scale) / scale, + Math.Floor(rect.Y * scale) / scale), + new Point( + Math.Ceiling(rect.Right * scale) / scale, + Math.Ceiling(rect.Bottom * scale) / scale)); + } + + public void AddDirtyRect(Rect rect) + { + var snapped = SnapToDevicePixels(rect, Scaling); + _dirtyRect = _dirtyRect.Union(snapped); + } + + public void Invalidate() + { + _redrawRequested = true; + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs index ac112b846f..3f0995b257 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs @@ -21,6 +21,13 @@ namespace Avalonia.Rendering.Composition.Server } } + public override void Update(ServerCompositionTarget root, Matrix4x4 transform) + { + base.Update(root, transform); + foreach (var child in Children) + child.Update(root, GlobalTransformMatrix); + } + public ServerCompositionContainerVisual(ServerCompositor compositor) : base(compositor) { Children = new ServerCompositionVisualCollection(compositor); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs index 60569867de..786779bb2d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs @@ -8,7 +8,7 @@ namespace Avalonia.Rendering.Composition.Server { protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { - canvas.Transform = canvas.CutTransform(transform); + canvas.Transform = MatrixUtils.ToMatrix(transform); canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new RoundedRect(new Rect(new Size(Size)))); base.RenderCore(canvas, transform); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 37e188fb47..90b580bfa7 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -6,6 +6,7 @@ namespace Avalonia.Rendering.Composition.Server { unsafe partial class ServerCompositionVisual : ServerObject { + private bool _isDirty; protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { @@ -17,7 +18,7 @@ namespace Avalonia.Rendering.Composition.Server return; if(Opacity == 0) return; - canvas.PreTransform = canvas.CutTransform(transform); + canvas.PreTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; if (Opacity != 1) canvas.PushOpacity(Opacity); @@ -25,7 +26,8 @@ namespace Avalonia.Rendering.Composition.Server canvas.PushClip(new Rect(new Size(Size.X, Size.Y))); if (Clip != null) canvas.PushGeometryClip(Clip); - + + //TODO: Check clip RenderCore(canvas, transform); if (Clip != null) @@ -48,22 +50,31 @@ namespace Avalonia.Rendering.Composition.Server return ref _readback2; } - public Matrix4x4 CombinedTransformMatrix + public Matrix4x4 CombinedTransformMatrix { get; private set; } + public Matrix4x4 GlobalTransformMatrix { get; private set; } + + public virtual void Update(ServerCompositionTarget root, Matrix4x4 transform) { - get + var res = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, + Scale, RotationAngle, Orientation, Offset); + var i = Root!.Readback; + ref var readback = ref GetReadback(i.WriteIndex); + readback.Revision = i.WriteRevision; + readback.Matrix = res; + readback.TargetId = Root.Id; + //TODO: check effective opacity too + IsVisibleInFrame = Visible && Opacity > 0; + CombinedTransformMatrix = res; + GlobalTransformMatrix = res * transform; + //TODO: Cache + TransformedBounds = ContentBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); + + if (!IsVisibleInFrame) + _isDirty = false; + else if (_isDirty) { - if (Root == null) - return default; - - var res = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, - Scale, RotationAngle, Orientation, Offset); - var i = Root.Readback; - ref var readback = ref GetReadback(i.WriteIndex); - readback.Revision = i.WriteRevision; - readback.Matrix = res; - readback.TargetId = Root.Id; - - return res; + Root.AddDirtyRect(TransformedBounds); + _isDirty = false; } } @@ -81,11 +92,20 @@ namespace Avalonia.Rendering.Composition.Server Parent = c.Parent.Value; if (c.Root.IsSet) Root = c.Root.Value; + _isDirty = true; + + if (IsVisibleInFrame) + Root?.AddDirtyRect(TransformedBounds); + else + Root?.Invalidate(); } public ServerCompositionTarget? Root { get; private set; } public ServerCompositionVisual? Parent { get; private set; } + public bool IsVisibleInFrame { get; set; } + public Rect TransformedBounds { get; set; } + public virtual Rect ContentBounds => new Rect(0, 0, Size.X, Size.Y); } diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs new file mode 100644 index 0000000000..014adc7bbe --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs @@ -0,0 +1,11 @@ +namespace Avalonia.Rendering.Composition.Transport; + +partial class CompositionTargetChanges +{ + public Change RedrawRequested; + + partial void ResetExtra() + { + RedrawRequested.Reset(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index 1e6d7f8abb..25fce01de3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -59,6 +59,6 @@ namespace Avalonia.Rendering.Composition internal object? Tag { get; set; } - internal virtual bool HitTest(Vector2 point) => true; + internal virtual bool HitTest(Point point) => true; } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs index 35f33c3b38..fef4caf675 100644 --- a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs +++ b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs @@ -54,6 +54,11 @@ namespace Avalonia.Rendering.Composition partial void OnRemoved(CompositionVisual item) => item.Parent = null; + partial void OnBeforeClear() + { + foreach (var i in this) + i.Parent = null; + } partial void OnBeforeAdded(CompositionVisual item) { diff --git a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs index 05995f2069..827a02334a 100644 --- a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs +++ b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs @@ -9,7 +9,7 @@ namespace Avalonia.Threading public T Get() { - lock (_lock) + lock (_stack) { if(_stack.Count == 0) return new T(); @@ -19,7 +19,7 @@ namespace Avalonia.Threading public void Return(T obj) { - lock (_lock) + lock (_stack) { _stack.Push(obj); } diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index cf864dac2d..eb1ffe1922 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -25,6 +25,10 @@ + + + + From 0e8672017b4ba40c78413dc73a1e2ba0dcad971a Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 11 May 2022 20:08:14 +0300 Subject: [PATCH 006/171] Workaround for #8118 --- src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 90b580bfa7..bf5b8bf292 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -30,6 +30,9 @@ namespace Avalonia.Rendering.Composition.Server //TODO: Check clip RenderCore(canvas, transform); + canvas.PreTransform = MatrixUtils.ToMatrix(transform); + canvas.Transform = Matrix.Identity; + if (Clip != null) canvas.PopGeometryClip(); if (ClipToBounds) From 9ac37065dca2a23006617dc649debd3392e23b38 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 24 May 2022 19:49:43 +0300 Subject: [PATCH 007/171] Animations now work... more or less? --- samples/ControlCatalog/MainView.xaml | 3 + .../Pages/CompositionPage.axaml | 45 +++++ .../Pages/CompositionPage.axaml.cs | 157 ++++++++++++++++++ .../Animations/AnimatedValueStore.cs | 88 +++++++++- .../Animations/AnimationInstanceBase.cs | 77 +++++++++ .../Animations/CompositionAnimation.cs | 2 +- .../Animations/ExpressionAnimationInstance.cs | 25 +-- .../Animations/IAnimationInstance.cs | 7 +- .../Animations/KeyFrameAnimationInstance.cs | 59 +++++-- .../Composition/Animations/KeyFrames.cs | 2 +- .../Composition/CompositionPropertySet.cs | 12 +- .../Composition/ElementCompositionPreview.cs | 6 + .../Composition/Expressions/Expression.cs | 45 ++++- .../ExpressionEvaluationContext.cs | 1 + .../Expressions/ExpressionTrackedValues.cs | 57 +++++++ .../Composition/Server/FpsCounter.cs | 27 ++- .../Server/ServerCompositionTarget.cs | 5 +- .../Composition/Server/ServerCompositor.cs | 20 +++ .../Composition/Server/ServerObject.cs | 82 ++++++++- .../Composition/Server/ServerVisual.cs | 22 ++- .../Rendering/Composition/Visual.cs | 1 - .../Utilities/RefTrackingDictionary.cs | 67 ++++++++ .../Avalonia.SourceGenerator.csproj | 1 + .../CompositionRoslynGenerator.cs | 26 +-- .../CompositionGenerator/Generator.cs | 102 ++++++++++-- .../ICompositionGeneratorSink.cs | 6 + .../RoslynCompositionGeneratorSink.cs | 15 ++ 27 files changed, 873 insertions(+), 87 deletions(-) create mode 100644 samples/ControlCatalog/Pages/CompositionPage.axaml create mode 100644 samples/ControlCatalog/Pages/CompositionPage.axaml.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionTrackedValues.cs create mode 100644 src/Avalonia.Base/Utilities/RefTrackingDictionary.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/ICompositionGeneratorSink.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/RoslynCompositionGeneratorSink.cs diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 59d724db69..2ce5ab3934 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -13,6 +13,9 @@ + + + diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml b/samples/ControlCatalog/Pages/CompositionPage.axaml new file mode 100644 index 0000000000..592290fde5 --- /dev/null +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml @@ -0,0 +1,45 @@ + + + Implicit animations + + + + + + + + + + + + + + + + + + + + + + + Resize me + + + + + + + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs new file mode 100644 index 0000000000..b37231243d --- /dev/null +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Templates; +using Avalonia.Media; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages; + +public partial class CompositionPage : UserControl +{ + private ImplicitAnimationCollection _implicitAnimations; + + public CompositionPage() + { + AvaloniaXamlLoader.Load(this); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + this.FindControl("Items").Items = CreateColorItems(); + } + + private List CreateColorItems() + { + var list = new List(); + + list.Add(new ColorItem(Color.FromArgb(255, 255, 185, 0))); + list.Add(new ColorItem(Color.FromArgb(255, 231, 72, 86))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 120, 215))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 153, 188))); + list.Add(new ColorItem(Color.FromArgb(255, 122, 117, 116))); + list.Add(new ColorItem(Color.FromArgb(255, 118, 118, 118))); + list.Add(new ColorItem(Color.FromArgb(255, 255, 141, 0))); + list.Add(new ColorItem(Color.FromArgb(255, 232, 17, 35))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 99, 177))); + list.Add(new ColorItem(Color.FromArgb(255, 45, 125, 154))); + list.Add(new ColorItem(Color.FromArgb(255, 93, 90, 88))); + list.Add(new ColorItem(Color.FromArgb(255, 76, 74, 72))); + list.Add(new ColorItem(Color.FromArgb(255, 247, 99, 12))); + list.Add(new ColorItem(Color.FromArgb(255, 234, 0, 94))); + list.Add(new ColorItem(Color.FromArgb(255, 142, 140, 216))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 183, 195))); + list.Add(new ColorItem(Color.FromArgb(255, 104, 118, 138))); + list.Add(new ColorItem(Color.FromArgb(255, 105, 121, 126))); + list.Add(new ColorItem(Color.FromArgb(255, 202, 80, 16))); + list.Add(new ColorItem(Color.FromArgb(255, 195, 0, 82))); + list.Add(new ColorItem(Color.FromArgb(255, 107, 105, 214))); + list.Add(new ColorItem(Color.FromArgb(255, 3, 131, 135))); + list.Add(new ColorItem(Color.FromArgb(255, 81, 92, 107))); + list.Add(new ColorItem(Color.FromArgb(255, 74, 84, 89))); + list.Add(new ColorItem(Color.FromArgb(255, 218, 59, 1))); + list.Add(new ColorItem(Color.FromArgb(255, 227, 0, 140))); + list.Add(new ColorItem(Color.FromArgb(255, 135, 100, 184))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 178, 148))); + list.Add(new ColorItem(Color.FromArgb(255, 86, 124, 115))); + list.Add(new ColorItem(Color.FromArgb(255, 100, 124, 100))); + list.Add(new ColorItem(Color.FromArgb(255, 239, 105, 80))); + list.Add(new ColorItem(Color.FromArgb(255, 191, 0, 119))); + list.Add(new ColorItem(Color.FromArgb(255, 116, 77, 169))); + list.Add(new ColorItem(Color.FromArgb(255, 1, 133, 116))); + list.Add(new ColorItem(Color.FromArgb(255, 72, 104, 96))); + list.Add(new ColorItem(Color.FromArgb(255, 82, 94, 84))); + list.Add(new ColorItem(Color.FromArgb(255, 209, 52, 56))); + list.Add(new ColorItem(Color.FromArgb(255, 194, 57, 179))); + list.Add(new ColorItem(Color.FromArgb(255, 177, 70, 194))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 204, 106))); + list.Add(new ColorItem(Color.FromArgb(255, 73, 130, 5))); + list.Add(new ColorItem(Color.FromArgb(255, 132, 117, 69))); + list.Add(new ColorItem(Color.FromArgb(255, 255, 67, 67))); + list.Add(new ColorItem(Color.FromArgb(255, 154, 0, 137))); + list.Add(new ColorItem(Color.FromArgb(255, 136, 23, 152))); + list.Add(new ColorItem(Color.FromArgb(255, 16, 137, 62))); + list.Add(new ColorItem(Color.FromArgb(255, 16, 124, 16))); + list.Add(new ColorItem(Color.FromArgb(255, 126, 115, 95))); + + return list; + } + + public class ColorItem + { + public Color Color { get; private set; } + + public SolidColorBrush ColorBrush + { + get { return new SolidColorBrush(Color); } + } + + public String ColorHexValue + { + get { return Color.ToString().Substring(3).ToUpperInvariant(); } + } + + public ColorItem(Color color) + { + Color = color; + } + } + + private void EnsureImplicitAnimations() + { + if (_implicitAnimations == null) + { + var compositor = ElementCompositionPreview.GetElementVisual(this)!.Compositor; + + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + offsetAnimation.Duration = TimeSpan.FromMilliseconds(400); + + var rotationAnimation = compositor.CreateScalarKeyFrameAnimation(); + rotationAnimation.Target = "RotationAngle"; + rotationAnimation.InsertKeyFrame(.5f, 0.160f); + rotationAnimation.InsertKeyFrame(1f, 0f); + rotationAnimation.Duration = TimeSpan.FromMilliseconds(400); + + var animationGroup = compositor.CreateAnimationGroup(); + animationGroup.Add(offsetAnimation); + animationGroup.Add(rotationAnimation); + + _implicitAnimations = compositor.CreateImplicitAnimationCollection(); + _implicitAnimations["Offset"] = animationGroup; + } + } + + public static void SetEnableAnimations(Border border, bool value) + { + + var page = border.FindAncestorOfType(); + if (page == null) + { + border.AttachedToVisualTree += delegate { SetEnableAnimations(border, true); }; + return; + } + + if (ElementCompositionPreview.GetElementVisual(page) == null) + return; + + page.EnsureImplicitAnimations(); + ElementCompositionPreview.GetElementVisual((Visual)border.GetVisualParent()).ImplicitAnimations = + page._implicitAnimations; + } + + + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs index 180c45022f..e877b50b20 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs @@ -1,19 +1,62 @@ +using System.Runtime.InteropServices; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Animations { - internal struct AnimatedValueStore where T : struct + internal struct ServerObjectSubscriptionStore { + public bool IsValid; + public RefTrackingDictionary Subscribers; + + public void Invalidate() + { + if (IsValid) + return; + IsValid = false; + if (Subscribers != null) + foreach (var sub in Subscribers) + sub.Key.Invalidate(); + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct ServerValueStore + { + // HAS TO BE THE FIRST FIELD, accessed by field offset from ServerObject + private ServerObjectSubscriptionStore Subscriptions; + private T _value; + public T Value + { + set + { + _value = value; + Subscriptions.Invalidate(); + } + get + { + Subscriptions.IsValid = true; + return _value; + } + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct ServerAnimatedValueStore where T : struct + { + // HAS TO BE THE FIRST FIELD, accessed by field offset from ServerObject + private ServerObjectSubscriptionStore Subscriptions; + private IAnimationInstance? _animation; private T _direct; - private IAnimationInstance _animation; private T? _lastAnimated; public T Direct => _direct; public T GetAnimated(ServerCompositor compositor) { + Subscriptions.IsValid = true; if (_animation == null) return _direct; var v = _animation.Evaluate(compositor.ServerNow, ExpressionVariant.Create(_direct)) @@ -22,19 +65,50 @@ namespace Avalonia.Rendering.Composition.Animations return v; } + public void Activate(ServerObject parent) + { + if (_animation != null) + _animation.Activate(); + } + + public void Deactivate(ServerObject parent) + { + if (_animation != null) + _animation.Deactivate(); + } + private T LastAnimated => _animation != null ? _lastAnimated ?? _direct : _direct; public bool IsAnimation => _animation != null; - public void SetAnimation(ChangeSet cs, IAnimationInstance animation) + public void SetAnimation(ServerObject target, ChangeSet cs, IAnimationInstance animation, int storeOffset) { + _direct = default; + if (_animation != null) + { + if (target.IsActive) + _animation.Deactivate(); + } + _animation = animation; - _animation.Start(cs.Batch.CommitedAt, ExpressionVariant.Create(LastAnimated)); + _animation.Initialize(cs.Batch.CommitedAt, ExpressionVariant.Create(LastAnimated), storeOffset); + if (target.IsActive) + _animation.Activate(); + + Subscriptions.Invalidate(); } - public static implicit operator AnimatedValueStore(T value) => new AnimatedValueStore() + public void SetValue(ServerObject target, T value) { - _direct = value - }; + if (_animation != null) + { + if (target.IsActive) + _animation.Deactivate(); + } + + _animation = null; + _direct = value; + Subscriptions.Invalidate(); + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs new file mode 100644 index 0000000000..212237049f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Animations; + +internal abstract class AnimationInstanceBase : IAnimationInstance +{ + private List<(ServerObject obj, int member)>? _trackedObjects; + protected PropertySetSnapshot Parameters { get; } + public ServerObject TargetObject { get; } + protected int StoreOffset { get; private set; } + private bool _invalidated; + + public AnimationInstanceBase(ServerObject target, PropertySetSnapshot parameters) + { + Parameters = parameters; + TargetObject = target; + } + + protected void Initialize(int storeOffset, HashSet<(string name, string member)> trackedObjects) + { + if (trackedObjects.Count > 0) + { + _trackedObjects = new (); + foreach (var t in trackedObjects) + { + var obj = Parameters.GetObjectParameter(t.name); + if (obj is ServerObject tracked) + { + var off = tracked.GetFieldOffset(t.member); + if (off == null) +#if DEBUG + throw new InvalidCastException("Attempting to subscribe to unknown field"); +#else + continue; +#endif + _trackedObjects.Add((tracked, off.Value)); + } + } + } + + StoreOffset = storeOffset; + } + + public abstract void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset); + protected abstract ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue); + + public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) + { + _invalidated = false; + return EvaluateCore(now, currentValue); + } + + public virtual void Activate() + { + if (_trackedObjects != null) + foreach (var tracked in _trackedObjects) + tracked.obj.SubscribeToInvalidation(tracked.member, this); + } + + public virtual void Deactivate() + { + if (_trackedObjects != null) + foreach (var tracked in _trackedObjects) + tracked.obj.UnsubscribeFromInvalidation(tracked.member, this); + } + + public void Invalidate() + { + if (_invalidated) + return; + _invalidated = true; + TargetObject.NotifyAnimatedValueChanged(StoreOffset); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs index 9375faaaae..fe20115b38 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -53,7 +53,7 @@ namespace Avalonia.Rendering.Composition.Animations ExpressionVariant? finalValue); internal PropertySetSnapshot CreateSnapshot(bool server) - => _propertySet.Snapshot(server, 1); + => _propertySet.Snapshot(server); void ICompositionAnimationBase.InternalOnly() { diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs index 47b947b2e9..7944fe7990 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs @@ -1,22 +1,22 @@ using System; +using System.Collections.Generic; using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Animations { - internal class ExpressionAnimationInstance : IAnimationInstance + internal class ExpressionAnimationInstance : AnimationInstanceBase, IAnimationInstance { private readonly Expression _expression; - private readonly IExpressionObject _target; private ExpressionVariant _startingValue; private readonly ExpressionVariant? _finalValue; - private readonly PropertySetSnapshot _parameters; - public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) + protected override ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue) { var ctx = new ExpressionEvaluationContext { - Parameters = _parameters, - Target = _target, + Parameters = Parameters, + Target = TargetObject, ForeignFunctionInterface = BuiltInExpressionFfi.Instance, StartingValue = _startingValue, FinalValue = _finalValue ?? _startingValue, @@ -25,20 +25,21 @@ namespace Avalonia.Rendering.Composition.Animations return _expression.Evaluate(ref ctx); } - public void Start(TimeSpan startedAt, ExpressionVariant startingValue) + public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset) { _startingValue = startingValue; + var hs = new HashSet<(string, string)>(); + _expression.CollectReferences(hs); + base.Initialize(storeOffset, hs); } - + public ExpressionAnimationInstance(Expression expression, - IExpressionObject target, + ServerObject target, ExpressionVariant? finalValue, - PropertySetSnapshot parameters) + PropertySetSnapshot parameters) : base(target, parameters) { _expression = expression; - _target = target; _finalValue = finalValue; - _parameters = parameters; } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs index a0b066ae0c..05d1b50953 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs @@ -1,11 +1,16 @@ using System; using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Animations { internal interface IAnimationInstance { + ServerObject TargetObject { get; } ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue); - void Start(TimeSpan startedAt, ExpressionVariant startingValue); + void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset); + void Activate(); + void Deactivate(); + void Invalidate(); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs index b90a02148d..9571cef0b4 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs @@ -1,15 +1,15 @@ using System; +using System.Collections.Generic; using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Animations { - class KeyFrameAnimationInstance : IAnimationInstance where T : struct + class KeyFrameAnimationInstance : AnimationInstanceBase, IAnimationInstance where T : struct { private readonly IInterpolator _interpolator; private readonly ServerKeyFrame[] _keyFrames; - private readonly PropertySetSnapshot _snapshot; private readonly ExpressionVariant? _finalValue; - private readonly IExpressionObject _target; private readonly AnimationDelayBehavior _delayBehavior; private readonly TimeSpan _delayTime; private readonly AnimationDirection _direction; @@ -19,21 +19,21 @@ namespace Avalonia.Rendering.Composition.Animations private readonly AnimationStopBehavior _stopBehavior; private TimeSpan _startedAt; private T _startingValue; + private readonly TimeSpan _totalDuration; + private bool _finished; public KeyFrameAnimationInstance( IInterpolator interpolator, ServerKeyFrame[] keyFrames, PropertySetSnapshot snapshot, ExpressionVariant? finalValue, - IExpressionObject target, + ServerObject target, AnimationDelayBehavior delayBehavior, TimeSpan delayTime, AnimationDirection direction, TimeSpan duration, AnimationIterationBehavior iterationBehavior, - int iterationCount, AnimationStopBehavior stopBehavior) + int iterationCount, AnimationStopBehavior stopBehavior) : base(target, snapshot) { _interpolator = interpolator; _keyFrames = keyFrames; - _snapshot = snapshot; _finalValue = finalValue; - _target = target; _delayBehavior = delayBehavior; _delayTime = delayTime; _direction = direction; @@ -41,26 +41,43 @@ namespace Avalonia.Rendering.Composition.Animations _iterationBehavior = iterationBehavior; _iterationCount = iterationCount; _stopBehavior = stopBehavior; + if (_iterationBehavior == AnimationIterationBehavior.Count) + _totalDuration = delayTime.Add(TimeSpan.FromTicks(iterationCount * _duration.Ticks)); if (_keyFrames.Length == 0) throw new InvalidOperationException("Animation has no key frames"); if(_duration.Ticks <= 0) throw new InvalidOperationException("Invalid animation duration"); } - public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) + + protected override ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue) { - var elapsed = now - _startedAt; var starting = ExpressionVariant.Create(_startingValue); var ctx = new ExpressionEvaluationContext { - Parameters = _snapshot, - Target = _target, + Parameters = Parameters, + Target = TargetObject, CurrentValue = currentValue, FinalValue = _finalValue ?? starting, StartingValue = starting, ForeignFunctionInterface = BuiltInExpressionFfi.Instance }; + var elapsed = now - _startedAt; + var res = EvaluateImpl(elapsed, currentValue, ref ctx); + if (_iterationBehavior == AnimationIterationBehavior.Count + && !_finished + && elapsed > _totalDuration) + { + // Active check? + TargetObject.Compositor.RemoveFromClock(this); + _finished = true; + } + return res; + } + + private ExpressionVariant EvaluateImpl(TimeSpan elapsed, ExpressionVariant currentValue, ref ExpressionEvaluationContext ctx) + { if (elapsed < _delayTime) { if (_delayBehavior == AnimationDelayBehavior.SetInitialValueBeforeDelay) @@ -130,10 +147,28 @@ namespace Avalonia.Rendering.Composition.Animations return f.Value; } - public void Start(TimeSpan startedAt, ExpressionVariant startingValue) + public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset) { _startedAt = startedAt; _startingValue = startingValue.CastOrDefault(); + var hs = new HashSet<(string, string)>(); + + // TODO: Update subscriptions based on the current keyframe rather than keeping subscriptions to all of them + foreach (var frame in _keyFrames) + frame.Expression?.CollectReferences(hs); + Initialize(storeOffset, hs); + } + + public override void Activate() + { + TargetObject.Compositor.AddToClock(this); + base.Activate(); + } + + public override void Deactivate() + { + TargetObject.Compositor.RemoveFromClock(this); + base.Deactivate(); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs index d7f2504061..26ba35409d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs @@ -66,7 +66,7 @@ namespace Avalonia.Rendering.Composition.Animations struct ServerKeyFrame { public T Value; - public Expression Expression; + public Expression? Expression; public IEasingFunction EasingFunction; public float Key; } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs index 004c2676ff..bc0ce804dc 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs @@ -24,11 +24,16 @@ namespace Avalonia.Rendering.Composition _variants[key] = value; } + /* + For INTERNAL USE by CompositionAnimation ONLY, we DON'T support expression + paths like SomeParam.SomePropertyObject.SomeValue + */ internal void Set(string key, CompositionObject obj) { _objects[key] = obj ?? throw new ArgumentNullException(nameof(obj)); _variants.Remove(key); } + public void InsertColor(string propertyName, Avalonia.Media.Color value) => Set(propertyName, value); public void InsertMatrix3x2(string propertyName, Matrix3x2 value) => Set(propertyName, value); @@ -99,7 +104,10 @@ namespace Avalonia.Rendering.Composition _variants.Remove(key); } - internal PropertySetSnapshot Snapshot(bool server, int allowedNestingLevel) + internal PropertySetSnapshot Snapshot(bool server) => + SnapshotCore(server, 1); + + private PropertySetSnapshot SnapshotCore(bool server, int allowedNestingLevel) { var dic = new Dictionary(_objects.Count + _variants.Count); foreach (var o in _objects) @@ -108,7 +116,7 @@ namespace Avalonia.Rendering.Composition { if (allowedNestingLevel <= 0) throw new InvalidOperationException("PropertySet depth limit reached"); - dic[o.Key] = new PropertySetSnapshot.Value(ps.Snapshot(server, allowedNestingLevel - 1)); + dic[o.Key] = new PropertySetSnapshot.Value(ps.SnapshotCore(server, allowedNestingLevel - 1)); } else if (o.Value.Server == null) throw new InvalidOperationException($"Object of type {o.Value.GetType()} is not allowed"); diff --git a/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs b/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs new file mode 100644 index 0000000000..afda314276 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs @@ -0,0 +1,6 @@ +namespace Avalonia.Rendering.Composition; + +public static class ElementCompositionPreview +{ + public static CompositionVisual? GetElementVisual(Visual visual) => visual.CompositionVisual; +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs index 5577d2b52a..088771e1ba 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Reflection; +using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Expressions { @@ -15,6 +16,11 @@ namespace Avalonia.Rendering.Composition.Expressions public abstract ExpressionVariant Evaluate(ref ExpressionEvaluationContext context); + public virtual void CollectReferences(HashSet<(string parameter, string property)> references) + { + + } + protected abstract string Print(); public override string ToString() => Print(); @@ -114,6 +120,13 @@ namespace Avalonia.Rendering.Composition.Expressions return FalsePart.Evaluate(ref context); } + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + Condition.CollectReferences(references); + TruePart.CollectReferences(references); + FalsePart.CollectReferences(references); + } + protected override string Print() => $"({Condition}) ? ({TruePart}) : ({FalsePart})"; } @@ -128,6 +141,7 @@ namespace Avalonia.Rendering.Composition.Expressions } public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) => Constant; + protected override string Print() => Constant.ToString(CultureInfo.InvariantCulture); } @@ -155,6 +169,12 @@ namespace Avalonia.Rendering.Composition.Expressions return res; } + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + foreach(var arg in Parameters) + arg.CollectReferences(references); + } + protected override string Print() { return Name + "( (" + string.Join("), (", Parameters) + ") )"; @@ -173,18 +193,30 @@ namespace Avalonia.Rendering.Composition.Expressions Member = string.Intern(member); } + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + Target.CollectReferences(references); + if (Target is ParameterExpression pe) + references.Add((pe.Name, Member)); + } + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) { if (Target is KeywordExpression ke && ke.Keyword == ExpressionKeyword.Target) + { return context.Target.GetProperty(Member); + } + if (Target is ParameterExpression pe) { var obj = context.Parameters?.GetObjectParameter(pe.Name); if (obj != null) + { return obj.GetProperty(Member); + } } - + // Those are considered immutable return Target.Evaluate(ref context).GetProperty(Member); } @@ -263,6 +295,11 @@ namespace Avalonia.Rendering.Composition.Expressions return default; } + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + Parameter.CollectReferences(references); + } + protected override string Print() { return OperatorName(Type) + Parameter; @@ -313,6 +350,12 @@ namespace Avalonia.Rendering.Composition.Expressions return default; } + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + Left.CollectReferences(references); + Right.CollectReferences(references); + } + protected override string Print() { return "(" + Left + OperatorName(Type) + Right + ")"; diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs index a7ddabd70d..9d23551e43 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Expressions { diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionTrackedValues.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionTrackedValues.cs new file mode 100644 index 0000000000..334f975aa0 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionTrackedValues.cs @@ -0,0 +1,57 @@ +using System.Collections; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Expressions; + +internal class ExpressionTrackedObjects : IEnumerable +{ + private List _list = new(); + private HashSet _hashSet = new(); + + public void Add(IExpressionObject obj, string member) + { + if (_hashSet.Add(obj)) + _list.Add(obj); + } + + public void Clear() + { + _list.Clear(); + _hashSet.Clear(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _list.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_list).GetEnumerator(); + } + + public List.Enumerator GetEnumerator() => _list.GetEnumerator(); + + public struct Pool + { + private Stack _stack = new(); + + public Pool() + { + } + + public ExpressionTrackedObjects Get() + { + if (_stack.Count > 0) + return _stack.Pop(); + return new ExpressionTrackedObjects(); + } + + public void Return(ExpressionTrackedObjects obj) + { + _stack.Clear(); + _stack.Push(obj); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs index 8c2e6e774a..a60084d8f3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -10,24 +10,23 @@ namespace Avalonia.Rendering.Composition.Server; internal class FpsCounter { - private readonly GlyphTypeface _typeface; - private readonly bool _useManualFpsCounting; private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); private int _framesThisSecond; + private int _totalFrames; private int _fps; private TimeSpan _lastFpsUpdate; - private GlyphRun[] _runs = new GlyphRun[10]; + const int FirstChar = 32; + const int LastChar = 126; + private GlyphRun[] _runs = new GlyphRun[LastChar - FirstChar + 1]; - public FpsCounter(GlyphTypeface typeface, bool useManualFpsCounting = false) + public FpsCounter(GlyphTypeface typeface) { - for (var c = 0; c <= 9; c++) + for (var c = FirstChar; c <= LastChar; c++) { - var s = c.ToString(); + var s = new string((char)c, 1); var glyph = typeface.GetGlyph((uint)(s[0])); - _runs[c] = new GlyphRun(typeface, 18, new ReadOnlySlice(s.AsMemory()), new ushort[] { glyph }); + _runs[c - FirstChar] = new GlyphRun(typeface, 18, new ReadOnlySlice(s.AsMemory()), new ushort[] { glyph }); } - _typeface = typeface; - _useManualFpsCounting = useManualFpsCounting; } public void FpsTick() => _framesThisSecond++; @@ -37,8 +36,8 @@ internal class FpsCounter var now = _stopwatch.Elapsed; var elapsed = now - _lastFpsUpdate; - if (!_useManualFpsCounting) - ++_framesThisSecond; + ++_framesThisSecond; + ++_totalFrames; if (elapsed.TotalSeconds > 1) { @@ -47,12 +46,12 @@ internal class FpsCounter _lastFpsUpdate = now; } - var fpsLine = _fps.ToString("000"); + var fpsLine = $"Frame #{_totalFrames:00000000} FPS: {_fps:000}"; double width = 0; double height = 0; foreach (var ch in fpsLine) { - var run = _runs[ch - '0']; + var run = _runs[ch - FirstChar]; width += run.Size.Width; height = Math.Max(height, run.Size.Height); } @@ -64,7 +63,7 @@ internal class FpsCounter double offset = 0; foreach (var ch in fpsLine) { - var run = _runs[ch - '0']; + var run = _runs[ch - FirstChar]; context.Transform = Matrix.CreateTranslation(offset, 0); context.DrawGlyphRun(Brushes.White, run); offset += run.Size.Width; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 04ec711455..dab65fc8ed 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -56,10 +56,11 @@ namespace Avalonia.Rendering.Composition.Server Compositor.UpdateServerTime(); - Root.Update(this, Matrix4x4.Identity); - if(_dirtyRect.IsEmpty && !_redrawRequested) return; + + Root.Update(this, Matrix4x4.Identity); + _redrawRequested = false; using (var targetContext = _renderTarget.CreateDrawingContext(null)) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index e56f85acdf..5dbe9cfb17 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Rendering.Composition.Server @@ -13,6 +15,8 @@ namespace Avalonia.Rendering.Composition.Server public Stopwatch Clock { get; } = Stopwatch.StartNew(); public TimeSpan ServerNow { get; private set; } private List _activeTargets = new(); + private HashSet _activeAnimations = new(); + private List _animationsToUpdate = new(); public ServerCompositor(IRenderLoop renderLoop) { @@ -64,6 +68,7 @@ namespace Avalonia.Rendering.Composition.Server } bool IRenderLoopTask.NeedsUpdate => false; + void IRenderLoopTask.Update(TimeSpan time) { } @@ -71,6 +76,15 @@ namespace Avalonia.Rendering.Composition.Server void IRenderLoopTask.Render() { ApplyPendingBatches(); + + foreach(var animation in _activeAnimations) + _animationsToUpdate.Add(animation); + + foreach(var animation in _animationsToUpdate) + animation.Invalidate(); + + _animationsToUpdate.Clear(); + foreach (var t in _activeTargets) t.Render(); @@ -86,5 +100,11 @@ namespace Avalonia.Rendering.Composition.Server { _activeTargets.Remove(target); } + + public void AddToClock(IAnimationInstance animationInstance) => + _activeAnimations.Add(animationInstance); + + public void RemoveFromClock(IAnimationInstance animationInstance) => + _activeAnimations.Remove(animationInstance); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index 072377cd7e..5b2f58b186 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -1,5 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Server { @@ -9,7 +15,9 @@ namespace Avalonia.Rendering.Composition.Server public virtual long LastChangedBy => ItselfLastChangedBy; public long ItselfLastChangedBy { get; private set; } - + private uint _activationCount; + public bool IsActive => _activationCount != 0; + public ServerObject(ServerCompositor compositor) { Compositor = compositor; @@ -23,6 +31,7 @@ namespace Avalonia.Rendering.Composition.Server public void Apply(ChangeSet changes) { ApplyCore(changes); + ValuesInvalidated(); ItselfLastChangedBy = changes.Batch!.SequenceId; } @@ -32,5 +41,76 @@ namespace Avalonia.Rendering.Composition.Server } ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name); + + public void Activate() + { + _activationCount++; + if (_activationCount == 1) + Activated(); + } + + public void Deactivate() + { +#if DEBUG + if (_activationCount == 0) + throw new InvalidOperationException(); +#endif + _activationCount--; + if (_activationCount == 0) + Deactivated(); + } + + protected virtual void Activated() + { + + } + + protected virtual void Deactivated() + { + + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected int GetOffset(ref T field) where T : struct + { + return Unsafe.ByteOffset(ref Unsafe.As(ref _activationCount), + ref Unsafe.As(ref field)) + .ToInt32(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ref ServerObjectSubscriptionStore GetStoreFromOffset(int offset) + { + return ref Unsafe.As(ref Unsafe.AddByteOffset(ref _activationCount, + new IntPtr(offset))); + } + + public void NotifyAnimatedValueChanged(int offset) + { + ref var store = ref GetStoreFromOffset(offset); + store.Invalidate(); + ValuesInvalidated(); + } + + protected virtual void ValuesInvalidated() + { + + } + + public void SubscribeToInvalidation(int member, IAnimationInstance animation) + { + ref var store = ref GetStoreFromOffset(member); + if (store.Subscribers.AddRef(animation)) + Activate(); + } + + public void UnsubscribeFromInvalidation(int member, IAnimationInstance animation) + { + ref var store = ref GetStoreFromOffset(member); + if (store.Subscribers.ReleaseRef(animation)) + Deactivate(); + } + + public virtual int? GetFieldOffset(string fieldName) => null; } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index bf5b8bf292..05b63a7a73 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -7,6 +7,7 @@ namespace Avalonia.Rendering.Composition.Server unsafe partial class ServerCompositionVisual : ServerObject { private bool _isDirty; + private ServerCompositionTarget? _root; protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { @@ -95,16 +96,31 @@ namespace Avalonia.Rendering.Composition.Server Parent = c.Parent.Value; if (c.Root.IsSet) Root = c.Root.Value; - _isDirty = true; + ValuesInvalidated(); + } + + public ServerCompositionTarget? Root + { + get => _root; + private set + { + if(_root != null) + Deactivate(); + _root = value; + if (_root != null) + Activate(); + } + } + protected override void ValuesInvalidated() + { + _isDirty = true; if (IsVisibleInFrame) Root?.AddDirtyRect(TransformedBounds); else Root?.Invalidate(); } - public ServerCompositionTarget? Root { get; private set; } - public ServerCompositionVisual? Parent { get; private set; } public bool IsVisibleInFrame { get; set; } public Rect TransformedBounds { get; set; } diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index 25fce01de3..f7c8078073 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -38,7 +38,6 @@ namespace Avalonia.Rendering.Composition { } - internal Matrix4x4? TryGetServerTransform() { if (Root == null) diff --git a/src/Avalonia.Base/Utilities/RefTrackingDictionary.cs b/src/Avalonia.Base/Utilities/RefTrackingDictionary.cs new file mode 100644 index 0000000000..71305a8305 --- /dev/null +++ b/src/Avalonia.Base/Utilities/RefTrackingDictionary.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Avalonia.Utilities; + +internal class RefTrackingDictionary : Dictionary where TKey : class +{ + /// + /// Increase reference count for a key by 1. + /// + /// true if key was added to the dictionary, false otherwise + public bool AddRef(TKey key) + { +#if NET5_0_OR_GREATER + ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(this, key, out var _); + count++; +#else + TryGetValue(key, out var count); + count++; + this[key] = count; +#endif + return count == 1; + } + + /// + /// Decrease reference count for a key by 1. + /// + /// true if key was removed to the dictionary, false otherwise + public bool ReleaseRef(TKey key) + { +#if NET5_0_OR_GREATER + ref var count = ref CollectionsMarshal.GetValueRefOrNullRef(this, key); + if (Unsafe.IsNullRef(ref count)) +#if DEBUG + throw new InvalidOperationException("Attempting to release a non-referenced object"); +#else + return false; +#endif // DEBUG + count--; + if (count == 0) + { + Remove(key); + return true; + } + + return false; +#else + if (!TryGetValue(key, out var count)) +#if DEBUG + throw new InvalidOperationException("Attempting to release a non-referenced object"); +#else + return false; +#endif // DEBUG + count--; + if (count == 0) + { + Remove(key); + return true; + } + + this[key] = count; + return false; +#endif + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj index 97e58f8a64..3312f7a619 100644 --- a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj +++ b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj @@ -3,6 +3,7 @@ netstandard2.0 enable + false diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs index f079a339df..72311b4d18 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs @@ -2,20 +2,22 @@ using System.IO; using System.Xml.Serialization; using Microsoft.CodeAnalysis; -namespace Avalonia.SourceGenerator.CompositionGenerator; - -[Generator(LanguageNames.CSharp)] -public class CompositionRoslynGenerator : IIncrementalGenerator +namespace Avalonia.SourceGenerator.CompositionGenerator { - public void Initialize(IncrementalGeneratorInitializationContext context) + [Generator(LanguageNames.CSharp)] + public class CompositionRoslynGenerator : IIncrementalGenerator { - var schema = context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith("composition-schema.xml")); - var configs = schema.Select((t, _) => - (GConfig)new XmlSerializer(typeof(GConfig)).Deserialize(new StringReader(t.GetText().ToString()))); - context.RegisterSourceOutput(configs, (spc, config) => + public void Initialize(IncrementalGeneratorInitializationContext context) { - var generator = new Generator(spc, config); - generator.Generate(); - }); + var schema = + context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith("composition-schema.xml")); + var configs = schema.Select((t, _) => + (GConfig)new XmlSerializer(typeof(GConfig)).Deserialize(new StringReader(t.GetText().ToString()))); + context.RegisterSourceOutput(configs, (spc, config) => + { + var generator = new Generator(new RoslynCompositionGeneratorSink(spc), config); + generator.Generate(); + }); + } } } \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs index 43ef4a96e8..5a514a4eff 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs @@ -7,14 +7,14 @@ using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Avalonia.SourceGenerator.CompositionGenerator.Extensions; namespace Avalonia.SourceGenerator.CompositionGenerator { - partial class Generator + public partial class Generator { - private readonly SourceProductionContext _output; + private readonly ICompositionGeneratorSink _output; private readonly GConfig _config; private readonly HashSet _objects; private readonly HashSet _brushes; private readonly Dictionary _manuals; - public Generator(SourceProductionContext output, GConfig config) + public Generator(ICompositionGeneratorSink output, GConfig config) { _output = output; _config = config; @@ -168,17 +168,35 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ExpressionStatement(InvocationExpression(IdentifierName("ApplyChangesExtra")) .AddArgumentListArguments(Argument(IdentifierName("c")))) ); + + var uninitializedObjectName = "dummy"; + var serverStaticCtorBody = cl.Abstract + ? Block() + : Block( + ParseStatement( + $"var dummy = ({serverName})System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof({serverName}));"), + ParseStatement($"System.GC.SuppressFinalize(dummy);"), + ParseStatement("InitializeFieldOffsets(dummy);") + ); + + var initializeFieldOffsetsBody = cl.ServerBase == null + ? Block() + : Block(ParseStatement($"{cl.ServerBase}.InitializeFieldOffsets(dummy);")); var resetBody = Block(); var startAnimationBody = Block(); var getPropertyBody = Block(); var serverGetPropertyBody = Block(); + var serverGetFieldOffsetBody = Block(); + var activatedBody = Block(ParseStatement("base.Activated();")); + var deactivatedBody = Block(ParseStatement("base.Deactivated();")); var defaultsMethodBody = Block(); foreach (var prop in cl.Properties) { var fieldName = "_" + prop.Name.WithLowerFirst(); + var fieldOffsetName = "s_OffsetOf" + fieldName; var propType = ParseTypeName(prop.Type); var filteredPropertyType = prop.Type.TrimEnd('?'); var isObject = _objects.Contains(filteredPropertyType); @@ -229,7 +247,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator if (animatedServer) server = server.AddMembers( - DeclareField("AnimatedValueStore<" + serverPropertyType + ">", fieldName), + DeclareField("ServerAnimatedValueStore<" + serverPropertyType + ">", fieldName), PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) .AddModifiers(SyntaxKind.PublicKeyword) .WithExpressionBody(ArrowExpressionClause( @@ -240,24 +258,24 @@ namespace Avalonia.SourceGenerator.CompositionGenerator else { server = server - .AddMembers(DeclareField(serverPropertyType, fieldName)) + .AddMembers(DeclareField("ServerValueStore<" + serverPropertyType + ">", fieldName)) .AddMembers(PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) .AddModifiers(SyntaxKind.PublicKeyword) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, - Block(ReturnStatement(IdentifierName(fieldName)))), + Block(ReturnStatement(MemberAccess(IdentifierName(fieldName), "Value")))), AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, Block( ParseStatement("var changed = false;"), IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, - IdentifierName(fieldName), + MemberAccess(IdentifierName(fieldName), "Value"), IdentifierName("value")), Block( ParseStatement("On" + prop.Name + "Changing();"), ParseStatement($"changed = true;")) ), ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, - IdentifierName(fieldName), IdentifierName("value"))), + MemberAccess(IdentifierName(fieldName), "Value"), IdentifierName("value"))), ParseStatement($"if(changed) On" + prop.Name + "Changed();") )) )) @@ -270,15 +288,22 @@ namespace Avalonia.SourceGenerator.CompositionGenerator if (animatedServer) applyMethodBody = applyMethodBody.AddStatements( IfStatement(MemberAccess(changesVar, prop.Name, "IsValue"), - ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, - IdentifierName(fieldName), MemberAccess(changesVar, prop.Name, "Value")))), + ExpressionStatement( + InvocationExpression(MemberAccess(fieldName, "SetValue"), + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("this")), + Argument(MemberAccess(changesVar, prop.Name, "Value")), + }))))), IfStatement(MemberAccess(changesVar, prop.Name, "IsAnimation"), ExpressionStatement( InvocationExpression(MemberAccess(fieldName, "SetAnimation"), ArgumentList(SeparatedList(new[] { + Argument(IdentifierName("this")), Argument(changesVar), - Argument(MemberAccess(changesVar, prop.Name, "Animation")) + Argument(MemberAccess(changesVar, prop.Name, "Animation")), + Argument(IdentifierName(fieldOffsetName)) }))))) ); else @@ -288,16 +313,28 @@ namespace Avalonia.SourceGenerator.CompositionGenerator IdentifierName(prop.Name), MemberAccess(changesVar, prop.Name, "Value")))) ); - resetBody = resetBody.AddStatements( ExpressionStatement(InvocationExpression(MemberAccess(prop.Name, "Reset")))); if (animatedServer) + { startAnimationBody = ApplyStartAnimation(startAnimationBody, prop, fieldName); + activatedBody = activatedBody.AddStatements(ParseStatement($"{fieldName}.Activate(this);")); + deactivatedBody = deactivatedBody.AddStatements(ParseStatement($"{fieldName}.Deactivate(this);")); + } + getPropertyBody = ApplyGetProperty(getPropertyBody, prop); - serverGetPropertyBody = ApplyGetProperty(getPropertyBody, prop); + serverGetPropertyBody = ApplyGetProperty(serverGetPropertyBody, prop); + serverGetFieldOffsetBody = ApplyGetProperty(serverGetFieldOffsetBody, prop, fieldOffsetName); + + server = server.AddMembers(DeclareField("int", fieldOffsetName, SyntaxKind.StaticKeyword)); + initializeFieldOffsetsBody = initializeFieldOffsetsBody.AddStatements(ExpressionStatement( + AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, IdentifierName(fieldOffsetName), + InvocationExpression(MemberAccess(IdentifierName(uninitializedObjectName), "GetOffset"), + ArgumentList(SingletonSeparatedList(Argument( + RefExpression(MemberAccess(IdentifierName(uninitializedObjectName), fieldName))))))))); if (prop.DefaultValue != null) { @@ -357,6 +394,21 @@ namespace Avalonia.SourceGenerator.CompositionGenerator Parameter(Identifier("changes")).WithType(ParseTypeName("ChangeSet"))) .WithBody(applyMethodBody)); + server = server.AddMembers(ConstructorDeclaration(serverName) + .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))) + .WithBody(serverStaticCtorBody)); + + server = server.AddMembers( + ((MethodDeclarationSyntax)ParseMemberDeclaration( + $"protected static void InitializeFieldOffsets({serverName} dummy){{}}")!) + .WithBody(initializeFieldOffsetsBody)); + + server = server + .AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( + $"protected override void Activated(){{}}")!).WithBody(activatedBody)) + .AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( + $"protected override void Deactivated(){{}}")!).WithBody(deactivatedBody)); + client = client.AddMembers( MethodDeclaration(ParseTypeName("void"), "InitializeDefaults").WithBody(defaultsMethodBody)); @@ -374,7 +426,8 @@ namespace Avalonia.SourceGenerator.CompositionGenerator client = WithGetProperty(client, getPropertyBody, false); server = WithGetProperty(server, serverGetPropertyBody, true); - + server = WithGetFieldOffset(server, serverGetFieldOffsetBody); + if(cl.Implements.Count > 0) foreach (var impl in cl.Implements) { @@ -452,17 +505,19 @@ return; "Vector2", "Vector3", "Vector4", + "Matrix", "Matrix3x2", "Matrix4x4", "Quaternion", - "CompositionColor" + "Color", + "Avalonia.Media.Color" }; - BlockSyntax ApplyGetProperty(BlockSyntax body, GProperty prop) + BlockSyntax ApplyGetProperty(BlockSyntax body, GProperty prop, string? expr = null) { if (VariantPropertyTypes.Contains(prop.Type)) return body.AddStatements( - ParseStatement($"if(name == \"{prop.Name}\")\n return {prop.Name};\n") + ParseStatement($"if(name == \"{prop.Name}\")\n return {expr ?? prop.Name};\n") ); return body; @@ -480,6 +535,19 @@ return; return cl.AddMembers(method); } + + ClassDeclarationSyntax WithGetFieldOffset(ClassDeclarationSyntax cl, BlockSyntax body) + { + if (body.Statements.Count == 0) + return cl; + body = body.AddStatements( + ParseStatement("return base.GetFieldOffset(name);")); + var method = ((MethodDeclarationSyntax)ParseMemberDeclaration( + $"public override int? GetFieldOffset(string name){{}}")) + .WithBody(body); + + return cl.AddMembers(method); + } ClassDeclarationSyntax WithStartAnimation(ClassDeclarationSyntax cl, BlockSyntax body) { diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/ICompositionGeneratorSink.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/ICompositionGeneratorSink.cs new file mode 100644 index 0000000000..085a4041be --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/ICompositionGeneratorSink.cs @@ -0,0 +1,6 @@ +namespace Avalonia.SourceGenerator.CompositionGenerator; + +public interface ICompositionGeneratorSink +{ + void AddSource(string name, string code); +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/RoslynCompositionGeneratorSink.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/RoslynCompositionGeneratorSink.cs new file mode 100644 index 0000000000..6fec3faf93 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/RoslynCompositionGeneratorSink.cs @@ -0,0 +1,15 @@ +using Microsoft.CodeAnalysis; + +namespace Avalonia.SourceGenerator.CompositionGenerator; + +class RoslynCompositionGeneratorSink : ICompositionGeneratorSink +{ + private readonly SourceProductionContext _ctx; + + public RoslynCompositionGeneratorSink(SourceProductionContext ctx) + { + _ctx = ctx; + } + + public void AddSource(string name, string code) => _ctx.AddSource(name, code); +} \ No newline at end of file From bcbd86eca3bda0b3e6aeee03fb063a2ba6811c28 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 25 May 2022 21:15:45 +0300 Subject: [PATCH 008/171] Added VisualBrush support --- .../Composition/CompositionDrawListVisual.cs | 2 +- .../Drawing/CompositionDrawList.cs | 16 +++++++++- .../Drawing/CompositionDrawingContext.cs | 27 ++++++++++------- .../Composition/Server/DrawingContextProxy.cs | 30 ++++++++++++++++++- .../Server/ServerCompositionDrawListVisual.cs | 5 +--- .../Server/ServerCompositionTarget.cs | 5 ++-- .../Rendering/DeferredRenderer.cs | 6 ++-- .../SceneGraph/BrushDrawOperation.cs | 16 +++++++--- .../SceneGraph/DeferredDrawingContextImpl.cs | 4 +-- .../Rendering/SceneGraph/EllipseNode.cs | 7 ++--- .../Rendering/SceneGraph/GeometryNode.cs | 11 +++---- .../Rendering/SceneGraph/GlyphRunNode.cs | 11 +++---- .../Rendering/SceneGraph/LineNode.cs | 8 ++--- .../Rendering/SceneGraph/OpacityMaskNode.cs | 12 ++++---- .../Rendering/SceneGraph/RectangleNode.cs | 11 +++---- 15 files changed, 104 insertions(+), 67 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index b19c311663..e4ed0abd29 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -24,7 +24,7 @@ internal class CompositionDrawListVisual : CompositionContainerVisual private protected override IChangeSetPool ChangeSetPool => DrawListVisualChanges.Pool; - internal CompositionDrawListVisual(Compositor compositor, ServerCompositionContainerVisual server, Visual visual) : base(compositor, server) + internal CompositionDrawListVisual(Compositor compositor, ServerCompositionDrawListVisual server, Visual visual) : base(compositor, server) { Visual = visual; } diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs index aca8ef7c46..1d416f5a8a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Collections.Pooled; +using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; @@ -7,6 +8,8 @@ namespace Avalonia.Rendering.Composition.Drawing; internal class CompositionDrawList : PooledList> { + public Size? Size { get; set; } + public CompositionDrawList() { @@ -26,11 +29,22 @@ internal class CompositionDrawList : PooledList> public CompositionDrawList Clone() { - var clone = new CompositionDrawList(Count); + var clone = new CompositionDrawList(Count) { Size = Size }; foreach (var r in this) clone.Add(r.Clone()); return clone; } + + public void Render(CompositorDrawingContextProxy canvas) + { + foreach (var cmd in this) + { + canvas.VisualBrushDrawList = (cmd.Item as BrushDrawOperation)?.Aux as CompositionDrawList; + cmd.Item.Render(canvas); + } + + canvas.VisualBrushDrawList = null; + } } internal class CompositionDrawListBuilder diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index c8e5d9e064..96c0e22d56 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -359,25 +359,30 @@ internal class CompositionDrawingContext : IDrawingContextImpl ? _builder.DrawOperations[_drawOperationIndex] as IRef : null; } - - private IDictionary? CreateChildScene(IBrush? brush) + + private IDisposable? CreateChildScene(IBrush? brush) { - /* - var visualBrush = brush as VisualBrush; - - if (visualBrush != null) + if (brush is VisualBrush visualBrush) { var visual = visualBrush.Visual; if (visual != null) { + // TODO: This is a temporary solution to make visual brush to work like it does with DeferredRenderer + // We should directly reference the corresponding CompositionVisual (which should + // be attached to the same composition target) like UWP does. + // Render-able visuals shouldn't be dangling unattached (visual as IVisualBrushInitialize)?.EnsureInitialized(); - var scene = new Scene(visual); - _sceneBuilder.UpdateAll(scene); - return new Dictionary { { visualBrush.Visual, scene } }; - } - }*/ + var drawList = new CompositionDrawList() { Size = visual.Bounds.Size }; + var recorder = new CompositionDrawingContext(); + recorder.BeginUpdate(drawList); + ImmediateRenderer.Render(visual, new DrawingContext(recorder)); + recorder.EndUpdate(); + + return drawList; + } + } return null; } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 7d061e86a9..8b6ac5b0c2 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -2,6 +2,7 @@ using System.Numerics; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; @@ -10,12 +11,21 @@ namespace Avalonia.Rendering.Composition.Server; internal class CompositorDrawingContextProxy : IDrawingContextImpl { private IDrawingContextImpl _impl; + private readonly VisualBrushRenderer _visualBrushRenderer; - public CompositorDrawingContextProxy(IDrawingContextImpl impl) + public CompositorDrawingContextProxy(IDrawingContextImpl impl, VisualBrushRenderer visualBrushRenderer) { _impl = impl; + _visualBrushRenderer = visualBrushRenderer; } + // This is a hack to make it work with the current way of handling visual brushes + public CompositionDrawList? VisualBrushDrawList + { + get => _visualBrushRenderer.VisualBrushDrawList; + set => _visualBrushRenderer.VisualBrushDrawList = value; + } + public Matrix PreTransform { get; set; } = Matrix.Identity; public void Dispose() @@ -135,4 +145,22 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl { _impl.Custom(custom); } + + public class VisualBrushRenderer : IVisualBrushRenderer + { + public CompositionDrawList? VisualBrushDrawList { get; set; } + public Size GetRenderTargetSize(IVisualBrush brush) + { + return VisualBrushDrawList?.Size ?? Size.Empty; + } + + public void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) + { + if (VisualBrushDrawList != null) + { + foreach (var cmd in VisualBrushDrawList) + cmd.Item.Render(context); + } + } + } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index f0384c36fc..ba18211459 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -51,10 +51,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua { if (_renderCommands != null) { - foreach (var cmd in _renderCommands) - { - cmd.Item.Render(canvas); - } + _renderCommands.Render(canvas); } base.RenderCore(canvas, transform); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index dab65fc8ed..d8a5de4f54 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -75,11 +75,12 @@ namespace Avalonia.Rendering.Composition.Server if (!_dirtyRect.IsEmpty) { - using (var context = _layer.CreateDrawingContext(null)) + var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer(); + using (var context = _layer.CreateDrawingContext(visualBrushHelper)) { context.PushClip(_dirtyRect); context.Clear(Colors.Transparent); - Root.Render(new CompositorDrawingContextProxy(context), Root.CombinedTransformMatrix); + Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), Root.CombinedTransformMatrix); context.PopClip(); } } diff --git a/src/Avalonia.Base/Rendering/DeferredRenderer.cs b/src/Avalonia.Base/Rendering/DeferredRenderer.cs index 82be0a1a0f..4236763e3b 100644 --- a/src/Avalonia.Base/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Base/Rendering/DeferredRenderer.cs @@ -272,16 +272,18 @@ namespace Avalonia.Rendering } } + Scene? TryGetChildScene(IRef? op) => (op?.Item as BrushDrawOperation)?.Aux as Scene; + /// Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) { - return (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty; + return TryGetChildScene(_currentDraw)?.Size ?? Size.Empty; } /// void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) { - var childScene = (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]; + var childScene = TryGetChildScene(_currentDraw); if (childScene != null) { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs index cd3dac699a..e81966ce81 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.VisualTree; @@ -9,14 +10,21 @@ namespace Avalonia.Rendering.SceneGraph /// internal abstract class BrushDrawOperation : DrawOperation { - public BrushDrawOperation(Rect bounds, Matrix transform) + public BrushDrawOperation(Rect bounds, Matrix transform, IDisposable? aux) : base(bounds, transform) { + Aux = aux; } /// - /// Gets a collection of child scenes that are needed to draw visual brushes. + /// Auxiliary data required to draw the brush /// - public abstract IDictionary? ChildScenes { get; } + public IDisposable? Aux { get; } + + public override void Dispose() + { + Aux?.Dispose(); + base.Dispose(); + } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 3f495c619c..07082e4ac3 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -457,7 +457,7 @@ namespace Avalonia.Rendering.SceneGraph return _drawOperationindex < _node!.DrawOperations.Count ? _node.DrawOperations[_drawOperationindex] as IRef : null; } - private IDictionary? CreateChildScene(IBrush? brush) + private IDisposable? CreateChildScene(IBrush? brush) { var visualBrush = brush as VisualBrush; @@ -470,7 +470,7 @@ namespace Avalonia.Rendering.SceneGraph (visual as IVisualBrushInitialize)?.EnsureInitialized(); var scene = new Scene(visual); _sceneBuilder.UpdateAll(scene); - return new Dictionary { { visualBrush.Visual, scene } }; + return scene; } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs index c1fc6a81f6..4600653b9d 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs @@ -17,14 +17,13 @@ namespace Avalonia.Rendering.SceneGraph IBrush? brush, IPen? pen, Rect rect, - IDictionary? childScenes = null) - : base(rect.Inflate(pen?.Thickness ?? 0), transform) + IDisposable? aux = null) + : base(rect.Inflate(pen?.Thickness ?? 0), transform, aux) { Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; - ChildScenes = childScenes; } /// @@ -47,8 +46,6 @@ namespace Avalonia.Rendering.SceneGraph /// public Rect Rect { get; } - public override IDictionary? ChildScenes { get; } - public bool Equals(Matrix transform, IBrush? brush, IPen? pen, Rect rect) { return transform == Transform && diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs index 70748989d6..4b43f93aee 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; @@ -23,14 +24,13 @@ namespace Avalonia.Rendering.SceneGraph IBrush? brush, IPen? pen, IGeometryImpl geometry, - IDictionary? childScenes = null) - : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform) + IDisposable? aux) + : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, aux) { Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Geometry = geometry; - ChildScenes = childScenes; } /// @@ -53,9 +53,6 @@ namespace Avalonia.Rendering.SceneGraph /// public IGeometryImpl Geometry { get; } - /// - public override IDictionary? ChildScenes { get; } - /// /// Determines if this draw operation equals another. /// diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index d6da087120..9199611ed6 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; @@ -23,13 +24,12 @@ namespace Avalonia.Rendering.SceneGraph Matrix transform, IBrush foreground, GlyphRun glyphRun, - IDictionary? childScenes = null) - : base(new Rect(glyphRun.Size), transform) + IDisposable? aux = null) + : base(new Rect(glyphRun.Size), transform, aux) { Transform = transform; Foreground = foreground.ToImmutable(); GlyphRun = glyphRun; - ChildScenes = childScenes; } /// @@ -47,9 +47,6 @@ namespace Avalonia.Rendering.SceneGraph /// public GlyphRun GlyphRun { get; } - /// - public override IDictionary? ChildScenes { get; } - /// public override void Render(IDrawingContextImpl context) { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs index a9e1ce8ed7..ee5ec0a5fc 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs @@ -25,14 +25,13 @@ namespace Avalonia.Rendering.SceneGraph IPen pen, Point p1, Point p2, - IDictionary? childScenes = null) - : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform) + IDisposable? aux = null) + : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform, aux) { Transform = transform; Pen = pen.ToImmutable(); P1 = p1; P2 = p2; - ChildScenes = childScenes; } /// @@ -55,9 +54,6 @@ namespace Avalonia.Rendering.SceneGraph /// public Point P2 { get; } - /// - public override IDictionary? ChildScenes { get; } - /// /// Determines if this draw operation equals another. /// diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index 4b6e7d2254..549c1fd7de 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Platform; using Avalonia.VisualTree; @@ -17,12 +18,11 @@ namespace Avalonia.Rendering.SceneGraph /// The opacity mask to push. /// The bounds of the mask. /// Child scenes for drawing visual brushes. - public OpacityMaskNode(IBrush mask, Rect bounds, IDictionary? childScenes = null) - : base(Rect.Empty, Matrix.Identity) + public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null) + : base(Rect.Empty, Matrix.Identity, aux) { Mask = mask.ToImmutable(); MaskBounds = bounds; - ChildScenes = childScenes; } /// @@ -30,7 +30,7 @@ namespace Avalonia.Rendering.SceneGraph /// opacity mask pop. /// public OpacityMaskNode() - : base(Rect.Empty, Matrix.Identity) + : base(Rect.Empty, Matrix.Identity, null) { } @@ -44,8 +44,6 @@ namespace Avalonia.Rendering.SceneGraph /// public Rect? MaskBounds { get; } - /// - public override IDictionary? ChildScenes { get; } /// public override bool HitTest(Point p) => false; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs index 3279c3a549..7b79c446f9 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; @@ -26,14 +27,13 @@ namespace Avalonia.Rendering.SceneGraph IPen? pen, RoundedRect rect, BoxShadows boxShadows, - IDictionary? childScenes = null) - : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform) + IDisposable? aux = null) + : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, aux) { Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; - ChildScenes = childScenes; BoxShadows = boxShadows; } @@ -62,9 +62,6 @@ namespace Avalonia.Rendering.SceneGraph /// public BoxShadows BoxShadows { get; } - /// - public override IDictionary? ChildScenes { get; } - /// /// Determines if this draw operation equals another. /// From 7e4fa1d84b7a8eac22fa72fb2d0c9c612e743bf3 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 25 May 2022 21:46:52 +0300 Subject: [PATCH 009/171] Fixed hit-testing for the first frame --- .../Composition/Server/ReadbackIndices.cs | 25 ++++++++----------- .../Server/ServerCompositionTarget.cs | 10 +++++--- .../Composition/Server/ServerVisual.cs | 3 +-- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs index 372fa4d9ce..1971451811 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs @@ -4,29 +4,24 @@ namespace Avalonia.Rendering.Composition.Server { private readonly object _lock = new object(); public int ReadIndex { get; private set; } = 0; - public int WriteIndex { get; private set; } = -1; + public int WriteIndex { get; private set; } = 1; + public int WrittenIndex { get; private set; } = 0; public ulong ReadRevision { get; private set; } - public ulong WriteRevision { get; private set; } - private ulong[] _revisions = new ulong[3]; - - + public ulong LastWrittenRevision { get; private set; } + public void NextRead() { lock (_lock) { - for (var c = 0; c < 3; c++) + if (ReadRevision < LastWrittenRevision) { - if (c != WriteIndex && c != ReadIndex && _revisions[c] > ReadRevision) - { - ReadIndex = c; - ReadRevision = _revisions[c]; - return; - } + ReadIndex = WrittenIndex; + ReadRevision = LastWrittenRevision; } } } - public void NextWrite(ulong revision) + public void CompleteWrite(ulong writtenRevision) { lock (_lock) { @@ -34,9 +29,9 @@ namespace Avalonia.Rendering.Composition.Server { if (c != WriteIndex && c != ReadIndex) { + WrittenIndex = WriteIndex; + LastWrittenRevision = writtenRevision; WriteIndex = c; - WriteRevision = revision; - _revisions[c] = revision; return; } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index d8a5de4f54..a50562eabc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -15,7 +15,7 @@ namespace Avalonia.Rendering.Composition.Server private readonly Func _renderTargetFactory; private static long s_nextId = 1; public long Id { get; } - private ulong _frame = 1; + public ulong Revision { get; private set; } private IRenderTarget? _renderTarget; private FpsCounter _fpsCounter = new FpsCounter(Typeface.Default.GlyphTypeface); private Rect _dirtyRect; @@ -58,9 +58,14 @@ namespace Avalonia.Rendering.Composition.Server if(_dirtyRect.IsEmpty && !_redrawRequested) return; + + Revision++; + // Update happens in a separate phase to extend dirty rect if needed Root.Update(this, Matrix4x4.Identity); + Readback.CompleteWrite(Revision); + _redrawRequested = false; using (var targetContext = _renderTarget.CreateDrawingContext(null)) { @@ -102,9 +107,6 @@ namespace Avalonia.Rendering.Composition.Server _dirtyRect = Rect.Empty; } - - Readback.NextWrite(_frame); - _frame++; } private static Rect SnapToDevicePixels(Rect rect, double scale) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 05b63a7a73..4e320c34be 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -63,7 +63,7 @@ namespace Avalonia.Rendering.Composition.Server Scale, RotationAngle, Orientation, Offset); var i = Root!.Readback; ref var readback = ref GetReadback(i.WriteIndex); - readback.Revision = i.WriteRevision; + readback.Revision = root.Revision; readback.Matrix = res; readback.TargetId = Root.Id; //TODO: check effective opacity too @@ -85,7 +85,6 @@ namespace Avalonia.Rendering.Composition.Server public struct ReadbackData { public Matrix4x4 Matrix; - public bool Visible; public ulong Revision; public long TargetId; } From 7a9d9ea304e3e96f0b857afb9b3c14bb864fd6b9 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 25 May 2022 22:17:35 +0300 Subject: [PATCH 010/171] Invalidate visual's rect if it's moved in the global coordinate space --- .../Rendering/Composition/Server/ServerVisual.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 4e320c34be..9bfc909fe4 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -69,7 +69,14 @@ namespace Avalonia.Rendering.Composition.Server //TODO: check effective opacity too IsVisibleInFrame = Visible && Opacity > 0; CombinedTransformMatrix = res; - GlobalTransformMatrix = res * transform; + var newTransform = res * transform; + if (GlobalTransformMatrix != newTransform) + { + // Visual was moved alongside with its parent + _isDirty = true; + Root.AddDirtyRect(TransformedBounds); + } + GlobalTransformMatrix = newTransform; //TODO: Cache TransformedBounds = ContentBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); From f0989357a01a1df28ad5b522aad180e08d5e539f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 15:29:56 +0300 Subject: [PATCH 011/171] Visiblity check fix --- src/Avalonia.Base/Rendering/Composition/Visual.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index f7c8078073..fa8d5d8f3b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -45,8 +45,8 @@ namespace Avalonia.Rendering.Composition var i = Root.Server.Readback; ref var readback = ref Server.GetReadback(i.ReadIndex); - // CompositionVisual wasn't visible - if (readback.Revision < i.ReadRevision) + // CompositionVisual wasn't visible or wasn't even attached to the composition target during the lat frame + if (!readback.Visible || readback.Revision < i.ReadRevision) return null; // CompositionVisual was reparented (potential race here) From f974859323c06b493226dc8d85de66349a5ac4a9 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 15:30:12 +0300 Subject: [PATCH 012/171] Change stream WIP --- .../Composition/Server/ServerVisual.cs | 51 ++++-- .../Composition/Transport/BatchStream.cs | 173 ++++++++++++++++++ .../Transport/BatchStreamArrayPool.cs | 144 +++++++++++++++ 3 files changed, 354 insertions(+), 14 deletions(-) create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 9bfc909fe4..801bfb2f65 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -8,6 +8,7 @@ namespace Avalonia.Rendering.Composition.Server { private bool _isDirty; private ServerCompositionTarget? _root; + private bool _isBackface; protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { @@ -59,23 +60,35 @@ namespace Avalonia.Rendering.Composition.Server public virtual void Update(ServerCompositionTarget root, Matrix4x4 transform) { - var res = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, + // Calculate new parent-relative transform + CombinedTransformMatrix = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, Scale, RotationAngle, Orientation, Offset); - var i = Root!.Readback; - ref var readback = ref GetReadback(i.WriteIndex); - readback.Revision = root.Revision; - readback.Matrix = res; - readback.TargetId = Root.Id; - //TODO: check effective opacity too - IsVisibleInFrame = Visible && Opacity > 0; - CombinedTransformMatrix = res; - var newTransform = res * transform; + + var newTransform = CombinedTransformMatrix * transform; + + // Check if visual was moved and recalculate face orientation + var positionChanged = false; if (GlobalTransformMatrix != newTransform) { - // Visual was moved alongside with its parent - _isDirty = true; - Root.AddDirtyRect(TransformedBounds); + _isBackface = Vector3.Transform( + new Vector3(0, 0, float.PositiveInfinity), GlobalTransformMatrix).Z <= 0; + positionChanged = true; + } + + var wasVisible = IsVisibleInFrame; + //TODO: check effective opacity too + IsVisibleInFrame = Visible && Opacity > 0 && !_isBackface; + + // Invalidate previous rect and queue new rect based on visibility + if (positionChanged) + { + if(wasVisible) + Root!.AddDirtyRect(TransformedBounds); + + if (IsVisibleInFrame) + _isDirty = true; } + GlobalTransformMatrix = newTransform; //TODO: Cache TransformedBounds = ContentBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); @@ -84,9 +97,18 @@ namespace Avalonia.Rendering.Composition.Server _isDirty = false; else if (_isDirty) { - Root.AddDirtyRect(TransformedBounds); + Root!.AddDirtyRect(TransformedBounds); _isDirty = false; } + + // Update readback indices + var i = Root!.Readback; + ref var readback = ref GetReadback(i.WriteIndex); + readback.Revision = root.Revision; + readback.Matrix = CombinedTransformMatrix; + readback.TargetId = Root.Id; + readback.Visible = IsVisibleInFrame; + } public struct ReadbackData @@ -94,6 +116,7 @@ namespace Avalonia.Rendering.Composition.Server public Matrix4x4 Matrix; public ulong Revision; public long TargetId; + public bool Visible; } partial void ApplyChangesExtra(CompositionVisualChanges c) diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs new file mode 100644 index 0000000000..feb892d134 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport; + +internal class BatchStreamData +{ + public Queue> Objects { get; } = new(); + public Queue> Structs { get; } = new(); +} + +public struct BatchStreamSegment +{ + public TData Data { get; set; } + public int ElementCount { get; set; } +} + +internal class BatchStreamWriter : IDisposable +{ + private readonly BatchStreamData _output; + private readonly BatchStreamMemoryPool _memoryPool; + private readonly BatchStreamObjectPool _objectPool; + + private BatchStreamSegment _currentObjectSegment; + private BatchStreamSegment _currentDataSegment; + + public BatchStreamWriter(BatchStreamData output, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) + { + _output = output; + _memoryPool = memoryPool; + _objectPool = objectPool; + } + + void CommitDataSegment() + { + if (_currentDataSegment.Data != IntPtr.Zero) + _output.Structs.Enqueue(_currentDataSegment); + _currentDataSegment = new (); + } + + void NextDataSegment() + { + CommitDataSegment(); + _currentDataSegment.Data = _memoryPool.Get(); + } + + void CommitObjectSegment() + { + if (_currentObjectSegment.Data != null) + _output.Objects.Enqueue(_currentObjectSegment!); + _currentObjectSegment = new(); + } + + void NextObjectSegment() + { + CommitObjectSegment(); + _currentObjectSegment.Data = _objectPool.Get(); + } + + public unsafe void Write(T item) where T : unmanaged + { + var size = Unsafe.SizeOf(); + if (_currentDataSegment.Data == IntPtr.Zero || _currentDataSegment.ElementCount + size > _memoryPool.BufferSize) + NextDataSegment(); + *(T*)((byte*)_currentDataSegment.Data + _currentDataSegment.ElementCount) = item; + _currentDataSegment.ElementCount += size; + } + + public void Write(ServerObject item) + { + if (_currentObjectSegment.Data == null || + _currentObjectSegment.ElementCount >= _currentObjectSegment.Data.Length) + NextObjectSegment(); + _currentObjectSegment.Data![_currentObjectSegment.ElementCount] = item; + _currentObjectSegment.ElementCount++; + } + + public void Dispose() + { + CommitDataSegment(); + CommitObjectSegment(); + } +} + +internal class BatchStreamReader : IDisposable +{ + private readonly BatchStreamData _input; + private readonly BatchStreamMemoryPool _memoryPool; + private readonly BatchStreamObjectPool _objectPool; + + private BatchStreamSegment _currentObjectSegment; + private BatchStreamSegment _currentDataSegment; + private int _memoryOffset, _objectOffset; + + public BatchStreamReader(BatchStreamData _input, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) + { + this._input = _input; + _memoryPool = memoryPool; + _objectPool = objectPool; + } + + public unsafe T Read() where T : unmanaged + { + var size = Unsafe.SizeOf(); + if (_currentDataSegment.Data == IntPtr.Zero) + { + if (_input.Structs.Count == 0) + throw new EndOfStreamException(); + _currentDataSegment = _input.Structs.Dequeue(); + _memoryOffset = 0; + } + + if (_memoryOffset + size > _currentDataSegment.ElementCount) + throw new InvalidOperationException("Attempted to read more memory then left in the current segment"); + + var rv = *(T*)((byte*)_currentDataSegment.Data + size); + _memoryOffset += size; + if (_memoryOffset == _currentDataSegment.ElementCount) + { + _memoryPool.Return(_currentDataSegment.Data); + _currentDataSegment = new(); + } + + return rv; + } + + public ServerObject ReadObject() + { + if (_currentObjectSegment.Data == null) + { + if (_input.Objects.Count == 0) + throw new EndOfStreamException(); + _currentObjectSegment = _input.Objects.Dequeue()!; + _objectOffset = 0; + } + + var rv = _currentObjectSegment.Data![_objectOffset]; + _objectOffset++; + if (_objectOffset == _currentObjectSegment.ElementCount) + { + _objectPool.Return(_currentObjectSegment.Data); + _currentObjectSegment = new(); + } + + return rv; + } + + public bool IsStructEof => _currentDataSegment.Data == IntPtr.Zero && _input.Structs.Count == 0; + + public void Dispose() + { + if (_currentDataSegment.Data != IntPtr.Zero) + { + _memoryPool.Return(_currentDataSegment.Data); + _currentDataSegment = new(); + } + + while (_input.Structs.Count > 0) + _memoryPool.Return(_input.Structs.Dequeue().Data); + + if (_currentObjectSegment.Data != null) + { + _objectPool.Return(_currentObjectSegment.Data); + _currentObjectSegment = new(); + } + + while (_input.Objects.Count > 0) + _objectPool.Return(_input.Objects.Dequeue().Data); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs new file mode 100644 index 0000000000..913958765a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using Avalonia.Threading; + +namespace Avalonia.Rendering.Composition.Transport; + +/// +/// A pool that keeps a number of elements that was used in the last 10 seconds +/// +internal abstract class BatchStreamPoolBase : IDisposable +{ + readonly Stack _pool = new(); + bool _disposed; + int _usage; + readonly int[] _usageStatistics = new int[10]; + int _usageStatisticsSlot; + + public BatchStreamPoolBase(bool needsFinalize = false) + { + if(!needsFinalize) + GC.SuppressFinalize(needsFinalize); + + var updateRef = new WeakReference>(this); + StartUpdateTimer(updateRef); + } + + static void StartUpdateTimer(WeakReference> updateRef) + { + DispatcherTimer.Run(() => + { + if (updateRef.TryGetTarget(out var target)) + { + target.UpdateStatistics(); + return true; + } + return false; + + }, TimeSpan.FromSeconds(1)); + } + + private void UpdateStatistics() + { + lock (_pool) + { + var maximumUsage = _usageStatistics.Max(); + var recentlyUsedPooledSlots = maximumUsage - _usage; + while (recentlyUsedPooledSlots < _pool.Count) + DestroyItem(_pool.Pop()); + + _usageStatistics[_usage] = 0; + _usageStatisticsSlot = (_usageStatisticsSlot + 1) % _usageStatistics.Length; + } + } + + protected abstract T CreateItem(); + + protected virtual void DestroyItem(T item) + { + + } + + public T Get() + { + lock (_pool) + { + _usage++; + if (_usageStatistics[_usageStatisticsSlot] < _usage) + _usageStatistics[_usageStatisticsSlot] = _usage; + + if (_pool.Count != 0) + return _pool.Pop(); + } + + return CreateItem(); + } + + public void Return(T item) + { + lock (_pool) + { + _usage--; + if (!_disposed) + { + _pool.Push(item); + return; + } + } + + DestroyItem(item); + } + + public void Dispose() + { + lock (_pool) + { + _disposed = true; + foreach (var item in _pool) + DestroyItem(item); + _pool.Clear(); + } + } + + ~BatchStreamPoolBase() + { + Dispose(); + } +} + +internal sealed class BatchStreamObjectPool : BatchStreamPoolBase where T : class +{ + private readonly int _arraySize; + + public BatchStreamObjectPool(int arraySize = 1024) + { + _arraySize = arraySize; + } + + protected override T[] CreateItem() + { + return new T[_arraySize]; + } + + protected override void DestroyItem(T[] item) + { + Array.Clear(item, 0, item.Length); + } +} + +internal sealed class BatchStreamMemoryPool : BatchStreamPoolBase +{ + public int BufferSize { get; } + + public BatchStreamMemoryPool(int bufferSize = 16384) + { + BufferSize = bufferSize; + } + + protected override IntPtr CreateItem() => Marshal.AllocHGlobal(BufferSize); + + protected override void DestroyItem(IntPtr item) => Marshal.FreeHGlobal(item); +} \ No newline at end of file From 4991d4f370acd7503260258722bcad328e619d65 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 21:35:50 +0300 Subject: [PATCH 013/171] Switched to byte-stream based transport --- .../Animations/AnimatedValueStore.cs | 5 +- .../Animations/CompositionAnimation.cs | 5 +- .../Animations/CompositionAnimationGroup.cs | 2 - .../Animations/ExpressionAnimation.cs | 2 +- .../Animations/ImplicitAnimationCollection.cs | 2 - .../Composition/CompositionDrawListVisual.cs | 16 +- .../Composition/CompositionEasingFunction.cs | 4 +- .../Composition/CompositionObject.cs | 57 ++- .../Composition/CompositionPropertySet.cs | 12 +- .../Composition/CompositionTarget.cs | 2 +- .../Rendering/Composition/Compositor.cs | 50 ++- .../Rendering/Composition/ContainerVisual.cs | 13 +- .../Rendering/Composition/CustomDrawVisual.cs | 56 --- .../Server/ServerCompositionDrawListVisual.cs | 10 +- .../Server/ServerCompositionTarget.cs | 4 +- .../Composition/Server/ServerCompositor.cs | 23 +- .../Server/ServerContainerVisual.cs | 9 +- .../Server/ServerCustomDrawVisual.cs | 31 -- .../Composition/Server/ServerList.cs | 24 +- .../Composition/Server/ServerObject.cs | 28 +- .../Composition/Server/ServerVisual.cs | 31 +- .../Rendering/Composition/Transport/Batch.cs | 7 +- .../Composition/Transport/BatchStream.cs | 27 +- .../Transport/BatchStreamArrayPool.cs | 24 +- .../Transport/BatchStreamDebugMarker.cs | 9 + .../Rendering/Composition/Transport/Change.cs | 82 ---- .../Composition/Transport/ChangeSet.cs | 36 -- .../Composition/Transport/ChangeSetPool.cs | 42 -- .../Transport/CompositionTargetChanges.cs | 11 - .../Transport/CustomDrawVisualChanges.cs | 20 - .../Transport/DrawListVisualChanges.cs | 48 -- .../Composition/Transport/ListChange.cs | 19 - .../Composition/Transport/ListChangeSet.cs | 25 -- .../Transport/ServerListProxyHelper.cs | 55 +-- .../Composition/Transport/VisualChanges.cs | 16 - .../Rendering/Composition/Visual.cs | 35 +- .../Rendering/Composition/VisualCollection.cs | 1 + src/Avalonia.Base/composition-schema.xml | 7 +- .../CompositionGenerator/Config.cs | 6 + .../CompositionGenerator/Extensions.cs | 9 + .../Generator.KeyFrameAnimation.cs | 2 +- .../Generator.ListProxy.cs | 12 +- .../CompositionGenerator/Generator.cs | 413 ++++++++++-------- src/Avalonia.X11/X11Platform.cs | 2 +- src/Windows/Avalonia.Win32/Win32Platform.cs | 2 +- .../Composition/BatchStreamTests.cs | 45 ++ 46 files changed, 532 insertions(+), 809 deletions(-) delete mode 100644 src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/Change.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs create mode 100644 tests/Avalonia.Base.UnitTests/Composition/BatchStreamTests.cs diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs index e877b50b20..95bc384743 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs @@ -1,3 +1,4 @@ +using System; using System.Runtime.InteropServices; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Server; @@ -81,7 +82,7 @@ namespace Avalonia.Rendering.Composition.Animations public bool IsAnimation => _animation != null; - public void SetAnimation(ServerObject target, ChangeSet cs, IAnimationInstance animation, int storeOffset) + public void SetAnimation(ServerObject target, TimeSpan commitedAt, IAnimationInstance animation, int storeOffset) { _direct = default; if (_animation != null) @@ -91,7 +92,7 @@ namespace Avalonia.Rendering.Composition.Animations } _animation = animation; - _animation.Initialize(cs.Batch.CommitedAt, ExpressionVariant.Create(LastAnimated), storeOffset); + _animation.Initialize(commitedAt, ExpressionVariant.Create(LastAnimated), storeOffset); if (target.IsActive) _animation.Activate(); diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs index fe20115b38..cf81c6e656 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -18,8 +18,6 @@ namespace Avalonia.Rendering.Composition.Animations _propertySet = new CompositionPropertySet(compositor); } - private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); - public void ClearAllParameters() => _propertySet.ClearAll(); public void ClearParameter(string key) => _propertySet.Clear(key); @@ -52,8 +50,7 @@ namespace Avalonia.Rendering.Composition.Animations internal abstract IAnimationInstance CreateInstance(ServerObject targetObject, ExpressionVariant? finalValue); - internal PropertySetSnapshot CreateSnapshot(bool server) - => _propertySet.Snapshot(server); + internal PropertySetSnapshot CreateSnapshot() => _propertySet.Snapshot(); void ICompositionAnimationBase.InternalOnly() { diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs index 833f7e498c..89f8ba411d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs @@ -20,7 +20,5 @@ namespace Avalonia.Rendering.Composition.Animations public CompositionAnimationGroup(Compositor compositor) : base(compositor, null!) { } - - private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs index a6f24c2e35..6a2c07e6ef 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs @@ -29,6 +29,6 @@ namespace Avalonia.Rendering.Composition.Animations internal override IAnimationInstance CreateInstance( ServerObject targetObject, ExpressionVariant? finalValue) => new ExpressionAnimationInstance(ParsedExpression, - targetObject, finalValue, CreateSnapshot(true)); + targetObject, finalValue, CreateSnapshot()); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs index be91352527..fa5b69dae9 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs @@ -15,8 +15,6 @@ namespace Avalonia.Rendering.Composition.Animations _innerface = _inner; } - private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); - public IEnumerator> GetEnumerator() => _inner.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable) _inner).GetEnumerator(); diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index e4ed0abd29..069d888fbb 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -9,7 +9,7 @@ internal class CompositionDrawListVisual : CompositionContainerVisual { public Visual Visual { get; } - private new DrawListVisualChanges Changes => (DrawListVisualChanges)base.Changes; + private bool _drawListChanged; private CompositionDrawList? _drawList; public CompositionDrawList? DrawList { @@ -18,11 +18,21 @@ internal class CompositionDrawListVisual : CompositionContainerVisual { _drawList?.Dispose(); _drawList = value; - Changes.DrawCommands = value?.Clone(); + _drawListChanged = true; + RegisterForSerialization(); } } - private protected override IChangeSetPool ChangeSetPool => DrawListVisualChanges.Pool; + private protected override void SerializeChangesCore(BatchStreamWriter writer) + { + writer.Write((byte)(_drawListChanged ? 1 : 0)); + if (_drawListChanged) + { + writer.WriteObject(DrawList?.Clone()); + _drawListChanged = false; + } + base.SerializeChangesCore(writer); + } internal CompositionDrawListVisual(Compositor compositor, ServerCompositionDrawListVisual server, Visual visual) : base(compositor, server) { diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs b/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs index 73db243e93..90b2bec268 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs @@ -10,9 +10,7 @@ namespace Avalonia.Rendering.Composition internal CompositionEasingFunction(Compositor compositor) : base(compositor, null!) { } - - private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); - + internal abstract IEasingFunction Snapshot(); } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs index 2417ecaba8..d561338a36 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs @@ -6,7 +6,7 @@ using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Rendering.Composition { - public abstract class CompositionObject : IDisposable, IExpressionObject + public abstract class CompositionObject : IDisposable { public ImplicitAnimationCollection? ImplicitAnimations { get; set; } internal CompositionObject(Compositor compositor, ServerObject server) @@ -18,46 +18,17 @@ namespace Avalonia.Rendering.Composition public Compositor Compositor { get; } internal ServerObject Server { get; } public bool IsDisposed { get; private set; } - private ChangeSet? _changes; + private bool _registeredForSerialization; private static void ThrowInvalidOperation() => throw new InvalidOperationException("There is no server-side counterpart for this object"); - - private protected ChangeSet Changes - { - get - { - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - if (Server == null) ThrowInvalidOperation(); - var currentBatch = Compositor.CurrentBatch; - if (_changes != null && _changes.Batch != currentBatch) - _changes = null; - if (_changes == null) - { - _changes = ChangeSetPool.Get(Server!, currentBatch); - currentBatch.Changes!.Add(_changes); - Compositor.QueueImplicitBatchCommit(); - } - - return _changes; - } - } - - private protected abstract IChangeSetPool ChangeSetPool { get; } public void Dispose() { - Changes.Dispose = true; + //Changes.Dispose = true; IsDisposed = true; } - internal virtual ExpressionVariant GetPropertyForAnimation(string name) - { - return default; - } - - ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name); - public void StartAnimation(string propertyName, CompositionAnimation animation) => StartAnimation(propertyName, animation, null); @@ -121,5 +92,27 @@ namespace Avalonia.Rendering.Composition throw new ArgumentException(); } + + protected void RegisterForSerialization() + { + if (Server == null) + throw new InvalidOperationException("The object doesn't have an associated server counterpart"); + + if(_registeredForSerialization) + return; + _registeredForSerialization = true; + Compositor.RegisterForSerialization(this); + } + + internal void SerializeChanges(BatchStreamWriter writer) + { + _registeredForSerialization = false; + SerializeChangesCore(writer); + } + + private protected virtual void SerializeChangesCore(BatchStreamWriter writer) + { + + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs index bc0ce804dc..584969cbc0 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs @@ -16,8 +16,6 @@ namespace Avalonia.Rendering.Composition { } - private protected override IChangeSetPool ChangeSetPool => throw new NotSupportedException(); - internal void Set(string key, ExpressionVariant value) { _objects.Remove(key); @@ -104,10 +102,10 @@ namespace Avalonia.Rendering.Composition _variants.Remove(key); } - internal PropertySetSnapshot Snapshot(bool server) => - SnapshotCore(server, 1); + internal PropertySetSnapshot Snapshot() => + SnapshotCore(1); - private PropertySetSnapshot SnapshotCore(bool server, int allowedNestingLevel) + private PropertySetSnapshot SnapshotCore(int allowedNestingLevel) { var dic = new Dictionary(_objects.Count + _variants.Count); foreach (var o in _objects) @@ -116,12 +114,12 @@ namespace Avalonia.Rendering.Composition { if (allowedNestingLevel <= 0) throw new InvalidOperationException("PropertySet depth limit reached"); - dic[o.Key] = new PropertySetSnapshot.Value(ps.SnapshotCore(server, allowedNestingLevel - 1)); + dic[o.Key] = new PropertySetSnapshot.Value(ps.SnapshotCore(allowedNestingLevel - 1)); } else if (o.Value.Server == null) throw new InvalidOperationException($"Object of type {o.Value.GetType()} is not allowed"); else - dic[o.Key] = new PropertySetSnapshot.Value(server ? (IExpressionObject) o.Value.Server : o.Value); + dic[o.Key] = new PropertySetSnapshot.Value(o.Value.Server); } foreach (var v in _variants) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index 8d052389c2..c5cfaeacce 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -102,6 +102,6 @@ namespace Avalonia.Rendering.Composition return false; } - public void RequestRedraw() => Changes.RedrawRequested.Value = true; + public void RequestRedraw() => RegisterForSerialization(); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 14d779dbc4..96564f0800 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -16,18 +16,17 @@ namespace Avalonia.Rendering.Composition public partial class Compositor { private ServerCompositor _server; - private Batch _currentBatch; private bool _implicitBatchCommitQueued; private Action _implicitBatchCommit; - - internal Batch CurrentBatch => _currentBatch; + private BatchStreamObjectPool _batchObjectPool = new(); + private BatchStreamMemoryPool _batchMemoryPool = new(); + private List _objectsForSerialization = new(); internal ServerCompositor Server => _server; internal CompositionEasingFunction DefaultEasing { get; } - - private Compositor(ServerCompositor server) + + public Compositor(IRenderLoop loop) { - _server = server; - _currentBatch = new Batch(); + _server = new ServerCompositor(loop, _batchObjectPool, _batchMemoryPool); _implicitBatchCommit = ImplicitBatchCommit; DefaultEasing = new CubicBezierEasingFunction(this, new Vector2(0.25f, 0.1f), new Vector2(0.25f, 1f)); @@ -40,18 +39,27 @@ namespace Avalonia.Rendering.Composition public Task RequestCommitAsync() { - var batch = CurrentBatch; - _currentBatch = new Batch(); + var batch = new Batch(); + + using (var writer = new BatchStreamWriter(batch.Changes, _batchMemoryPool, _batchObjectPool)) + { + foreach (var obj in _objectsForSerialization) + { + writer.WriteObject(obj.Server); + obj.SerializeChanges(writer); +#if DEBUG_COMPOSITOR_SERIALIZATION + writer.Write(BatchStreamDebugMarkers.ObjectEndMagic); + writer.WriteObject(BatchStreamDebugMarkers.ObjectEndMarker); +#endif + } + _objectsForSerialization.Clear(); + } + batch.CommitedAt = Server.Clock.Elapsed; _server.EnqueueBatch(batch); return batch.Completed; } - public static Compositor Create(IRenderLoop timer) - { - return new Compositor(new ServerCompositor(timer)); - } - public void Dispose() { @@ -122,12 +130,8 @@ namespace Avalonia.Rendering.Composition public ImplicitAnimationCollection CreateImplicitAnimationCollection() => new ImplicitAnimationCollection(this); public CompositionAnimationGroup CreateAnimationGroup() => new CompositionAnimationGroup(this); - - internal CustomDrawVisual CreateCustomDrawVisual(ICustomDrawVisualRenderer renderer, - ICustomDrawVisualHitTest? hitTest = null) where T : IEquatable => - new CustomDrawVisual(this, renderer, hitTest); - - public void QueueImplicitBatchCommit() + + private void QueueImplicitBatchCommit() { if(_implicitBatchCommitQueued) return; @@ -140,5 +144,11 @@ namespace Avalonia.Rendering.Composition _implicitBatchCommitQueued = false; RequestCommitAsync(); } + + internal void RegisterForSerialization(CompositionObject compositionObject) + { + _objectsForSerialization.Add(compositionObject); + QueueImplicitBatchCommit(); + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs index f650d3e995..5b2a4be1bc 100644 --- a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs @@ -2,19 +2,20 @@ using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition { - public class CompositionContainerVisual : CompositionVisual + public partial class CompositionContainerVisual : CompositionVisual { - public CompositionVisualCollection Children { get; } - internal CompositionContainerVisual(Compositor compositor, ServerCompositionContainerVisual server) : base(compositor, server) + public CompositionVisualCollection Children { get; private set; } = null!; + + partial void InitializeDefaultsExtra() { - Children = new CompositionVisualCollection(this, server.Children); + Children = new CompositionVisualCollection(this, Server.Children); } - private protected override void OnRootChanged() + private protected override void OnRootChangedCore() { foreach (var ch in Children) ch.Root = Root; - base.OnRootChanged(); + base.OnRootChangedCore(); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs b/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs deleted file mode 100644 index 0505d6a46c..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Numerics; -using Avalonia.Platform; -using Avalonia.Rendering.Composition.Server; -using Avalonia.Rendering.Composition.Transport; - -namespace Avalonia.Rendering.Composition -{ - internal class CustomDrawVisual : CompositionContainerVisual where TData : IEquatable - { - private readonly ICustomDrawVisualHitTest? _hitTest; - - internal CustomDrawVisual(Compositor compositor, ICustomDrawVisualRenderer renderer, - ICustomDrawVisualHitTest? hitTest) : base(compositor, - new ServerCustomDrawVisual(compositor.Server, renderer)) - { - _hitTest = hitTest; - } - - private TData? _data; - - static bool Eq(TData? left, TData? right) - { - if (left == null && right == null) - return true; - if (left == null) - return false; - return left.Equals(right); - } - - public TData? Data - { - get => _data; - set - { - if (!Eq(_data, value)) - { - ((CustomDrawVisualChanges) Changes).Data.Value = value; - _data = value; - } - } - } - - private protected override IChangeSetPool ChangeSetPool => CustomDrawVisualChanges.Pool; - } - - public interface ICustomDrawVisualRenderer - { - void Render(IDrawingContextImpl canvas, TData? data); - } - - public interface ICustomDrawVisualHitTest - { - bool HitTest(TData data, Vector2 vector2); - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index ba18211459..f1b5032cd3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -1,3 +1,4 @@ +using System; using System.Numerics; using Avalonia.Collections.Pooled; using Avalonia.Platform; @@ -35,16 +36,15 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua } } - protected override void ApplyCore(ChangeSet changes) + protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) { - var ch = (DrawListVisualChanges)changes; - if (ch.DrawCommandsIsSet) + if (reader.Read() == 1) { _renderCommands?.Dispose(); - _renderCommands = ch.AcquireDrawCommands(); + _renderCommands = reader.ReadObject(); _contentBounds = null; } - base.ApplyCore(changes); + base.DeserializeChangesCore(reader, commitedAt); } protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index a50562eabc..7567eba534 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -42,8 +42,8 @@ namespace Avalonia.Rendering.Composition.Server else _compositor.RemoveCompositionTarget(this); } - - partial void ApplyChangesExtra(CompositionTargetChanges c) + + partial void DeserializeChangesExtra(BatchStreamReader c) { _redrawRequested = true; } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index 5dbe9cfb17..f7de704b23 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -17,10 +17,14 @@ namespace Avalonia.Rendering.Composition.Server private List _activeTargets = new(); private HashSet _activeAnimations = new(); private List _animationsToUpdate = new(); + private BatchStreamObjectPool _batchObjectPool; + private BatchStreamMemoryPool _batchMemoryPool; - public ServerCompositor(IRenderLoop renderLoop) + public ServerCompositor(IRenderLoop renderLoop, BatchStreamObjectPool batchObjectPool, BatchStreamMemoryPool batchMemoryPool) { _renderLoop = renderLoop; + _batchObjectPool = batchObjectPool; + _batchMemoryPool = batchMemoryPool; _renderLoop.Add(this); } @@ -45,14 +49,21 @@ namespace Avalonia.Rendering.Composition.Server batch = _batches.Dequeue(); } - foreach (var change in batch.Changes) + using (var stream = new BatchStreamReader(batch.Changes, _batchMemoryPool, _batchObjectPool)) { - if (change.Dispose) + while (!stream.IsObjectEof) { - //TODO + var target = (ServerObject)stream.ReadObject()!; + target.DeserializeChanges(stream, batch); +#if DEBUG_COMPOSITOR_SERIALIZATION + if (stream.ReadObject() != BatchStreamDebugMarkers.ObjectEndMarker) + throw new InvalidOperationException( + $"Object {target.GetType()} failed to deserialize properly on object stream"); + if(stream.Read() != BatchStreamDebugMarkers.ObjectEndMagic) + throw new InvalidOperationException( + $"Object {target.GetType()} failed to deserialize properly on data stream"); +#endif } - change.Target!.Apply(change); - change.Reset(); } _reusableToCompleteList.Add(batch); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs index 3f0995b257..a277450214 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs @@ -3,10 +3,9 @@ using Avalonia.Platform; namespace Avalonia.Rendering.Composition.Server { - internal class ServerCompositionContainerVisual : ServerCompositionVisual + internal partial class ServerCompositionContainerVisual : ServerCompositionVisual { - public ServerCompositionVisualCollection Children { get; } - + public ServerCompositionVisualCollection Children { get; private set; } = null!; protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { @@ -28,9 +27,9 @@ namespace Avalonia.Rendering.Composition.Server child.Update(root, GlobalTransformMatrix); } - public ServerCompositionContainerVisual(ServerCompositor compositor) : base(compositor) + partial void Initialize() { - Children = new ServerCompositionVisualCollection(compositor); + Children = new ServerCompositionVisualCollection(Compositor); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs deleted file mode 100644 index 5f3eb051a4..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Numerics; -using Avalonia.Platform; -using Avalonia.Rendering.Composition.Transport; - -namespace Avalonia.Rendering.Composition.Server -{ - class ServerCustomDrawVisual : ServerCompositionContainerVisual - { - private readonly ICustomDrawVisualRenderer _renderer; - private TData? _data; - public ServerCustomDrawVisual(ServerCompositor compositor, ICustomDrawVisualRenderer renderer) : base(compositor) - { - _renderer = renderer; - } - - protected override void ApplyCore(ChangeSet changes) - { - var c = (CustomDrawVisualChanges) changes; - if (c.Data.IsSet) - _data = c.Data.Value; - - base.ApplyCore(changes); - } - - protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) - { - _renderer.Render(canvas, _data); - base.RenderCore(canvas, transform); - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs index 09ef119e6b..4beea4715b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs @@ -7,25 +7,19 @@ namespace Avalonia.Rendering.Composition.Server class ServerList : ServerObject where T : ServerObject { public List List { get; } = new List(); - protected override void ApplyCore(ChangeSet changes) + + protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) { - var c = (ListChangeSet) changes; - if (c.HasListChanges) + if (reader.Read() == 1) { - foreach (var lc in c.ListChanges) - { - if(lc.Action == ListChangeAction.Clear) - List.Clear(); - if(lc.Action == ListChangeAction.RemoveAt) - List.RemoveAt(lc.Index); - if(lc.Action == ListChangeAction.InsertAt) - List.Insert(lc.Index, lc.Added!); - if (lc.Action == ListChangeAction.ReplaceAt) - List[lc.Index] = lc.Added!; - } + List.Clear(); + var count = reader.Read(); + for (var c = 0; c < count; c++) + List.Add(reader.ReadObject()); } + base.DeserializeChangesCore(reader, commitedAt); } - + public override long LastChangedBy { get diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index 5b2f58b186..16f57d9059 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -23,18 +23,6 @@ namespace Avalonia.Rendering.Composition.Server Compositor = compositor; } - protected virtual void ApplyCore(ChangeSet changes) - { - - } - - public void Apply(ChangeSet changes) - { - ApplyCore(changes); - ValuesInvalidated(); - ItselfLastChangedBy = changes.Batch!.SequenceId; - } - public virtual ExpressionVariant GetPropertyForAnimation(string name) { return default; @@ -81,6 +69,10 @@ namespace Avalonia.Rendering.Composition.Server [MethodImpl(MethodImplOptions.AggressiveInlining)] private ref ServerObjectSubscriptionStore GetStoreFromOffset(int offset) { +#if DEBUG + if (offset == 0) + throw new InvalidOperationException(); +#endif return ref Unsafe.As(ref Unsafe.AddByteOffset(ref _activationCount, new IntPtr(offset))); } @@ -112,5 +104,17 @@ namespace Avalonia.Rendering.Composition.Server } public virtual int? GetFieldOffset(string fieldName) => null; + + protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) + { + + } + + public void DeserializeChanges(BatchStreamReader reader, Batch batch) + { + DeserializeChangesCore(reader, batch.CommitedAt); + ValuesInvalidated(); + ItselfLastChangedBy = batch.SequenceId; + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 801bfb2f65..5717ab2f8c 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -7,7 +7,6 @@ namespace Avalonia.Rendering.Composition.Server unsafe partial class ServerCompositionVisual : ServerObject { private bool _isDirty; - private ServerCompositionTarget? _root; private bool _isBackface; protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { @@ -119,28 +118,24 @@ namespace Avalonia.Rendering.Composition.Server public bool Visible; } - partial void ApplyChangesExtra(CompositionVisualChanges c) + + partial void DeserializeChangesExtra(BatchStreamReader c) { - if (c.Parent.IsSet) - Parent = c.Parent.Value; - if (c.Root.IsSet) - Root = c.Root.Value; ValuesInvalidated(); } - public ServerCompositionTarget? Root + partial void OnRootChanging() { - get => _root; - private set - { - if(_root != null) - Deactivate(); - _root = value; - if (_root != null) - Activate(); - } + if(Root != null) + Deactivate(); } - + + partial void OnRootChanged() + { + if (Root != null) + Activate(); + } + protected override void ValuesInvalidated() { _isDirty = true; @@ -149,8 +144,6 @@ namespace Avalonia.Rendering.Composition.Server else Root?.Invalidate(); } - - public ServerCompositionVisual? Parent { get; private set; } public bool IsVisibleInFrame { get; set; } public Rect TransformedBounds { get; set; } public virtual Rect ContentBounds => new Rect(0, 0, Size.X, Size.Y); diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs index 7b64c01d09..0714db5781 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs @@ -9,23 +9,22 @@ namespace Avalonia.Rendering.Composition.Transport internal class Batch { private static long _nextSequenceId = 1; - private static ConcurrentBag> _pool = new ConcurrentBag>(); + private static ConcurrentBag _pool = new(); public long SequenceId { get; } public Batch() { SequenceId = Interlocked.Increment(ref _nextSequenceId); if (!_pool.TryTake(out var lst)) - lst = new List(); + lst = new BatchStreamData(); Changes = lst; } private TaskCompletionSource _tcs = new TaskCompletionSource(); - public List Changes { get; private set; } + public BatchStreamData Changes { get; private set; } public TimeSpan CommitedAt { get; set; } public void Complete() { - Changes.Clear(); _pool.Add(Changes); Changes = null!; diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs index feb892d134..9e9ed739fb 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs @@ -2,13 +2,14 @@ using System; using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; +using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Transport; internal class BatchStreamData { - public Queue> Objects { get; } = new(); + public Queue> Objects { get; } = new(); public Queue> Structs { get; } = new(); } @@ -22,12 +23,12 @@ internal class BatchStreamWriter : IDisposable { private readonly BatchStreamData _output; private readonly BatchStreamMemoryPool _memoryPool; - private readonly BatchStreamObjectPool _objectPool; + private readonly BatchStreamObjectPool _objectPool; - private BatchStreamSegment _currentObjectSegment; + private BatchStreamSegment _currentObjectSegment; private BatchStreamSegment _currentDataSegment; - public BatchStreamWriter(BatchStreamData output, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) + public BatchStreamWriter(BatchStreamData output, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) { _output = output; _memoryPool = memoryPool; @@ -69,7 +70,7 @@ internal class BatchStreamWriter : IDisposable _currentDataSegment.ElementCount += size; } - public void Write(ServerObject item) + public void WriteObject(object? item) { if (_currentObjectSegment.Data == null || _currentObjectSegment.ElementCount >= _currentObjectSegment.Data.Length) @@ -89,15 +90,15 @@ internal class BatchStreamReader : IDisposable { private readonly BatchStreamData _input; private readonly BatchStreamMemoryPool _memoryPool; - private readonly BatchStreamObjectPool _objectPool; + private readonly BatchStreamObjectPool _objectPool; - private BatchStreamSegment _currentObjectSegment; + private BatchStreamSegment _currentObjectSegment; private BatchStreamSegment _currentDataSegment; private int _memoryOffset, _objectOffset; - public BatchStreamReader(BatchStreamData _input, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) + public BatchStreamReader(BatchStreamData input, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) { - this._input = _input; + _input = input; _memoryPool = memoryPool; _objectPool = objectPool; } @@ -116,7 +117,7 @@ internal class BatchStreamReader : IDisposable if (_memoryOffset + size > _currentDataSegment.ElementCount) throw new InvalidOperationException("Attempted to read more memory then left in the current segment"); - var rv = *(T*)((byte*)_currentDataSegment.Data + size); + var rv = *(T*)((byte*)_currentDataSegment.Data + _memoryOffset); _memoryOffset += size; if (_memoryOffset == _currentDataSegment.ElementCount) { @@ -127,7 +128,9 @@ internal class BatchStreamReader : IDisposable return rv; } - public ServerObject ReadObject() + public T ReadObject() where T : class? => (T)ReadObject()!; + + public object? ReadObject() { if (_currentObjectSegment.Data == null) { @@ -148,6 +151,8 @@ internal class BatchStreamReader : IDisposable return rv; } + public bool IsObjectEof => _currentObjectSegment.Data == null && _input.Objects.Count == 0; + public bool IsStructEof => _currentDataSegment.Data == IntPtr.Zero && _input.Structs.Count == 0; public void Dispose() diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs index 913958765a..d76a9c609e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -18,27 +18,31 @@ internal abstract class BatchStreamPoolBase : IDisposable readonly int[] _usageStatistics = new int[10]; int _usageStatisticsSlot; - public BatchStreamPoolBase(bool needsFinalize = false) + public BatchStreamPoolBase(bool needsFinalize, Action>? startTimer = null) { if(!needsFinalize) GC.SuppressFinalize(needsFinalize); var updateRef = new WeakReference>(this); - StartUpdateTimer(updateRef); + StartUpdateTimer(startTimer, updateRef); } - static void StartUpdateTimer(WeakReference> updateRef) + static void StartUpdateTimer(Action>? startTimer, WeakReference> updateRef) { - DispatcherTimer.Run(() => + Func timerProc = () => { if (updateRef.TryGetTarget(out var target)) { target.UpdateStatistics(); return true; } - return false; - }, TimeSpan.FromSeconds(1)); + return false; + }; + if (startTimer != null) + startTimer(timerProc); + else + DispatcherTimer.Run(timerProc, TimeSpan.FromSeconds(1)); } private void UpdateStatistics() @@ -50,7 +54,7 @@ internal abstract class BatchStreamPoolBase : IDisposable while (recentlyUsedPooledSlots < _pool.Count) DestroyItem(_pool.Pop()); - _usageStatistics[_usage] = 0; + _usageStatistics[_usageStatisticsSlot] = 0; _usageStatisticsSlot = (_usageStatisticsSlot + 1) % _usageStatistics.Length; } } @@ -109,11 +113,11 @@ internal abstract class BatchStreamPoolBase : IDisposable } } -internal sealed class BatchStreamObjectPool : BatchStreamPoolBase where T : class +internal sealed class BatchStreamObjectPool : BatchStreamPoolBase where T : class? { private readonly int _arraySize; - public BatchStreamObjectPool(int arraySize = 1024) + public BatchStreamObjectPool(int arraySize = 1024, Action>? startTimer = null) : base(false, startTimer) { _arraySize = arraySize; } @@ -133,7 +137,7 @@ internal sealed class BatchStreamMemoryPool : BatchStreamPoolBase { public int BufferSize { get; } - public BatchStreamMemoryPool(int bufferSize = 16384) + public BatchStreamMemoryPool(int bufferSize = 16384, Action>? startTimer = null) : base(true, startTimer) { BufferSize = bufferSize; } diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs new file mode 100644 index 0000000000..7d21b03f24 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs @@ -0,0 +1,9 @@ +using System; + +namespace Avalonia.Rendering.Composition.Transport; + +internal class BatchStreamDebugMarkers +{ + public static object ObjectEndMarker = new object(); + public static Guid ObjectEndMagic = Guid.NewGuid(); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs deleted file mode 100644 index cbee350ab3..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using Avalonia.Rendering.Composition.Animations; - -namespace Avalonia.Rendering.Composition.Transport -{ - struct Change - { - private T? _value; - - public bool IsSet { get; private set; } - - public T? Value - { - get - { - if(!IsSet) - throw new InvalidOperationException(); - return _value; - } - set - { - IsSet = true; - _value = value; - } - } - - public void Reset() - { - _value = default; - IsSet = false; - } - } - - struct AnimatedChange - { - private T? _value; - private IAnimationInstance? _animation; - - public bool IsValue { get; private set; } - public bool IsAnimation { get; private set; } - - public T Value - { - get - { - if(!IsValue) - throw new InvalidOperationException(); - return _value!; - } - set - { - IsAnimation = false; - _animation = null; - IsValue = true; - _value = value; - } - } - - public IAnimationInstance Animation - { - get - { - if(!IsAnimation) - throw new InvalidOperationException(); - return _animation!; - } - set - { - IsValue = false; - _value = default; - IsAnimation = true; - _animation = value; - } - } - - public void Reset() - { - this = default; - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs deleted file mode 100644 index 898885dce6..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition.Transport -{ - internal abstract class ChangeSet - { - private readonly IChangeSetPool _pool; - public Batch Batch = null!; - public ServerObject? Target; - public bool Dispose; - - public ChangeSet(IChangeSetPool pool) - { - _pool = pool; - } - - public virtual void Reset() - { - Batch = null!; - Target = null; - Dispose = false; - } - - public void Return() - { - _pool.Return(this); - } - } - - internal class CompositionObjectChanges : ChangeSet - { - public CompositionObjectChanges(IChangeSetPool pool) : base(pool) - { - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs deleted file mode 100644 index ea97cd7d44..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Concurrent; -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition.Transport -{ - interface IChangeSetPool - { - void Return(ChangeSet changes); - ChangeSet Get(ServerObject target, Batch batch); - } - - class ChangeSetPool : IChangeSetPool where T : ChangeSet - { - private readonly Func _factory; - private readonly ConcurrentBag _pool = new ConcurrentBag(); - - public ChangeSetPool(Func factory) - { - _factory = factory; - } - - public void Return(T changes) - { - changes.Reset(); - _pool.Add(changes); - } - - void IChangeSetPool.Return(ChangeSet changes) => Return((T) changes); - ChangeSet IChangeSetPool.Get(ServerObject target, Batch batch) => Get(target, batch); - - public T Get(ServerObject target, Batch batch) - { - if (!_pool.TryTake(out var res)) - res = _factory(this); - res.Target = target; - res.Batch = batch; - res.Dispose = false; - return res; - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs deleted file mode 100644 index 014adc7bbe..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Avalonia.Rendering.Composition.Transport; - -partial class CompositionTargetChanges -{ - public Change RedrawRequested; - - partial void ResetExtra() - { - RedrawRequested.Reset(); - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs deleted file mode 100644 index aed041b62e..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Avalonia.Rendering.Composition.Transport -{ - class CustomDrawVisualChanges : CompositionVisualChanges - { - public CustomDrawVisualChanges(IChangeSetPool pool) : base(pool) - { - } - - public Change Data; - - public override void Reset() - { - Data.Reset(); - base.Reset(); - } - - public new static ChangeSetPool> Pool { get; } = - new ChangeSetPool>(pool => new CustomDrawVisualChanges(pool)); - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs deleted file mode 100644 index 215c03b229..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using Avalonia.Collections.Pooled; -using Avalonia.Rendering.Composition.Drawing; -using Avalonia.Rendering.SceneGraph; -using Avalonia.Utilities; - -namespace Avalonia.Rendering.Composition.Transport; - -internal class DrawListVisualChanges : CompositionVisualChanges -{ - private CompositionDrawList? _drawCommands; - - public DrawListVisualChanges(IChangeSetPool pool) : base(pool) - { - } - - public CompositionDrawList? DrawCommands - { - get => _drawCommands; - set - { - _drawCommands?.Dispose(); - _drawCommands = value; - DrawCommandsIsSet = true; - } - } - - public bool DrawCommandsIsSet { get; private set; } - - public CompositionDrawList? AcquireDrawCommands() - { - var rv = _drawCommands; - _drawCommands = null; - DrawCommandsIsSet = false; - return rv; - } - - public override void Reset() - { - _drawCommands?.Dispose(); - _drawCommands = null; - DrawCommandsIsSet = false; - base.Reset(); - } - - public new static ChangeSetPool Pool { get; } = - new ChangeSetPool(pool => new(pool)); -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs deleted file mode 100644 index ee6e4231f8..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition.Transport -{ - internal class ListChange where T : ServerObject - { - public int Index; - public ListChangeAction Action; - public T? Added; - } - - internal enum ListChangeAction - { - InsertAt, - RemoveAt, - Clear, - ReplaceAt - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs deleted file mode 100644 index 9bb101a080..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition.Transport -{ - class ListChangeSet : ChangeSet where T : ServerObject - { - private List>? _listChanges; - public List> ListChanges => _listChanges ??= new List>(); - public bool HasListChanges => _listChanges != null; - - public override void Reset() - { - _listChanges?.Clear(); - base.Reset(); - } - - public ListChangeSet(IChangeSetPool pool) : base(pool) - { - } - - public static readonly ChangeSetPool> Pool = - new ChangeSetPool>(pool => new ListChangeSet(pool)); - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs index 1add3aa990..2399bd71d7 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs @@ -8,19 +8,21 @@ namespace Avalonia.Rendering.Composition.Transport where TServer : ServerObject where TClient : CompositionObject { - private readonly IGetChanges _parent; - private readonly List _list = new List(); + private readonly IRegisterForSerialization _parent; + private bool _changed; - public interface IGetChanges + public interface IRegisterForSerialization { - ListChangeSet GetChanges(); + void RegisterForSerialization(); } - public ServerListProxyHelper(IGetChanges parent) + public ServerListProxyHelper(IRegisterForSerialization parent) { _parent = parent; } - + + private readonly List _list = new List(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public List.Enumerator GetEnumerator() => _list.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); @@ -30,10 +32,8 @@ namespace Avalonia.Rendering.Composition.Transport public void Clear() { _list.Clear(); - _parent.GetChanges().ListChanges.Add(new ListChange - { - Action = ListChangeAction.Clear - }); + _changed = true; + _parent.RegisterForSerialization(); } public bool Contains(TClient item) => _list.Contains(item); @@ -56,22 +56,15 @@ namespace Avalonia.Rendering.Composition.Transport public void Insert(int index, TClient item) { _list.Insert(index, item); - _parent.GetChanges().ListChanges.Add(new ListChange - { - Action = ListChangeAction.InsertAt, - Index = index, - Added = (TServer) item.Server - }); + _changed = true; + _parent.RegisterForSerialization(); } public void RemoveAt(int index) { _list.RemoveAt(index); - _parent.GetChanges().ListChanges.Add(new ListChange - { - Action = ListChangeAction.RemoveAt, - Index = index - }); + _changed = true; + _parent.RegisterForSerialization(); } public TClient this[int index] @@ -80,13 +73,21 @@ namespace Avalonia.Rendering.Composition.Transport set { _list[index] = value; - _parent.GetChanges().ListChanges.Add(new ListChange - { - Action = ListChangeAction.ReplaceAt, - Index = index, - Added = (TServer) value.Server - }); + _changed = true; + _parent.RegisterForSerialization(); + } + } + + public void Serialize(BatchStreamWriter writer) + { + writer.Write((byte)(_changed ? 1 : 0)); + if (_changed) + { + writer.Write(_list.Count); + foreach (var el in _list) + writer.WriteObject(el.Server); } + _changed = false; } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs deleted file mode 100644 index c87fb96967..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition.Transport -{ - partial class CompositionVisualChanges - { - public Change Parent; - public Change Root; - - partial void ResetExtra() - { - Parent.Reset(); - Root.Reset(); - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index fa8d5d8f3b..5bf5dcee74 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -4,39 +4,14 @@ namespace Avalonia.Rendering.Composition { public abstract partial class CompositionVisual { - private CompositionVisual? _parent; - private CompositionTarget? _root; - - public CompositionVisual? Parent - { - get => _parent; - internal set - { - if (_parent == value) - return; - _parent = value; - Changes.Parent.Value = value?.Server; - Root = _parent?.Root; - } - } - - // TODO: hide behind private-ish API - public CompositionTarget? Root + private protected virtual void OnRootChangedCore() { - get => _root; - internal set - { - var changed = _root != value; - _root = value; - Changes.Root.Value = value?.Server; - if (changed) - OnRootChanged(); - } } - private protected virtual void OnRootChanged() - { - } + partial void OnRootChanged() => OnRootChangedCore(); + + partial void OnParentChanged() => Root = Parent?.Root; + internal Matrix4x4? TryGetServerTransform() { diff --git a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs index fef4caf675..42226a8b4d 100644 --- a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs +++ b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs @@ -64,6 +64,7 @@ namespace Avalonia.Rendering.Composition { if (item.Parent != null) throw new InvalidOperationException("Visual already has a parent"); + item.Parent = item; } } } \ No newline at end of file diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index eb1ffe1922..a7ae341bb3 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -4,10 +4,12 @@ Avalonia.Rendering.Composition.Server Avalonia.Rendering.Composition.Transport Avalonia.Rendering.Composition.Animations - - + + + + @@ -21,6 +23,7 @@ + diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs index 8b6aca33cd..096864e52a 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs @@ -35,6 +35,10 @@ namespace Avalonia.SourceGenerator.CompositionGenerator [XmlAttribute] public string Name { get; set; } + + [XmlAttribute] + public bool Passthrough { get; set; } + [XmlAttribute] public string ServerName { get; set; } } @@ -104,6 +108,8 @@ namespace Avalonia.SourceGenerator.CompositionGenerator public string DefaultValue { get; set; } [XmlAttribute] public bool Animated { get; set; } + [XmlAttribute] + public bool InternalSet { get; set; } } public class GAnimationType diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs index 43a4a4afa7..d88e9b4600 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs @@ -41,6 +41,15 @@ namespace Avalonia.SourceGenerator.CompositionGenerator return cl; return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); } + + public static EnumDeclarationSyntax AddModifiers(this EnumDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static string WithLowerFirst(this string s) { diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs index 7d5146c5f5..314ac1acbf 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs @@ -25,7 +25,7 @@ namespace Avalonia.Rendering.Composition internal override IAnimationInstance CreateInstance(Avalonia.Rendering.Composition.Server.ServerObject targetObject, ExpressionVariant? finalValue) {{ - return new KeyFrameAnimationInstance<{a.Type}>({name}Interpolator.Instance, _keyFrames.Snapshot(), CreateSnapshot(true), + return new KeyFrameAnimationInstance<{a.Type}>({name}Interpolator.Instance, _keyFrames.Snapshot(), CreateSnapshot(), finalValue?.CastOrDefault<{a.Type}>(), targetObject, DelayBehavior, DelayTime, Direction, Duration, IterationBehavior, IterationCount, StopBehavior); diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs index 593386f713..e0ea5b20ae 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs @@ -11,9 +11,7 @@ class Template { private ServerListProxyHelper _list = null!; - ListChangeSet - ServerListProxyHelper.IGetChanges. - GetChanges() => Changes; + void ServerListProxyHelper.IRegisterForSerialization.RegisterForSerialization() => RegisterForSerialization(); public List.Enumerator GetEnumerator() => _list.GetEnumerator(); @@ -87,7 +85,11 @@ class Template partial void OnBeforeReplace(ItemTypeName oldItem, ItemTypeName newItem); partial void OnReplace(ItemTypeName oldItem, ItemTypeName newItem); partial void OnClear(); -} + private protected override void SerializeChangesCore(BatchStreamWriter writer) + {{ + _list.Serialize(writer); + base.SerializeChangesCore(writer); + }} "; private ClassDeclarationSyntax AppendListProxy(GList list, ClassDeclarationSyntax cl) @@ -97,7 +99,7 @@ class Template var serverItemType = ServerName(itemType); cl = cl.AddBaseListTypes(SimpleBaseType( - ParseTypeName("ServerListProxyHelper<" + itemType + ", " + serverItemType + ">.IGetChanges")), + ParseTypeName("ServerListProxyHelper<" + itemType + ", " + serverItemType + ">.IRegisterForSerialization")), SimpleBaseType(ParseTypeName("IList<" + itemType + ">")) ); var code = ListProxyTemplate.Replace("ListTypeName", list.Name) diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs index 5a514a4eff..3c38c0331e 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; @@ -38,7 +39,12 @@ namespace Avalonia.SourceGenerator.CompositionGenerator string ServerName(string c) => c != null ? ("Server" + c) : "ServerObject"; string ChangesName(string c) => c != null ? (c + "Changes") : "ChangeSet"; - + string ChangedFieldsTypeName(GClass c) => c.Name + "ChangedFields"; + string ChangedFieldsFieldName(GClass c) => "_changedFieldsOf" + c.Name; + string PropertyBackingFieldName(GProperty prop) => "_" + prop.Name.WithLowerFirst(); + string ServerPropertyOffsetFieldName(GProperty prop) => "s_OffsetOf" + PropertyBackingFieldName(prop); + string PropertyPendingAnimationFieldName(GProperty prop) => "_pendingAnimationFor" + prop.Name; + void GenerateClass(GClass cl) { var list = cl as GList; @@ -68,36 +74,13 @@ namespace Avalonia.SourceGenerator.CompositionGenerator .WithBaseType(serverBase); string changesName = ChangesName(cl.Name); - var changesBase = ChangesName(cl.ChangesBase ?? cl.Inherits); + string changedFieldsTypeName = ChangedFieldsTypeName(cl); + string changedFieldsName = ChangedFieldsFieldName(cl); - if (list != null) - changesBase = "ListChangeSet<" + ServerName(list.ItemType) + ">"; - - var changeSetPoolType = "ChangeSetPool<" + changesName + ">"; - var transport = ClassDeclaration(changesName) - .AddModifiers(SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) - .WithBaseType(changesBase) - .AddMembers(DeclareField(changeSetPoolType, "Pool", - EqualsValueClause( - ParseExpression($"new {changeSetPoolType}(pool => new {changesName}(pool))") - ), - SyntaxKind.PublicKeyword, - SyntaxKind.StaticKeyword, SyntaxKind.ReadOnlyKeyword)) - .AddMembers(ParseMemberDeclaration($"public {changesName}(IChangeSetPool pool) : base(pool){{}}")); - - client = client - .AddMembers( - PropertyDeclaration(ParseTypeName("IChangeSetPool"), "ChangeSetPool") - .AddModifiers(SyntaxKind.PrivateKeyword, SyntaxKind.ProtectedKeyword, - SyntaxKind.OverrideKeyword) - .WithExpressionBody( - ArrowExpressionClause(MemberAccess(changesName, "Pool"))) - .WithSemicolonToken(Semicolon())) - .AddMembers(PropertyDeclaration(ParseTypeName(changesName), "Changes") - .AddModifiers(SyntaxKind.PrivateKeyword, SyntaxKind.NewKeyword) - .WithExpressionBody(ArrowExpressionClause(CastExpression(ParseTypeName(changesName), - MemberAccess(BaseExpression(), "Changes")))) - .WithSemicolonToken(Semicolon())); + if (cl.Properties.Count > 0) + client = client + .AddMembers(DeclareField(changedFieldsTypeName, changedFieldsName)); + if (!cl.CustomCtor) { @@ -105,7 +88,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator .AddModifiers(SyntaxKind.InternalKeyword, SyntaxKind.NewKeyword) .AddAccessorListAccessors(AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithSemicolonToken(Semicolon()))); - client = client.AddMembers( + client = client.AddMembers( ConstructorDeclaration(cl.Name) .AddModifiers(SyntaxKind.InternalKeyword) .WithParameterList(ParameterList(SeparatedList(new[] @@ -141,20 +124,20 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ArgumentList(SeparatedList(new[] { Argument(IdentifierName("compositor")), - })))).WithBody(Block())); + })))).WithBody(Block(ParseStatement("Initialize();")))); } + server = server.AddMembers( + MethodDeclaration(ParseTypeName("void"), "Initialize") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + var changesVarName = "c"; var changesVar = IdentifierName(changesVarName); server = server.AddMembers( - MethodDeclaration(ParseTypeName("void"), "ApplyChangesExtra") - .AddParameterListParameters(Parameter(Identifier("c")).WithType(ParseTypeName(changesName))) - .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); - - transport = transport.AddMembers( - MethodDeclaration(ParseTypeName("void"), "ResetExtra") + MethodDeclaration(ParseTypeName("void"), "DeserializeChangesExtra") + .AddParameterListParameters(Parameter(Identifier("c")).WithType(ParseTypeName("BatchStreamReader"))) .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); var applyMethodBody = Block( @@ -168,7 +151,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ExpressionStatement(InvocationExpression(IdentifierName("ApplyChangesExtra")) .AddArgumentListArguments(Argument(IdentifierName("c")))) ); - + var uninitializedObjectName = "dummy"; var serverStaticCtorBody = cl.Abstract ? Block() @@ -179,72 +162,51 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ParseStatement("InitializeFieldOffsets(dummy);") ); - var initializeFieldOffsetsBody = cl.ServerBase == null + var initializeFieldOffsetsBody = cl.Inherits == null ? Block() - : Block(ParseStatement($"{cl.ServerBase}.InitializeFieldOffsets(dummy);")); + : Block(ParseStatement($"Server{cl.Inherits}.InitializeFieldOffsets(dummy);")); var resetBody = Block(); var startAnimationBody = Block(); - var getPropertyBody = Block(); var serverGetPropertyBody = Block(); var serverGetFieldOffsetBody = Block(); var activatedBody = Block(ParseStatement("base.Activated();")); var deactivatedBody = Block(ParseStatement("base.Deactivated();")); + var serializeMethodBody = SerializeChangesPrologue(cl); + var deserializeMethodBody = DeserializeChangesPrologue(cl); - var defaultsMethodBody = Block(); + var defaultsMethodBody = Block(ParseStatement("InitializeDefaultsExtra();")); foreach (var prop in cl.Properties) { - var fieldName = "_" + prop.Name.WithLowerFirst(); - var fieldOffsetName = "s_OffsetOf" + fieldName; + var fieldName = PropertyBackingFieldName(prop); + var animatedFieldName = PropertyPendingAnimationFieldName(prop); + var fieldOffsetName = ServerPropertyOffsetFieldName(prop); var propType = ParseTypeName(prop.Type); var filteredPropertyType = prop.Type.TrimEnd('?'); var isObject = _objects.Contains(filteredPropertyType); var isNullable = prop.Type.EndsWith("?"); - - - - - client = client - .AddMembers(DeclareField(prop.Type, fieldName)) - .AddMembers(PropertyDeclaration(propType, prop.Name) - .AddModifiers(SyntaxKind.PublicKeyword) - .AddAccessorListAccessors( - AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, - Block(ReturnStatement(IdentifierName(fieldName)))), - AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, - Block( - ParseStatement("var changed = false;"), - IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, - IdentifierName(fieldName), - IdentifierName("value")), - Block( - ParseStatement("On" + prop.Name + "Changing();"), - ParseStatement("changed = true;"), - GeneratePropertySetterAssignment(prop, fieldName, isObject, isNullable)) - ), - ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, - IdentifierName(fieldName), IdentifierName("value"))), - ParseStatement($"if(changed) On" + prop.Name + "Changed();") - )) - )) - .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed") - .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())) - .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") - .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + bool isPassthrough = false; + if (prop.Animated) + client = client.AddMembers(DeclareField("IAnimationInstance?", animatedFieldName)); + client = GenerateClientProperty(client, cl, prop, propType, isObject, isNullable); var animatedServer = prop.Animated; var serverPropertyType = ((isObject ? "Server" : "") + prop.Type); - if (_manuals.TryGetValue(filteredPropertyType, out var manual) && manual.ServerName != null) - serverPropertyType = manual.ServerName + (isNullable ? "?" : ""); - - - transport = transport - .AddMembers(DeclareField((animatedServer ? "Animated" : "") + "Change<" + serverPropertyType + ">", - prop.Name, SyntaxKind.PublicKeyword)); + if (_manuals.TryGetValue(filteredPropertyType, out var manual)) + { + if (manual.Passthrough) + { + isPassthrough = true; + serverPropertyType = prop.Type; + } + if (manual.ServerName != null) + serverPropertyType = manual.ServerName + (isNullable ? "?" : ""); + } + if (animatedServer) server = server.AddMembers( DeclareField("ServerAnimatedValueStore<" + serverPropertyType + ">", fieldName), @@ -301,7 +263,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ArgumentList(SeparatedList(new[] { Argument(IdentifierName("this")), - Argument(changesVar), + Argument(ParseExpression("c.Batch.CommitedAt")), Argument(MemberAccess(changesVar, prop.Name, "Animation")), Argument(IdentifierName(fieldOffsetName)) }))))) @@ -316,16 +278,18 @@ namespace Avalonia.SourceGenerator.CompositionGenerator resetBody = resetBody.AddStatements( ExpressionStatement(InvocationExpression(MemberAccess(prop.Name, "Reset")))); - + + serializeMethodBody = ApplySerializeField(serializeMethodBody,cl, prop, isObject, isPassthrough); + deserializeMethodBody = ApplyDeserializeField(deserializeMethodBody,cl, prop, serverPropertyType, isObject); + if (animatedServer) { - startAnimationBody = ApplyStartAnimation(startAnimationBody, prop, fieldName); + startAnimationBody = ApplyStartAnimation(startAnimationBody, cl, prop); activatedBody = activatedBody.AddStatements(ParseStatement($"{fieldName}.Activate(this);")); deactivatedBody = deactivatedBody.AddStatements(ParseStatement($"{fieldName}.Deactivate(this);")); } - - getPropertyBody = ApplyGetProperty(getPropertyBody, prop); + serverGetPropertyBody = ApplyGetProperty(serverGetPropertyBody, prop); serverGetFieldOffsetBody = ApplyGetProperty(serverGetFieldOffsetBody, prop, fieldOffsetName); @@ -345,55 +309,6 @@ namespace Avalonia.SourceGenerator.CompositionGenerator } } - if (cl is GBrush brush && !cl.Abstract) - { - var brushName = brush.Name.StripPrefix("Composition"); - /* - server = server.AddMembers( - MethodDeclaration(ParseTypeName("ICbBrush"), "CreateBackendBrush") - .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) - .WithExpressionBody(ArrowExpressionClause( - InvocationExpression(MemberAccess("Compositor", "Backend", "Create" + brushName)) - )).WithSemicolonToken(Semicolon()) - ); - if (!brush.CustomUpdate) - server = server.AddMembers( - MethodDeclaration(ParseTypeName("void"), "UpdateBackendBrush") - .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) - .AddParameterListParameters(Parameter(Identifier("brush")) - .WithType(ParseTypeName("ICbBrush"))) - .AddBodyStatements( - ExpressionStatement( - InvocationExpression( - MemberAccess( - ParenthesizedExpression( - CastExpression(ParseTypeName("ICb" + brushName), IdentifierName("brush"))), "Update"), - ArgumentList(SeparatedList(cl.Properties.Select(x => - { - if(x.Type.TrimEnd('?') == "ICompositionSurface") - return Argument( - ConditionalAccessExpression(IdentifierName(x.Name), - MemberBindingExpression(IdentifierName("BackendSurface"))) - ); - if (_brushes.Contains(x.Type)) - return Argument( - ConditionalAccessExpression(IdentifierName(x.Name), - MemberBindingExpression(IdentifierName("Brush"))) - ); - return Argument(IdentifierName(x.Name)); - })))) - ))); - -*/ - } - - server = server.AddMembers( - MethodDeclaration(ParseTypeName("void"), "ApplyCore") - .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) - .AddParameterListParameters( - Parameter(Identifier("changes")).WithType(ParseTypeName("ChangeSet"))) - .WithBody(applyMethodBody)); - server = server.AddMembers(ConstructorDeclaration(serverName) .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))) .WithBody(serverStaticCtorBody)); @@ -408,26 +323,32 @@ namespace Avalonia.SourceGenerator.CompositionGenerator $"protected override void Activated(){{}}")!).WithBody(activatedBody)) .AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( $"protected override void Deactivated(){{}}")!).WithBody(deactivatedBody)); + if (cl.Properties.Count > 0) + server = server.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( + $"protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt){{}}") + !) + .WithBody(deserializeMethodBody)); client = client.AddMembers( - MethodDeclaration(ParseTypeName("void"), "InitializeDefaults").WithBody(defaultsMethodBody)); - - transport = transport.AddMembers(MethodDeclaration(ParseTypeName("void"), "Reset") - .AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.OverrideKeyword) - .WithBody(resetBody.AddStatements( - ExpressionStatement(InvocationExpression(IdentifierName("ResetExtra"))), - ExpressionStatement(InvocationExpression(MemberAccess("base", "Reset")))))); - + MethodDeclaration(ParseTypeName("void"), "InitializeDefaults").WithBody(defaultsMethodBody)) + .AddMembers( + MethodDeclaration(ParseTypeName("void"), "InitializeDefaultsExtra") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + if (cl.Properties.Count > 0) + client = client.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( + $"private protected override void SerializeChangesCore(BatchStreamWriter writer){{}}")!) + .WithBody(serializeMethodBody)); + if (list != null) client = AppendListProxy(list, client); if (startAnimationBody.Statements.Count != 0) client = WithStartAnimation(client, startAnimationBody); - - client = WithGetProperty(client, getPropertyBody, false); - server = WithGetProperty(server, serverGetPropertyBody, true); + + server = WithGetPropertyForAnimation(server, serverGetPropertyBody); server = WithGetFieldOffset(server, serverGetFieldOffsetBody); - + if(cl.Implements.Count > 0) foreach (var impl in cl.Implements) { @@ -441,57 +362,121 @@ namespace Avalonia.SourceGenerator.CompositionGenerator } + SaveTo(unit.AddMembers(GenerateChangedFieldsEnum(cl)), "Transport", + ChangedFieldsTypeName(cl) + ".generated.cs"); + SaveTo(unit.AddMembers(clientNs.AddMembers(client)), cl.Name + ".generated.cs"); SaveTo(unit.AddMembers(serverNs.AddMembers(server)), "Server", "Server" + cl.Name + ".generated.cs"); - SaveTo(unit.AddMembers(transportNs.AddMembers(transport)), - "Transport", cl.Name + "Changes.generated.cs"); + } + + private ClassDeclarationSyntax GenerateClientProperty(ClassDeclarationSyntax client, GClass cl, GProperty prop, + TypeSyntax propType, bool isObject, bool isNullable) + { + var fieldName = PropertyBackingFieldName(prop); + return client + .AddMembers(DeclareField(prop.Type, fieldName)) + .AddMembers(PropertyDeclaration(propType, prop.Name) + .AddModifiers(SyntaxKind.PublicKeyword) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, + Block(ReturnStatement(IdentifierName(fieldName)))), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, + Block( + ParseStatement("var changed = false;"), + IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, + IdentifierName(fieldName), + IdentifierName("value")), + Block( + ParseStatement("On" + prop.Name + "Changing();"), + ParseStatement("changed = true;"), + GeneratePropertySetterAssignment(cl, prop, isObject, isNullable)) + ), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldName), IdentifierName("value"))), + ParseStatement($"if(changed) On" + prop.Name + "Changed();") + )).WithModifiers(TokenList(prop.InternalSet ? new[]{Token(SyntaxKind.InternalKeyword)} : Array.Empty())) + )) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); } - StatementSyntax GeneratePropertySetterAssignment(GProperty prop, string fieldName, bool isObject, bool isNullable) + EnumDeclarationSyntax GenerateChangedFieldsEnum(GClass cl) { - var normalChangesAssignment = (StatementSyntax)ExpressionStatement(AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - MemberAccess((ExpressionSyntax) IdentifierName("Changes"), prop.Name, - "Value"), - isObject - ? - ConditionalMemberAccess(IdentifierName("value"), "Server", isNullable) - : IdentifierName("value"))); - if (!prop.Animated) - return normalChangesAssignment; + var changedFieldsEnum = EnumDeclaration(Identifier(ChangedFieldsTypeName(cl))); + int count = 0; - var code = $@" -{{ - if(animation is CompositionAnimation a) - Changes.{prop.Name}.Animation = a.CreateInstance(this.Server, value); - else + void AddValue(string name) + { + var value = 1ul << count; + changedFieldsEnum = changedFieldsEnum.AddMembers( + EnumMemberDeclaration(name) + .WithEqualsValue(EqualsValueClause(ParseExpression(value.ToString())))); + count++; + } + + foreach (var prop in cl.Properties) + { + AddValue(prop.Name); + + if (prop.Animated) + AddValue(prop.Name + "Animated"); + } + + var baseType = count <= 8 ? "byte" : count <= 16 ? "ushort" : count <= 32 ? "uint" : "ulong"; + return changedFieldsEnum.AddBaseListTypes(SimpleBaseType(ParseTypeName(baseType))) + .AddAttributeLists(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("System.Flags"))))); + } + + StatementSyntax GeneratePropertySetterAssignment(GClass cl, GProperty prop, bool isObject, bool isNullable) + { + var pendingAnimationField = PropertyPendingAnimationFieldName(prop); + + var code = @$" + // Update the backing value + {PropertyBackingFieldName(prop)} = value; + + // Register object for serialization in the next batch + {ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}; + RegisterForSerialization(); +"; + if (prop.Animated) + { + code += @$" + // Reset previous animation if any + {pendingAnimationField} = null; + {ChangedFieldsFieldName(cl)} &= ~{ChangedFieldsTypeName(cl)}.{prop.Name}Animated; + // Check for implicit animations + if(ImplicitAnimations != null && ImplicitAnimations.TryGetValue(""{prop.Name}"", out var animation) == true) {{ - var saved = Changes.{prop.Name}; - if(!StartAnimationGroup(animation, ""{prop.Name}"", value)) - Changes.{prop.Name}.Value = value; + // Animation affects only current property + if(animation is CompositionAnimation a) + {{ + {ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}Animated; + {pendingAnimationField} = a.CreateInstance(this.Server, value); + }} + // Animation is triggered by the current field, but does not necessary affects it + StartAnimationGroup(animation, ""{prop.Name}"", value); }} -}} - "; - - return IfStatement( - ParseExpression( - $"ImplicitAnimations != null && ImplicitAnimations.TryGetValue(\"{prop.Name}\", out var animation) == true"), - ParseStatement(code), - ElseClause(normalChangesAssignment) - ); + } + + return ParseStatement("{\n" + code + "\n}"); } - BlockSyntax ApplyStartAnimation(BlockSyntax body, GProperty prop, string fieldName) + BlockSyntax ApplyStartAnimation(BlockSyntax body, GClass cl, GProperty prop) { var code = $@" if (propertyName == ""{prop.Name}"") {{ -var current = {fieldName}; +var current = {PropertyBackingFieldName(prop)}; var server = animation.CreateInstance(this.Server, finalValue); -Changes.{prop.Name}.Animation = server; +{PropertyPendingAnimationFieldName(prop)} = server; +{ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}Animated; +RegisterForSerialization(); return; }} "; @@ -522,15 +507,75 @@ return; return body; } + + private BlockSyntax SerializeChangesPrologue(GClass cl) + { + return Block( + ParseStatement("base.SerializeChangesCore(writer);"), + ParseStatement($"writer.Write({ChangedFieldsFieldName(cl)});") + ); + } + + BlockSyntax ApplySerializeField(BlockSyntax body, GClass cl, GProperty prop, bool isObject, bool isPassthrough) + { + var changedFields = ChangedFieldsFieldName(cl); + var changedFieldsType = ChangedFieldsTypeName(cl); + + var code = ""; + if (prop.Animated) + { + code = $@" + if(({changedFields} & {changedFieldsType}.{prop.Name}Animated) == {changedFieldsType}.{prop.Name}Animated) + writer.WriteObject({PropertyPendingAnimationFieldName(prop)}); + else "; + } + + code += $@" + if(({changedFields} & {changedFieldsType}.{prop.Name}) == {changedFieldsType}.{prop.Name}) + writer.Write{(isObject ? "Object" : "")}({PropertyBackingFieldName(prop)}{(isObject && !isPassthrough ? "?.Server!":"")}); +"; + return body.AddStatements(ParseStatement(code)); + } + + private BlockSyntax DeserializeChangesPrologue(GClass cl) + { + return Block(ParseStatement($@" +base.DeserializeChangesCore(reader, commitedAt); +DeserializeChangesExtra(reader); +var changed = reader.Read<{ChangedFieldsTypeName(cl)}>(); +")); + } + + BlockSyntax ApplyDeserializeField(BlockSyntax body, GClass cl, GProperty prop, string serverType, bool isObject) + { + var changedFieldsType = ChangedFieldsTypeName(cl); + var code = ""; + if (prop.Animated) + { + code = $@" + if((changed & {changedFieldsType}.{prop.Name}Animated) == {changedFieldsType}.{prop.Name}Animated) + {PropertyBackingFieldName(prop)}.SetAnimation(this, commitedAt, reader.ReadObject(), {ServerPropertyOffsetFieldName(prop)}); + else "; + } + + var readValueCode = $"reader.Read{(isObject ? "Object" : "")}<{serverType}>()"; + code += $@" + if((changed & {changedFieldsType}.{prop.Name}) == {changedFieldsType}.{prop.Name}) +"; + if (prop.Animated) + code += $"{PropertyBackingFieldName(prop)}.SetValue(this, {readValueCode});"; + else code += $"{prop.Name} = {readValueCode};"; + return body.AddStatements(ParseStatement(code)); + } - ClassDeclarationSyntax WithGetProperty(ClassDeclarationSyntax cl, BlockSyntax body, bool server) + ClassDeclarationSyntax WithGetPropertyForAnimation(ClassDeclarationSyntax cl, BlockSyntax body) { if (body.Statements.Count == 0) return cl; body = body.AddStatements( ParseStatement("return base.GetPropertyForAnimation(name);")); var method = ((MethodDeclarationSyntax) ParseMemberDeclaration( - $"{(server ? "public" : "internal")} override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}")) + $"public override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}")) .WithBody(body); return cl.AddMembers(method); diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 7a64b39575..9c82288c8e 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -104,7 +104,7 @@ namespace Avalonia.X11 } if (options.UseCompositor) - Compositor = Compositor.Create(AvaloniaLocator.Current.GetService()!); + Compositor = new Compositor(AvaloniaLocator.Current.GetService()!); } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 32705a2cc6..4b0350f40f 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -188,7 +188,7 @@ namespace Avalonia.Win32 AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); if (Options.UseCompositor) - Compositor = Compositor.Create(AvaloniaLocator.Current.GetRequiredService()); + Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService()); } public bool HasMessages() diff --git a/tests/Avalonia.Base.UnitTests/Composition/BatchStreamTests.cs b/tests/Avalonia.Base.UnitTests/Composition/BatchStreamTests.cs new file mode 100644 index 0000000000..a1b55257e6 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Composition/BatchStreamTests.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks.Dataflow; +using Avalonia.Rendering.Composition.Transport; +using Xunit; + +namespace Avalonia.Base.UnitTests.Composition; + +public class BatchStreamTests +{ + [Fact] + public void BatchStreamCorrectlyWritesAndReadsData() + { + var data = new BatchStreamData(); + var memPool = new BatchStreamMemoryPool(100, _ => { }); + var objPool = new BatchStreamObjectPool(10, _ => { }); + + var guids = new List(); + var objects = new List(); + for (var c = 0; c < 453; c++) + { + guids.Add(Guid.NewGuid()); + objects.Add(new object()); + } + + using (var writer = new BatchStreamWriter(data, memPool, objPool)) + { + foreach(var guid in guids) + writer.Write(guid); + foreach (var obj in objects) + writer.WriteObject(obj); + } + + using (var reader = new BatchStreamReader(data, memPool, objPool)) + { + foreach (var guid in guids) + Assert.Equal(guid, reader.Read()); + foreach (var obj in objects) + Assert.Equal(obj, reader.ReadObject()); + } + + + + } +} \ No newline at end of file From 2d8be0dff68dc4036bc6e8b5c40894f0367e6271 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 21:51:40 +0300 Subject: [PATCH 014/171] Fixes --- .../Composition/Transport/BatchStreamArrayPool.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs index d76a9c609e..97d05704af 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -51,11 +51,12 @@ internal abstract class BatchStreamPoolBase : IDisposable { var maximumUsage = _usageStatistics.Max(); var recentlyUsedPooledSlots = maximumUsage - _usage; - while (recentlyUsedPooledSlots < _pool.Count) + var keepSlots = Math.Max(recentlyUsedPooledSlots, 10); + while (keepSlots < _pool.Count) DestroyItem(_pool.Pop()); - _usageStatistics[_usageStatisticsSlot] = 0; _usageStatisticsSlot = (_usageStatisticsSlot + 1) % _usageStatistics.Length; + _usageStatistics[_usageStatisticsSlot] = 0; } } @@ -137,7 +138,7 @@ internal sealed class BatchStreamMemoryPool : BatchStreamPoolBase { public int BufferSize { get; } - public BatchStreamMemoryPool(int bufferSize = 16384, Action>? startTimer = null) : base(true, startTimer) + public BatchStreamMemoryPool(int bufferSize = 1024, Action>? startTimer = null) : base(true, startTimer) { BufferSize = bufferSize; } From dbbed2c70bcb7abff176edffa7a7b045a0dd914d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 23:14:13 +0300 Subject: [PATCH 015/171] Added Dispose for CompositionTarget --- .../Composition/CompositionObject.cs | 5 ++-- .../Server/ServerCompositionTarget.cs | 27 ++++++++++++++++++- .../Composition/Server/ServerCompositor.cs | 2 ++ .../Composition/Server/ServerObject.cs | 4 ++- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs index d561338a36..baf1bfcddf 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs @@ -25,7 +25,7 @@ namespace Avalonia.Rendering.Composition public void Dispose() { - //Changes.Dispose = true; + RegisterForSerialization(); IsDisposed = true; } @@ -112,7 +112,8 @@ namespace Avalonia.Rendering.Composition private protected virtual void SerializeChangesCore(BatchStreamWriter writer) { - + if (Server is IDisposable) + writer.Write((byte)(IsDisposed ? 1 : 0)); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 7567eba534..9513fb58fa 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -9,7 +9,7 @@ using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Server { - internal partial class ServerCompositionTarget + internal partial class ServerCompositionTarget : IDisposable { private readonly ServerCompositor _compositor; private readonly Func _renderTargetFactory; @@ -23,6 +23,7 @@ namespace Avalonia.Rendering.Composition.Server private Size _layerSize; private IDrawingContextLayerImpl? _layer; private bool _redrawRequested; + private bool _disposed; public ReadbackIndices Readback { get; } = new(); @@ -50,6 +51,12 @@ namespace Avalonia.Rendering.Composition.Server public void Render() { + if (_disposed) + { + Compositor.RemoveCompositionTarget(this); + return; + } + if (Root == null) return; _renderTarget ??= _renderTargetFactory(); @@ -69,6 +76,12 @@ namespace Avalonia.Rendering.Composition.Server _redrawRequested = false; using (var targetContext = _renderTarget.CreateDrawingContext(null)) { + // This is a hack to safely dispose layer created by some other render target + // because we can only dispose layers with the corresponding GPU context being + // active on the current thread + while (Compositor.LayersToDispose.Count > 0) + Compositor.LayersToDispose.Dequeue().Dispose(); + var layerSize = Size * Scaling; if (layerSize != _layerSize || _layer == null) { @@ -130,5 +143,17 @@ namespace Avalonia.Rendering.Composition.Server { _redrawRequested = true; } + + public void Dispose() + { + _disposed = true; + if (_layer != null) + { + Compositor.LayersToDispose.Enqueue(_layer); + _layer = null; + } + _renderTarget?.Dispose(); + _renderTarget = null; + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index f7de704b23..241be479ff 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Avalonia.Platform; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Transport; @@ -19,6 +20,7 @@ namespace Avalonia.Rendering.Composition.Server private List _animationsToUpdate = new(); private BatchStreamObjectPool _batchObjectPool; private BatchStreamMemoryPool _batchMemoryPool; + public Queue LayersToDispose { get; } = new(); public ServerCompositor(IRenderLoop renderLoop, BatchStreamObjectPool batchObjectPool, BatchStreamMemoryPool batchMemoryPool) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index 16f57d9059..dde711c3b5 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -107,7 +107,9 @@ namespace Avalonia.Rendering.Composition.Server protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) { - + if (this is IDisposable disp + && reader.Read() == 1) + disp.Dispose(); } public void DeserializeChanges(BatchStreamReader reader, Batch batch) From 7790a513065b0e91676ba675bea4587ff428d402 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 23:15:39 +0300 Subject: [PATCH 016/171] Fixed hittest filtering --- .../Rendering/Composition/CompositingRenderer.cs | 6 +++--- .../Composition/CompositionDrawListVisual.cs | 6 +++++- .../Rendering/Composition/CompositionTarget.cs | 13 ++++++++----- src/Avalonia.Base/Rendering/Composition/Visual.cs | 4 +++- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 88c6948e48..b83f804a8f 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -65,9 +65,9 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor QueueUpdate(); } - public IEnumerable HitTest(Point p, IVisual root, Func filter) + public IEnumerable HitTest(Point p, IVisual root, Func? filter) { - var res = _target.TryHitTest(p); + var res = _target.TryHitTest(p, filter); if(res == null) yield break; for (var index = res.Count - 1; index >= 0; index--) @@ -81,7 +81,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor } } - public IVisual? HitTestFirst(Point p, IVisual root, Func filter) + public IVisual? HitTestFirst(Point p, IVisual root, Func? filter) { // TODO: Optimize return HitTest(p, root, filter).FirstOrDefault(); diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index 069d888fbb..cc2b411822 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -1,7 +1,9 @@ +using System; using System.Numerics; using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Transport; +using Avalonia.VisualTree; namespace Avalonia.Rendering.Composition; @@ -39,10 +41,12 @@ internal class CompositionDrawListVisual : CompositionContainerVisual Visual = visual; } - internal override bool HitTest(Point pt) + internal override bool HitTest(Point pt, Func? filter) { if (DrawList == null) return false; + if (filter != null && !filter(Visual)) + return false; if (Visual is ICustomHitTest custom) return custom.HitTest(pt); foreach (var op in DrawList) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index c5cfaeacce..3243934932 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -1,6 +1,8 @@ +using System; using System.Collections.Generic; using System.Numerics; using Avalonia.Collections.Pooled; +using Avalonia.VisualTree; namespace Avalonia.Rendering.Composition { @@ -18,13 +20,13 @@ namespace Avalonia.Rendering.Composition Root.Root = null; } - public PooledList? TryHitTest(Point point) + public PooledList? TryHitTest(Point point, Func? filter) { Server.Readback.NextRead(); if (Root == null) return null; var res = new PooledList(); - HitTestCore(Root, point, res); + HitTestCore(Root, point, res, filter); return res; } @@ -69,7 +71,8 @@ namespace Avalonia.Rendering.Composition return false; } - bool HitTestCore(CompositionVisual visual, Point point, PooledList result) + bool HitTestCore(CompositionVisual visual, Point point, PooledList result, + Func? filter) { //TODO: Check readback too if (visual.Visible == false) @@ -80,7 +83,7 @@ namespace Avalonia.Rendering.Composition { bool success = false; // Hit-test the current node - if (visual.HitTest(point)) + if (visual.HitTest(point, filter)) { result.Add(visual); success = true; @@ -91,7 +94,7 @@ namespace Avalonia.Rendering.Composition for (var c = cv.Children.Count - 1; c >= 0; c--) { var ch = cv.Children[c]; - var hit = HitTestCore(ch, point, result); + var hit = HitTestCore(ch, point, result, filter); if (hit) return true; } diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index 5bf5dcee74..3d6e3fdaeb 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -1,4 +1,6 @@ +using System; using System.Numerics; +using Avalonia.VisualTree; namespace Avalonia.Rendering.Composition { @@ -33,6 +35,6 @@ namespace Avalonia.Rendering.Composition internal object? Tag { get; set; } - internal virtual bool HitTest(Point point) => true; + internal virtual bool HitTest(Point point, Func? filter) => true; } } \ No newline at end of file From fcb7d254f9d0f147dd85c38200a3ebd896c65b3c Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 20 May 2022 00:01:21 -0400 Subject: [PATCH 017/171] Add IColorPalette --- .../ColorPalette/IColorPalette.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPalette/IColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/IColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalette/IColorPalette.cs new file mode 100644 index 0000000000..7c6ebc3f6a --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPalette/IColorPalette.cs @@ -0,0 +1,38 @@ +using Avalonia.Media; + +namespace Avalonia.Controls +{ + /// + /// Interface to define a color palette. + /// + public interface IColorPalette + { + /// + /// Gets the total number of colors in this palette. + /// A color is not necessarily a single value and may be composed of several shades. + /// + /// + /// Represents total columns in a table. + /// + int ColorCount { get; } + + /// + /// Gets the total number of shades for each color in this palette. + /// Shades are usually a variation of the color lightening or darkening it. + /// + /// + /// Represents total rows in a table. + /// + int ShadeCount { get; } + + /// + /// Gets a color in the palette by index. + /// + /// The index of the color in the palette. + /// The index must be between zero and . + /// The index of the color shade in the palette. + /// The index must be between zero and . + /// The color at the specified index or an exception. + Color GetColor(int colorIndex, int shadeIndex); + } +} From fa5a47b4426d32a00b01233540e25342f2013646 Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 20 May 2022 00:01:44 -0400 Subject: [PATCH 018/171] Add initial ColorView.Properties --- .../ColorSpectrum/ColorSpectrum.Properties.cs | 2 +- .../ColorView/ColorView.Properties.cs | 362 ++++++++++++++++++ .../ColorView/ColorView.cs | 21 + 3 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs create mode 100644 src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs index 587a89ee38..00d84f5dd3 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs @@ -97,7 +97,7 @@ namespace Avalonia.Controls.Primitives /// Gets or sets the currently selected color in the RGB color model. /// /// - /// For control authors use instead to avoid loss + /// For control authors, use instead to avoid loss /// of precision and color drifting. /// public Color Color diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs new file mode 100644 index 0000000000..aa5dfb5fc4 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -0,0 +1,362 @@ +using System.Collections.ObjectModel; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Media; + +namespace Avalonia.Controls +{ + public partial class ColorView + { + // SelectedColorModel ActiveColorModel? + // SelectedTab + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorProperty = + AvaloniaProperty.Register( + nameof(Color), + Colors.White, + defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorSpectrumComponentsProperty = + AvaloniaProperty.Register( + nameof(ColorSpectrumComponents), + ColorSpectrumComponents.HueSaturation); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorSpectrumShapeProperty = + AvaloniaProperty.Register( + nameof(ColorSpectrumShape), + ColorSpectrumShape.Box); + + /// + /// Defines the property. + /// + public static readonly DirectProperty> CustomPaletteColorsProperty = + AvaloniaProperty.RegisterDirect>( + nameof(CustomPaletteColors), + o => o.CustomPaletteColors); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CustomPaletteColumnCountProperty = + AvaloniaProperty.Register( + nameof(CustomPaletteColumnCount), + 4); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CustomPaletteProperty = + AvaloniaProperty.Register( + nameof(CustomPalette), + null); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HsvColorProperty = + AvaloniaProperty.Register( + nameof(HsvColor), + Colors.White.ToHsv(), + defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAlphaEnabledProperty = + AvaloniaProperty.Register( + nameof(IsAlphaEnabled), + false); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAlphaSliderVisibleProperty = + AvaloniaProperty.Register( + nameof(IsAlphaSliderVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAlphaTextInputVisibleProperty = + AvaloniaProperty.Register( + nameof(IsAlphaTextInputVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsColorChannelTextInputVisibleProperty = + AvaloniaProperty.Register( + nameof(IsColorChannelTextInputVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsColorPaletteVisibleProperty = + AvaloniaProperty.Register( + nameof(IsColorPaletteVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsColorPreviewVisibleProperty = + AvaloniaProperty.Register( + nameof(IsColorPreviewVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsColorSliderVisibleProperty = + AvaloniaProperty.Register( + nameof(IsColorSliderVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsColorSpectrumVisibleProperty = + AvaloniaProperty.Register( + nameof(IsColorSpectrumVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsHexInputVisibleProperty = + AvaloniaProperty.Register( + nameof(IsHexInputVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxHueProperty = + AvaloniaProperty.Register( + nameof(MaxHue), + 359); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxSaturationProperty = + AvaloniaProperty.Register( + nameof(MaxSaturation), + 100); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxValueProperty = + AvaloniaProperty.Register( + nameof(MaxValue), + 100); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinHueProperty = + AvaloniaProperty.Register( + nameof(MinHue), + 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinSaturationProperty = + AvaloniaProperty.Register( + nameof(MinSaturation), + 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinValueProperty = + AvaloniaProperty.Register( + nameof(MinValue), + 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ShowAccentColorsProperty = + AvaloniaProperty.Register( + nameof(ShowAccentColors), + true); + + /// + public Color Color + { + get => GetValue(ColorProperty); + set => SetValue(ColorProperty, value); + } + + /// + public ColorSpectrumComponents ColorSpectrumComponents + { + get => GetValue(ColorSpectrumComponentsProperty); + set => SetValue(ColorSpectrumComponentsProperty, value); + } + + /// + public ColorSpectrumShape ColorSpectrumShape + { + get => GetValue(ColorSpectrumShapeProperty); + set => SetValue(ColorSpectrumShapeProperty, value); + } + + /// + /// Gets the list of custom palette colors. + /// + public ObservableCollection CustomPaletteColors + { + get => _customPaletteColors; + } + + /// + /// Gets or sets the number of colors in each row (section) of the custom color palette. + /// Within a standard palette, rows are shades and columns are colors. + /// + public int CustomPaletteColumnCount + { + get => GetValue(CustomPaletteColumnCountProperty); + set => SetValue(CustomPaletteColumnCountProperty, value); + } + + /// + /// Gets or sets the custom color palette. + /// This will automatically set and + /// overwriting any existing values. + /// + public IColorPalette? CustomPalette + { + get => GetValue(CustomPaletteProperty); + set => SetValue(CustomPaletteProperty, value); + } + + /// + public HsvColor HsvColor + { + get => GetValue(HsvColorProperty); + set => SetValue(HsvColorProperty, value); + } + + public bool IsAlphaEnabled + { + get => GetValue(IsAlphaEnabledProperty); + set => SetValue(IsAlphaEnabledProperty, value); + } + + public bool IsAlphaSliderVisible + { + get => GetValue(IsAlphaSliderVisibleProperty); + set => SetValue(IsAlphaSliderVisibleProperty, value); + } + + public bool IsAlphaTextInputVisible + { + get => GetValue(IsAlphaTextInputVisibleProperty); + set => SetValue(IsAlphaTextInputVisibleProperty, value); + } + + public bool IsColorChannelTextInputVisible // TODO: Component + { + get => GetValue(IsColorChannelTextInputVisibleProperty); + set => SetValue(IsColorChannelTextInputVisibleProperty, value); + } + + /// + /// Gets or sets a value indicating whether the color palette is visible. + /// + public bool IsColorPaletteVisible + { + get => GetValue(IsColorPaletteVisibleProperty); + set => SetValue(IsColorPaletteVisibleProperty, value); + } + + public bool IsColorPreviewVisible + { + get => GetValue(IsColorPreviewVisibleProperty); + set => SetValue(IsColorPreviewVisibleProperty, value); + } + + public bool IsColorSliderVisible + { + get => GetValue(IsColorSliderVisibleProperty); + set => SetValue(IsColorSliderVisibleProperty, value); + } + + public bool IsColorSpectrumVisible + { + get => GetValue(IsColorSpectrumVisibleProperty); + set => SetValue(IsColorSpectrumVisibleProperty, value); + } + + public bool IsHexInputVisible + { + get => GetValue(IsHexInputVisibleProperty); + set => SetValue(IsHexInputVisibleProperty, value); + } + + /// + public int MaxHue + { + get => GetValue(MaxHueProperty); + set => SetValue(MaxHueProperty, value); + } + + /// + public int MaxSaturation + { + get => GetValue(MaxSaturationProperty); + set => SetValue(MaxSaturationProperty, value); + } + + /// + public int MaxValue + { + get => GetValue(MaxValueProperty); + set => SetValue(MaxValueProperty, value); + } + + /// + public int MinHue + { + get => GetValue(MinHueProperty); + set => SetValue(MinHueProperty, value); + } + + /// + public int MinSaturation + { + get => GetValue(MinSaturationProperty); + set => SetValue(MinSaturationProperty, value); + } + + /// + public int MinValue + { + get => GetValue(MinValueProperty); + set => SetValue(MinValueProperty, value); + } + + /// + public bool ShowAccentColors + { + get => GetValue(ShowAccentColorsProperty); + set => SetValue(ShowAccentColorsProperty, value); + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs new file mode 100644 index 0000000000..3aff4614a5 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls.Primitives; +using Avalonia.Media; + +namespace Avalonia.Controls +{ + /// + /// Presents a color for user editing using a spectrum, palette and component sliders. + /// + public partial class ColorView : TemplatedControl + { + private ObservableCollection _customPaletteColors = new ObservableCollection(); + + + } +} From d3bad7bd1d5612901ff0f3f74080ca5349091228 Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 20 May 2022 00:01:55 -0400 Subject: [PATCH 019/171] Add ColorViewTab enum --- .../ColorView/ColorViewTab.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs new file mode 100644 index 0000000000..d8c7d5163c --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs @@ -0,0 +1,23 @@ +namespace Avalonia.Controls +{ + /// + /// Defines a specific tab (subview) within the . + /// + public enum ColorViewTab + { + /// + /// The components view with sliders and numeric input boxes. + /// + Components, + + /// + /// The color palette view with a grid of colors and shades. + /// + Palette, + + /// + /// The color spectrum view with a box/ring spectrum and sliders. + /// + Spectrum, + } +} From 77e36914d4c5bb49e12c8a79bb7d5de30a71f19c Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 22 May 2022 12:06:58 -0400 Subject: [PATCH 020/171] Add FluentColorPalette --- .../ColorPalette/FluentColorPalette.cs | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs new file mode 100644 index 0000000000..89400280a9 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs @@ -0,0 +1,142 @@ +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + /// + /// Implements the standard Windows 10 color palette. + /// + public class FluentColorPalette : IColorPalette + { + // Values were taken from the Settings App, Personalization > Colors which match with + // https://docs.microsoft.com/en-us/windows/uwp/whats-new/windows-docs-december-2017 + // + // The default ordering and grouping of colors was undesirable so was modified. + // Colors were transposed: the colors in rows within the Settings app became columns here. + // This is because columns in an IColorPalette generally should contain different shades of + // the same color. In the settings app this concept is somewhat loosely reversed. + // The first 'column' ordering, after being transposed, was then reversed so 'red' colors + // were near to each other. + // + // This new ordering most closely follows the Windows standard while: + // + // 1. Keeping colors in a 'spectrum' order + // 2. Keeping like colors next to each both in rows and columns + // (which is unique for the windows palette). + // For example, similar red colors are next to each other in both + // rows within the same column and rows within the column next to it. + // This follows a 'snake-like' pattern as illustrated below. + // 3. A downside of this ordering is colors don't follow strict 'shades' + // as in other palettes. + // + // The colors will be displayed in the below pattern. + // This pattern follows a spectrum while keeping like-colors near to one + // another across both rows and columns. + // + // ┌Red───┐ ┌Blue──┐ ┌Gray──┐ + // │ │ │ │ │ | + // │ │ │ │ │ | + // Yellow └Violet┘ └Green─┘ Brown + + private static Color[,] colorChart = new Color[,] + { + { + // Ordering reversed for this section only + Color.FromArgb(255, 255, 67, 67), /* #FF4343 */ + Color.FromArgb(255, 209, 52, 56), /* #D13438 */ + Color.FromArgb(255, 239, 105, 80), /* #EF6950 */ + Color.FromArgb(255, 218, 59, 1), /* #DA3B01 */ + Color.FromArgb(255, 202, 80, 16), /* #CA5010 */ + Color.FromArgb(255, 247, 99, 12), /* #F7630C */ + Color.FromArgb(255, 255, 140, 0), /* #FF8C00 */ + Color.FromArgb(255, 255, 185, 0), /* #FFB900 */ + }, + { + Color.FromArgb(255, 231, 72, 86), /* #E74856 */ + Color.FromArgb(255, 232, 17, 35), /* #E81123 */ + Color.FromArgb(255, 234, 0, 94), /* #EA005E */ + Color.FromArgb(255, 195, 0, 82), /* #C30052 */ + Color.FromArgb(255, 227, 0, 140), /* #E3008C */ + Color.FromArgb(255, 191, 0, 119), /* #BF0077 */ + Color.FromArgb(255, 194, 57, 179), /* #C239B3 */ + Color.FromArgb(255, 154, 0, 137), /* #9A0089 */ + }, + { + Color.FromArgb(255, 0, 120, 215), /* #0078D7 */ + Color.FromArgb(255, 0, 99, 177), /* #0063B1 */ + Color.FromArgb(255, 142, 140, 216), /* #8E8CD8 */ + Color.FromArgb(255, 107, 105, 214), /* #6B69D6 */ + Color.FromArgb(255, 135, 100, 184), /* #8764B8 */ + Color.FromArgb(255, 116, 77, 169), /* #744DA9 */ + Color.FromArgb(255, 177, 70, 194), /* #B146C2 */ + Color.FromArgb(255, 136, 23, 152), /* #881798 */ + }, + { + Color.FromArgb(255, 0, 153, 188), /* #0099BC */ + Color.FromArgb(255, 45, 125, 154), /* #2D7D9A */ + Color.FromArgb(255, 0, 183, 195), /* #00B7C3 */ + Color.FromArgb(255, 3, 131, 135), /* #038387 */ + Color.FromArgb(255, 0, 178, 148), /* #00B294 */ + Color.FromArgb(255, 1, 133, 116), /* #018574 */ + Color.FromArgb(255, 0, 204, 106), /* #00CC6A */ + Color.FromArgb(255, 16, 137, 62), /* #10893E */ + }, + { + Color.FromArgb(255, 122, 117, 116), /* #7A7574 */ + Color.FromArgb(255, 93, 90, 80), /* #5D5A58 */ + Color.FromArgb(255, 104, 118, 138), /* #68768A */ + Color.FromArgb(255, 81, 92, 107), /* #515C6B */ + Color.FromArgb(255, 86, 124, 115), /* #567C73 */ + Color.FromArgb(255, 72, 104, 96), /* #486860 */ + Color.FromArgb(255, 73, 130, 5), /* #498205 */ + Color.FromArgb(255, 16, 124, 16), /* #107C10 */ + }, + { + Color.FromArgb(255, 118, 118, 118), /* #767676 */ + Color.FromArgb(255, 76, 74, 72), /* #4C4A48 */ + Color.FromArgb(255, 105, 121, 126), /* #69797E */ + Color.FromArgb(255, 74, 84, 89), /* #4A5459 */ + Color.FromArgb(255, 100, 124, 100), /* #647C64 */ + Color.FromArgb(255, 82, 94, 84), /* #525E54 */ + Color.FromArgb(255, 132, 117, 69), /* #847545 */ + Color.FromArgb(255, 126, 115, 95), /* #7E735F */ + } + }; + + /// + /// Gets the index of the default shade of colors in this palette. + /// This has little meaning in this palette as colors are not strictly separated by shade. + /// + public const int DefaultShadeIndex = 0; + + /// + /// Gets the total number of colors in this palette. + /// A color is not necessarily a single value and may be composed of several shades. + /// This has little meaning in this palette as colors are not strictly separated. + /// + /// + public int ColorCount + { + get => colorChart.GetLength(0); + } + + /// + /// Gets the total number of shades for each color in this palette. + /// Shades are usually a variation of the color lightening or darkening it. + /// This has little meaning in this palette as colors are not strictly separated by shade. + /// + /// + public int ShadeCount + { + get => colorChart.GetLength(1); + } + + /// + public Color GetColor(int colorIndex, int shadeIndex) + { + return colorChart[ + MathUtilities.Clamp(colorIndex, 0, colorChart.GetLength(0)), + MathUtilities.Clamp(shadeIndex, 0, colorChart.GetLength(1))]; + } + } +} From 33637f651a3456f7791dbe01f3e16c79fd0725d6 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 22 May 2022 12:07:09 -0400 Subject: [PATCH 021/171] Fix ColorToHexConverter --- .../Converters/ColorToHexConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs index 9b09073d9d..8d5f2332be 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs @@ -42,7 +42,7 @@ namespace Avalonia.Controls.Converters return AvaloniaProperty.UnsetValue; } - string hexColor = color.ToString(); + string hexColor = color.ToUint32().ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant(); if (includeSymbol == false) { From 2835767b42ddfc23d40e64c34ea0719312ab8586 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 22 May 2022 12:09:28 -0400 Subject: [PATCH 022/171] Add incomplete ColorView control template --- .../ControlCatalog/Pages/ColorPickerPage.xaml | 9 +- .../ColorView/ColorView.Properties.cs | 4 +- .../ColorView/ColorView.cs | 86 ++++++ .../ColorView/ColorViewTab.cs | 6 +- .../Themes/Fluent/ColorView.xaml | 259 ++++++++++++++++++ .../Themes/Fluent/Fluent.xaml | 3 + 6 files changed, 360 insertions(+), 7 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index c0c83d6a35..47a407821c 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -13,8 +13,11 @@ - - + + - diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index aa5dfb5fc4..eddbeaf112 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -292,7 +292,9 @@ namespace Avalonia.Controls set => SetValue(IsColorPreviewVisibleProperty, value); } - public bool IsColorSliderVisible + // IsColorComponentsVisible + + public bool IsColorSliderVisible // ColorSpectrumSlider { get => GetValue(IsColorSliderVisibleProperty); set => SetValue(IsColorSliderVisibleProperty, value); diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 3aff4614a5..76e3cfe3e1 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -14,8 +14,94 @@ namespace Avalonia.Controls /// public partial class ColorView : TemplatedControl { + /// + /// Event for when the selected color changes within the slider. + /// + public event EventHandler? ColorChanged; + + private bool disableUpdates = false; + private ObservableCollection _customPaletteColors = new ObservableCollection(); + /// + /// Initializes a new instance of the class. + /// + public ColorView() : base() + { + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + this.CustomPalette = new FluentColorPalette(); + + base.OnApplyTemplate(e); + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (disableUpdates) + { + base.OnPropertyChanged(change); + return; + } + + // Always keep the two color properties in sync + if (change.Property == ColorProperty) + { + disableUpdates = true; + + HsvColor = Color.ToHsv(); + + OnColorChanged(new ColorChangedEventArgs( + change.GetOldValue(), + change.GetNewValue())); + + disableUpdates = false; + } + else if (change.Property == HsvColorProperty) + { + disableUpdates = true; + + Color = HsvColor.ToRgb(); + + OnColorChanged(new ColorChangedEventArgs( + change.GetOldValue().ToRgb(), + change.GetNewValue().ToRgb())); + + disableUpdates = false; + } + else if (change.Property == CustomPaletteProperty) + { + IColorPalette? palette = CustomPalette; + + // Any custom palette change must be automatically synced with the + // bound properties controlling the palette grid + if (palette != null) + { + CustomPaletteColumnCount = palette.ColorCount; + CustomPaletteColors.Clear(); + + for (int shadeIndex = 0; shadeIndex < palette.ShadeCount; shadeIndex++) + { + for (int colorIndex = 0; colorIndex < palette.ColorCount; colorIndex++) + { + CustomPaletteColors.Add(palette.GetColor(colorIndex, shadeIndex)); + } + } + } + } + + base.OnPropertyChanged(change); + } + /// + /// Called before the event occurs. + /// + /// The defining old/new colors. + protected virtual void OnColorChanged(ColorChangedEventArgs e) + { + ColorChanged?.Invoke(this, e); + } } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs index d8c7d5163c..677cdb8674 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs @@ -6,17 +6,17 @@ public enum ColorViewTab { /// - /// The components view with sliders and numeric input boxes. + /// The components subview with sliders and numeric input boxes. /// Components, /// - /// The color palette view with a grid of colors and shades. + /// The color palette subview with a grid of selectable colors. /// Palette, /// - /// The color spectrum view with a box/ring spectrum and sliders. + /// The color spectrum subview with a box/ring spectrum and sliders. /// Spectrum, } diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml new file mode 100644 index 0000000000..9f4324594d --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -0,0 +1,259 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml index c25d79727f..c55766e07c 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml @@ -25,4 +25,7 @@ + + + From 197ab047a510c58b30a32216fb96c8cfd2239c3a Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 27 May 2022 22:45:56 -0400 Subject: [PATCH 023/171] Add braces --- src/Avalonia.Controls/Primitives/TemplatedControl.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index db029d38c0..7c8c2f882f 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -291,7 +291,9 @@ namespace Avalonia.Controls.Primitives // Existing code kinda expect to see a NameScope even if it's empty if (nameScope == null) + { nameScope = new NameScope(); + } var e = new TemplateAppliedEventArgs(nameScope); OnApplyTemplate(e); From 5ed084123540fae249cc1881a21e67dad42ad0c1 Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 27 May 2022 22:46:12 -0400 Subject: [PATCH 024/171] Fix index clamping --- .../ColorPalette/FluentColorPalette.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs index 89400280a9..b6f9a244b1 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs @@ -135,8 +135,8 @@ namespace Avalonia.Controls public Color GetColor(int colorIndex, int shadeIndex) { return colorChart[ - MathUtilities.Clamp(colorIndex, 0, colorChart.GetLength(0)), - MathUtilities.Clamp(shadeIndex, 0, colorChart.GetLength(1))]; + MathUtilities.Clamp(colorIndex, 0, colorChart.GetLength(0) - 1), + MathUtilities.Clamp(shadeIndex, 0, colorChart.GetLength(1) - 1)]; } } } From e5b18d0b9c263b65b9121a25e19b6103e21d1d07 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 01:25:59 -0400 Subject: [PATCH 025/171] Further work on ColorView --- .../Converters/ContrastBrushConverter.cs | 84 +++++++++ .../Themes/Fluent/ColorView.xaml | 165 +++++++++++++++++- 2 files changed, 246 insertions(+), 3 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs new file mode 100644 index 0000000000..574f23dfae --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs @@ -0,0 +1,84 @@ +using System; +using System.Globalization; +using Avalonia.Controls.Converters; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives.Converters +{ + /// + /// Gets a , either black or white, depending on the luminance of the supplied color. + /// A default color supplied in the converter parameter may be returned if alpha is below the set threshold. + /// + public class ContrastBrushConverter : IValueConverter + { + private ToColorConverter toColorConverter = new ToColorConverter(); + + /// + /// Gets or sets the alpha channel threshold below which a default color is used instead of black/white. + /// + public byte AlphaThreshold { get; set; } = 128; + + /// + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + Color comparisonColor; + Color? defaultColor = null; + + // Get the changing color to compare against + var convertedValue = toColorConverter.Convert(value, targetType, parameter, culture); + if (convertedValue is Color valueColor) + { + comparisonColor = valueColor; + } + else + { + // Invalid color value provided + return AvaloniaProperty.UnsetValue; + } + + // Get the default color when transparency is high + var convertedParameter = toColorConverter.Convert(parameter, targetType, parameter, culture); + if (convertedParameter is Color parameterColor) + { + defaultColor = parameterColor; + } + + if (comparisonColor.A < AlphaThreshold && + defaultColor.HasValue) + { + // If the transparency is less than the threshold, just use the default brush + // This can commonly be something like the TextControlForeground brush + return new SolidColorBrush(defaultColor.Value); + } + else + { + // Chose a white/black brush based on contrast to the base color + if (ColorHelper.GetRelativeLuminance(comparisonColor) <= 0.5) + { + // Dark color, return light for contrast + return new SolidColorBrush(Colors.White); + } + else + { + // Bright color, return dark for contrast + return new SolidColorBrush(Colors.Black); + } + } + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + return AvaloniaProperty.UnsetValue; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 9f4324594d..5d3642a3b1 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -3,12 +3,15 @@ xmlns:converters="using:Avalonia.Controls.Converters" xmlns:primitives="using:Avalonia.Controls.Primitives" xmlns:pc="clr-namespace:Avalonia.Controls.Primitives.Converters;assembly=Avalonia.Controls.ColorPicker" + xmlns:globalization="clr-namespace:System.Globalization;assembly=mscorlib" x:CompileBindings="False"> + + + + + + + @@ -195,11 +230,11 @@ - + + + + + + + + + + + + + + + + From 53a08f126382f9a7eae4d5eda331efd1a7080bd5 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 12:04:26 -0400 Subject: [PATCH 026/171] Add new EnumToBooleanConverter --- .../Converters/EnumToBooleanConverter.cs | 57 +++++++++++++++++++ .../Converters/EnumValueEqualsConverter.cs | 12 +++- 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.Controls/Converters/EnumToBooleanConverter.cs diff --git a/src/Avalonia.Controls/Converters/EnumToBooleanConverter.cs b/src/Avalonia.Controls/Converters/EnumToBooleanConverter.cs new file mode 100644 index 0000000000..ba1c4cab3e --- /dev/null +++ b/src/Avalonia.Controls/Converters/EnumToBooleanConverter.cs @@ -0,0 +1,57 @@ +using System; +using System.Globalization; +using Avalonia.Data; +using Avalonia.Data.Converters; + +namespace Avalonia.Controls.Converters +{ + /// + /// Converter to convert an enum value to bool by comparing to the given parameter. + /// Both value and parameter must be of the same enum type. + /// + /// + /// This converter is useful to enable binding of radio buttons with a selected enum value. + /// + public class EnumToBooleanConverter : IValueConverter + { + /// + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + if (value == null && + parameter == null) + { + return true; + } + else if (value == null || + parameter == null) + { + return false; + } + else + { + return value!.Equals(parameter); + } + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + if (value is bool boolValue) + { + return boolValue ? parameter : BindingOperations.DoNothing; + } + else + { + return BindingOperations.DoNothing; + } + } + } +} diff --git a/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs b/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs index 1a33a82ca4..abd0fe1dfd 100644 --- a/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs +++ b/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs @@ -10,7 +10,11 @@ namespace Avalonia.Controls.Converters public class EnumValueEqualsConverter : IValueConverter { /// - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) { // Note: Unlike string comparisons, null/empty is not supported // Both 'value' and 'parameter' must exist and if both are missing they are not considered equal @@ -46,7 +50,11 @@ namespace Avalonia.Controls.Converters } /// - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) { throw new System.NotImplementedException(); } From 13b82a0d1d4f70eba999e6e29ae075aed3b6cd1b Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 12:08:16 -0400 Subject: [PATCH 027/171] Implement ColorModel switching in ColorView --- .../ColorSlider/ColorSlider.cs | 14 ++ .../ColorView/ColorView.Properties.cs | 20 +- .../Themes/Fluent/ColorView.xaml | 172 +++++++++++++----- 3 files changed, 163 insertions(+), 43 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 3c38c6ed1b..78a796e93a 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -332,6 +332,20 @@ namespace Avalonia.Controls.Primitives disableUpdates = false; } + else if (change.Property == ColorModelProperty) + { + disableUpdates = true; + + if (IsAutoUpdatingEnabled) + { + SetColorToSliderValues(); + UpdateBackground(); + } + + UpdatePseudoClasses(); + + disableUpdates = false; + } else if (change.Property == HsvColorProperty) { disableUpdates = true; diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index eddbeaf112..c56811b8a3 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -7,7 +7,6 @@ namespace Avalonia.Controls { public partial class ColorView { - // SelectedColorModel ActiveColorModel? // SelectedTab /// @@ -19,6 +18,14 @@ namespace Avalonia.Controls Colors.White, defaultBindingMode: BindingMode.TwoWay); + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorModelProperty = + AvaloniaProperty.Register( + nameof(ColorModel), + ColorModel.Rgba); + /// /// Defines the property. /// @@ -203,6 +210,17 @@ namespace Avalonia.Controls set => SetValue(ColorProperty, value); } + /// + /// + /// This property is only applicable to the components tab. + /// The spectrum tab must always be in HSV and the palette tab is pre-defined colors. + /// + public ColorModel ColorModel + { + get => GetValue(ColorModelProperty); + set => SetValue(ColorModelProperty, value); + } + /// public ColorSpectrumComponents ColorSpectrumComponents { diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 5d3642a3b1..b037d14956 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -1,5 +1,6 @@  + @@ -156,6 +158,11 @@ + + + + + + + + + + + + + + + + + + + + Grid.Column="2"> @@ -260,12 +333,17 @@ BorderThickness="1,1,0,1" CornerRadius="4,0,0,4" VerticalAlignment="Center"> - + + + + - + + + + - + + + + Date: Sat, 28 May 2022 12:08:28 -0400 Subject: [PATCH 028/171] Improve comments --- .../Converters/AccentColorConverter.cs | 4 +++- .../Converters/ContrastBrushConverter.cs | 3 +++ .../Converters/ThirdComponentConverter.cs | 4 +++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs index 4d05222e31..2c8e09adc9 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs @@ -7,8 +7,10 @@ namespace Avalonia.Controls.Primitives.Converters { /// /// Creates an accent color for a given base color value and step parameter. - /// This is a highly-specialized converter for the color picker. /// + /// + /// This is a highly-specialized converter for the color picker. + /// public class AccentColorConverter : IValueConverter { /// diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs index 574f23dfae..8b66b1a4e5 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs @@ -10,6 +10,9 @@ namespace Avalonia.Controls.Primitives.Converters /// Gets a , either black or white, depending on the luminance of the supplied color. /// A default color supplied in the converter parameter may be returned if alpha is below the set threshold. /// + /// + /// This is a highly-specialized converter for the color picker. + /// public class ContrastBrushConverter : IValueConverter { private ToColorConverter toColorConverter = new ToColorConverter(); diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs index 220a993f99..11e33c74f0 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs @@ -7,8 +7,10 @@ namespace Avalonia.Controls.Primitives.Converters /// /// Gets the third corresponding with a given /// that represents the other two components. - /// This is a highly-specialized converter for the color picker. /// + /// + /// This is a highly-specialized converter for the color picker. + /// public class ThirdComponentConverter : IValueConverter { /// From 4a267b69d61dbe8d784a3c7046c3af652632b9c8 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 12:15:04 -0400 Subject: [PATCH 029/171] Set MaxHue of ColorSlider to the same value as ColorSpectrum --- .../ColorSlider/ColorSlider.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 78a796e93a..4c7df0fda7 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -20,7 +20,15 @@ namespace Avalonia.Controls.Primitives /// public event EventHandler? ColorChanged; - private const double MaxHue = 359.99999999999999999; // 17 decimal places + /// + /// Defines the maximum hue component value + /// (other components are always 0..100 or 0.255). + /// + /// + /// This should match the default property. + /// + private const double MaxHue = 359; + private bool disableUpdates = false; /// From 36f85325bc7242487f20eeb3fb2fe3142aa0872a Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 12:28:23 -0400 Subject: [PATCH 030/171] Remove EnumValueEqualsConverter replaced by EnumToBooleanConverter --- .../Themes/Default/ColorSpectrum.xaml | 15 ++--- .../Themes/Fluent/ColorSpectrum.xaml | 15 ++--- .../Converters/EnumValueEqualsConverter.cs | 62 ------------------- 3 files changed, 16 insertions(+), 76 deletions(-) delete mode 100644 src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml index 78e6da8aa3..891e040e9f 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml @@ -1,10 +1,11 @@  - + @@ -24,26 +25,26 @@ IsHitTestVisible="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" - IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Box'}" + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}" RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}" RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml index ac8e2a9c06..779f228b97 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml @@ -1,10 +1,11 @@  - + @@ -24,26 +25,26 @@ IsHitTestVisible="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" - IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Box'}" + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}" RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}" RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> diff --git a/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs b/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs deleted file mode 100644 index abd0fe1dfd..0000000000 --- a/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Globalization; -using Avalonia.Data.Converters; - -namespace Avalonia.Controls.Converters -{ - /// - /// Converter that checks if an enum value is equal to the given parameter enum value. - /// - public class EnumValueEqualsConverter : IValueConverter - { - /// - public object? Convert( - object? value, - Type targetType, - object? parameter, - CultureInfo culture) - { - // Note: Unlike string comparisons, null/empty is not supported - // Both 'value' and 'parameter' must exist and if both are missing they are not considered equal - if (value != null && - parameter != null) - { - Type type = value.GetType(); - - if (type.IsEnum) - { - var valueStr = value?.ToString(); - var paramStr = parameter?.ToString(); - - if (string.Equals(valueStr, paramStr, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - /* - // TODO: When .net Standard 2.0 is no longer supported the code can be changed to below - // This is a little more type safe - if (type.IsEnum && - Enum.TryParse(type, value?.ToString(), true, out object? valueEnum) && - Enum.TryParse(type, parameter?.ToString(), true, out object? paramEnum)) - { - return valueEnum == paramEnum; - } - */ - } - - return false; - } - - /// - public object? ConvertBack( - object? value, - Type targetType, - object? parameter, - CultureInfo culture) - { - throw new System.NotImplementedException(); - } - } -} From 043b56c7da836dc6e8976c91957ce82626ca5b26 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 13:46:40 -0400 Subject: [PATCH 031/171] Remove IsAutoUpdatingEnabled property from ColorSlider This was unused and is an unnecessary complexity --- .../ColorSlider/ColorSlider.Properties.cs | 21 ------------- .../ColorSlider/ColorSlider.cs | 31 ++++++------------- 2 files changed, 9 insertions(+), 43 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs index 31bd296288..b1be794794 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs @@ -48,14 +48,6 @@ namespace Avalonia.Controls.Primitives nameof(IsAlphaMaxForced), true); - /// - /// Defines the property. - /// - public static readonly StyledProperty IsAutoUpdatingEnabledProperty = - AvaloniaProperty.Register( - nameof(IsAutoUpdatingEnabled), - true); - /// /// Defines the property. /// @@ -119,19 +111,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(IsAlphaMaxForcedProperty, value); } - /// - /// Gets or sets a value indicating whether automatic background and foreground updates will be - /// calculated when the set color changes. - /// - /// - /// This can be disabled for performance reasons when working with multiple sliders. - /// - public bool IsAutoUpdatingEnabled - { - get => GetValue(IsAutoUpdatingEnabledProperty); - set => SetValue(IsAutoUpdatingEnabledProperty, value); - } - /// /// Gets or sets a value indicating whether the saturation and value components are always forced to maximum values /// when using the HSVA color model. Only component values other than will be changed. diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 4c7df0fda7..641516c474 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -327,13 +327,10 @@ namespace Avalonia.Controls.Primitives HsvColor = Color.ToHsv(); - if (IsAutoUpdatingEnabled) - { - SetColorToSliderValues(); - UpdateBackground(); - } - + SetColorToSliderValues(); + UpdateBackground(); UpdatePseudoClasses(); + OnColorChanged(new ColorChangedEventArgs( change.GetOldValue(), change.GetNewValue())); @@ -344,12 +341,8 @@ namespace Avalonia.Controls.Primitives { disableUpdates = true; - if (IsAutoUpdatingEnabled) - { - SetColorToSliderValues(); - UpdateBackground(); - } - + SetColorToSliderValues(); + UpdateBackground(); UpdatePseudoClasses(); disableUpdates = false; @@ -360,13 +353,10 @@ namespace Avalonia.Controls.Primitives Color = HsvColor.ToRgb(); - if (IsAutoUpdatingEnabled) - { - SetColorToSliderValues(); - UpdateBackground(); - } - + SetColorToSliderValues(); + UpdateBackground(); UpdatePseudoClasses(); + OnColorChanged(new ColorChangedEventArgs( change.GetOldValue().ToRgb(), change.GetNewValue().ToRgb())); @@ -375,10 +365,7 @@ namespace Avalonia.Controls.Primitives } else if (change.Property == BoundsProperty) { - if (IsAutoUpdatingEnabled) - { - UpdateBackground(); - } + UpdateBackground(); } else if (change.Property == ValueProperty || change.Property == MinimumProperty || From 8ef440ceda8237a746c409a3eaa9e230b820cdb9 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 14:24:00 -0400 Subject: [PATCH 032/171] Finish ColorView design/style --- .../Themes/Fluent/ColorView.xaml | 215 +++++++++++------- 1 file changed, 136 insertions(+), 79 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index b037d14956..09feeb38f9 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -14,35 +14,125 @@ + 48 + + + M3 2C3.27614 2 3.5 2.22386 3.5 2.5V5.5C3.5 5.77614 3.72386 6 4 6H16C16.2761 6 16.5 5.77614 + 16.5 5.5V2.5C16.5 2.22386 16.7239 2 17 2C17.2761 2 17.5 2.22386 17.5 2.5V5.5C17.5 6.32843 + 16.8284 7 16 7H15.809L12.2236 14.1708C12.0615 14.4951 11.7914 14.7431 11.4695 + 14.8802C11.4905 15.0808 11.5 15.2891 11.5 15.5C11.5 16.0818 11.4278 16.6623 11.2268 + 17.1165C11.019 17.5862 10.6266 18 10 18C9.37343 18 8.98105 17.5862 8.77323 17.1165C8.57222 + 16.6623 8.5 16.0818 8.5 15.5C8.5 15.2891 8.50952 15.0808 8.53051 14.8802C8.20863 14.7431 + 7.93851 14.4951 7.77639 14.1708L4.19098 7H4C3.17157 7 2.5 6.32843 2.5 5.5V2.5C2.5 2.22386 + 2.72386 2 3 2ZM9.11803 14H10.882C11.0714 14 11.2445 13.893 11.3292 13.7236L14.691 + 7H5.30902L8.67082 13.7236C8.75552 13.893 8.92865 14 9.11803 14ZM9.52346 15C9.50787 15.1549 + 9.5 15.3225 9.5 15.5C9.5 16.0228 9.56841 16.4423 9.6877 16.7119C9.8002 16.9661 9.90782 17 + 10 17C10.0922 17 10.1998 16.9661 10.3123 16.7119C10.4316 16.4423 10.5 16.0228 10.5 + 15.5C10.5 15.3225 10.4921 15.1549 10.4765 15H9.52346Z + + + + M9.75003 6.5C10.1642 6.5 10.5 6.16421 10.5 5.75C10.5 5.33579 10.1642 5 9.75003 5C9.33582 + 5 9.00003 5.33579 9.00003 5.75C9.00003 6.16421 9.33582 6.5 9.75003 6.5ZM12.75 7.5C13.1642 + 7.5 13.5 7.16421 13.5 6.75C13.5 6.33579 13.1642 6 12.75 6C12.3358 6 12 6.33579 12 6.75C12 + 7.16421 12.3358 7.5 12.75 7.5ZM15.25 9C15.25 9.41421 14.9142 9.75 14.5 9.75C14.0858 9.75 + 13.75 9.41421 13.75 9C13.75 8.58579 14.0858 8.25 14.5 8.25C14.9142 8.25 15.25 8.58579 + 15.25 9ZM14.5 12.75C14.9142 12.75 15.25 12.4142 15.25 12C15.25 11.5858 14.9142 11.25 14.5 + 11.25C14.0858 11.25 13.75 11.5858 13.75 12C13.75 12.4142 14.0858 12.75 14.5 12.75ZM13.25 + 14C13.25 14.4142 12.9142 14.75 12.5 14.75C12.0858 14.75 11.75 14.4142 11.75 14C11.75 + 13.5858 12.0858 13.25 12.5 13.25C12.9142 13.25 13.25 13.5858 13.25 14ZM13.6972 + 2.99169C10.9426 1.57663 8.1432 1.7124 5.77007 3.16636C4.55909 3.9083 3.25331 5.46925 + 2.51605 7.05899C2.14542 7.85816 1.89915 8.70492 1.90238 9.49318C1.90566 10.2941 2.16983 + 11.0587 2.84039 11.6053C3.45058 12.1026 3.98165 12.353 4.49574 12.3784C5.01375 12.404 + 5.41804 12.1942 5.73429 12.0076C5.80382 11.9666 5.86891 11.927 5.93113 11.8892C6.17332 + 11.7421 6.37205 11.6214 6.62049 11.5426C6.90191 11.4534 7.2582 11.4205 7.77579 + 11.5787C7.96661 11.637 8.08161 11.7235 8.16212 11.8229C8.24792 11.9289 8.31662 12.0774 + 8.36788 12.2886C8.41955 12.5016 8.44767 12.7527 8.46868 13.0491C8.47651 13.1594 8.48379 + 13.2855 8.49142 13.4176C8.50252 13.6098 8.51437 13.8149 8.52974 14.0037C8.58435 14.6744 + 8.69971 15.4401 9.10362 16.1357C9.51764 16.8488 10.2047 17.439 11.307 17.8158C12.9093 + 18.3636 14.3731 17.9191 15.5126 17.0169C16.6391 16.125 17.4691 14.7761 17.8842 + 13.4272C19.1991 9.15377 17.6728 5.03394 13.6972 2.99169ZM6.29249 4.01905C8.35686 2.75426 + 10.7844 2.61959 13.2403 3.88119C16.7473 5.68275 18.1135 9.28161 16.9284 13.1332C16.5624 + 14.3227 15.8338 15.4871 14.8919 16.2329C13.963 16.9684 12.8486 17.286 11.6305 + 16.8696C10.7269 16.5607 10.2467 16.1129 9.96842 15.6336C9.68001 15.1369 9.57799 14.5556 + 9.52644 13.9225C9.51101 13.733 9.50132 13.5621 9.49147 13.3884C9.48399 13.2564 9.47642 + 13.1229 9.46618 12.9783C9.44424 12.669 9.41175 12.3499 9.33968 12.0529C9.26719 11.7541 + 9.14902 11.4527 8.93935 11.1937C8.72439 10.9282 8.43532 10.7346 8.06801 10.6223C7.36648 + 10.408 6.80266 10.4359 6.31839 10.5893C5.94331 10.7082 5.62016 10.9061 5.37179 + 11.0582C5.31992 11.0899 5.2713 11.1197 5.22616 11.1463C4.94094 11.3146 4.75357 11.39 + 4.54514 11.3796C4.33279 11.3691 4.00262 11.2625 3.47218 10.8301C3.0866 10.5158 2.90473 + 10.0668 2.90237 9.48908C2.89995 8.89865 3.08843 8.20165 3.42324 7.47971C4.09686 6.0272 + 5.28471 4.63649 6.29249 4.01905Z + + + + M14.95 5C14.7184 3.85888 13.7095 3 12.5 3C11.2905 3 10.2816 3.85888 10.05 5H2.5C2.22386 + 5 2 5.22386 2 5.5C2 5.77614 2.22386 6 2.5 6H10.05C10.2816 7.14112 11.2905 8 12.5 8C13.7297 + 8 14.752 7.11217 14.961 5.94254C14.9575 5.96177 14.9539 5.98093 14.95 6H17.5C17.7761 6 18 + 5.77614 18 5.5C18 5.22386 17.7761 5 17.5 5H14.95ZM12.5 7C11.6716 7 11 6.32843 11 5.5C11 + 4.67157 11.6716 4 12.5 4C13.3284 4 14 4.67157 14 5.5C14 6.32843 13.3284 7 12.5 7ZM9.94999 + 14C9.71836 12.8589 8.70948 12 7.5 12C6.29052 12 5.28164 12.8589 5.05001 14H2.5C2.22386 + 14 2 14.2239 2 14.5C2 14.7761 2.22386 15 2.5 15H5.05001C5.28164 16.1411 6.29052 17 7.5 + 17C8.70948 17 9.71836 16.1411 9.94999 15H17.5C17.7761 15 18 14.7761 18 14.5C18 14.2239 + 17.7761 14 17.5 14H9.94999ZM7.5 16C6.67157 16 6 15.3284 6 14.5C6 13.6716 6.67157 13 7.5 + 13C8.32843 13 9 13.6716 9 14.5C9 15.3284 8.32843 16 7.5 16Z + + - - + + + + RowDefinitions="Auto,*" + Margin="12"> - - + + + + SelectedItem="{Binding Color, ElementName=ColorSpectrum}" + UseLayoutRounding="False" + Margin="12"> @@ -281,17 +343,11 @@ @@ -497,7 +553,8 @@ + HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" + Margin="12,0,12,12" /> From 12a8ecb9239f315767448bc5d434b7652b7987f8 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 15:17:40 -0400 Subject: [PATCH 033/171] Implement the hex input TextBox in ColorView --- .../ColorView/ColorView.cs | 85 +++++++++++++++++-- .../Themes/Fluent/ColorView.xaml | 19 ++--- 2 files changed, 88 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 76e3cfe3e1..0363d9c182 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -1,9 +1,8 @@ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Globalization; +using Avalonia.Controls.Converters; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Media; @@ -12,6 +11,7 @@ namespace Avalonia.Controls /// /// Presents a color for user editing using a spectrum, palette and component sliders. /// + [TemplatePart("PART_HexTextBox", typeof(TextBox))] public partial class ColorView : TemplatedControl { /// @@ -19,20 +19,70 @@ namespace Avalonia.Controls /// public event EventHandler? ColorChanged; - private bool disableUpdates = false; + // XAML template parts + private TextBox? _hexTextBox; private ObservableCollection _customPaletteColors = new ObservableCollection(); + private ColorToHexConverter colorToHexConverter = new ColorToHexConverter(); + private bool disableUpdates = false; /// /// Initializes a new instance of the class. /// public ColorView() : base() { + this.CustomPalette = new FluentColorPalette(); + } + + /// + /// Gets the value of the hex TextBox and sets it as the current . + /// If invalid, the TextBox hex text will revert back to the last valid color. + /// + private void GetColorFromHexTextBox() + { + if (_hexTextBox != null) + { + var convertedColor = colorToHexConverter.ConvertBack(_hexTextBox.Text, typeof(Color), null, CultureInfo.CurrentCulture); + + if (convertedColor is Color color) + { + Color = color; + } + + // Re-apply the hex value + // This ensure the hex color value is always valid and formatted correctly + _hexTextBox.Text = colorToHexConverter.Convert(Color, typeof(string), null, CultureInfo.CurrentCulture) as string; + } + } + + /// + /// Sets the current to the hex TextBox. + /// + private void SetColorToHexTextBox() + { + if (_hexTextBox != null) + { + _hexTextBox.Text = colorToHexConverter.Convert(Color, typeof(string), null, CultureInfo.CurrentCulture) as string; + } } + /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - this.CustomPalette = new FluentColorPalette(); + if (_hexTextBox != null) + { + _hexTextBox.KeyDown -= HexTextBox_KeyDown; + _hexTextBox.LostFocus -= HexTextBox_LostFocus; + } + + _hexTextBox = e.NameScope.Find("PART_HexTextBox"); + SetColorToHexTextBox(); + + if (_hexTextBox != null) + { + _hexTextBox.KeyDown += HexTextBox_KeyDown; + _hexTextBox.LostFocus += HexTextBox_LostFocus; + } base.OnApplyTemplate(e); } @@ -52,6 +102,7 @@ namespace Avalonia.Controls disableUpdates = true; HsvColor = Color.ToHsv(); + SetColorToHexTextBox(); OnColorChanged(new ColorChangedEventArgs( change.GetOldValue(), @@ -64,6 +115,7 @@ namespace Avalonia.Controls disableUpdates = true; Color = HsvColor.ToRgb(); + SetColorToHexTextBox(); OnColorChanged(new ColorChangedEventArgs( change.GetOldValue().ToRgb(), @@ -103,5 +155,26 @@ namespace Avalonia.Controls { ColorChanged?.Invoke(this, e); } + + /// + /// Event handler for when a key is pressed within the Hex RGB value TextBox. + /// This is used to trigger re-evaluation of the color based on the TextBox value. + /// + private void HexTextBox_KeyDown(object? sender, Input.KeyEventArgs e) + { + if (e.Key == Input.Key.Enter) + { + GetColorFromHexTextBox(); + } + } + + /// + /// Event handler for when the Hex RGB value TextBox looses focus. + /// This is used to trigger re-evaluation of the color based on the TextBox value. + /// + private void HexTextBox_LostFocus(object? sender, Interactivity.RoutedEventArgs e) + { + GetColorFromHexTextBox(); + } } } diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 09feeb38f9..9800b1b3a5 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -152,8 +152,7 @@ Grid.Row="0" Grid.RowSpan="2" Components="{TemplateBinding ColorSpectrumComponents}" - Color="{TemplateBinding Color}" - HsvColor="{TemplateBinding HsvColor}" + HsvColor="{Binding $parent[ColorView].HsvColor}" MinHue="{TemplateBinding MinHue}" MaxHue="{TemplateBinding MaxHue}" MinSaturation="{TemplateBinding MinSaturation}" @@ -188,7 +187,7 @@ @@ -369,12 +368,12 @@ HorizontalAlignment="Center" VerticalAlignment="Center" /> - + @@ -419,7 +418,7 @@ Orientation="Horizontal" ColorComponent="Component1" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" - HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" + HsvColor="{Binding $parent[ColorView].HsvColor}" Margin="12,0,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Center" /> @@ -462,7 +461,7 @@ Orientation="Horizontal" ColorComponent="Component2" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" - HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" + HsvColor="{Binding $parent[ColorView].HsvColor}" Margin="12,0,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Center" /> @@ -505,7 +504,7 @@ Orientation="Horizontal" ColorComponent="Component3" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" - HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" + HsvColor="{Binding $parent[ColorView].HsvColor}" Margin="12,0,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Center" /> @@ -543,7 +542,7 @@ Orientation="Horizontal" ColorComponent="Alpha" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" - HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" + HsvColor="{Binding $parent[ColorView].HsvColor}" Margin="12,0,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Center" /> @@ -553,7 +552,7 @@ From a5b3bb9cb6c80b9d83c38ea372c9a64cc663acb6 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 15:58:48 -0400 Subject: [PATCH 034/171] Implement ColorSlider component value rounding --- .../ColorSlider/ColorSlider.Properties.cs | 21 +++++++++++ .../ColorSlider/ColorSlider.cs | 35 +++++++++++++++++-- .../Themes/Fluent/ColorView.xaml | 12 +++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs index b1be794794..e2a34a7f90 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs @@ -48,6 +48,14 @@ namespace Avalonia.Controls.Primitives nameof(IsAlphaMaxForced), true); + /// + /// Defines the property. + /// + public static readonly StyledProperty IsRoundingEnabledProperty = + AvaloniaProperty.Register( + nameof(IsRoundingEnabled), + false); + /// /// Defines the property. /// @@ -111,6 +119,19 @@ namespace Avalonia.Controls.Primitives set => SetValue(IsAlphaMaxForcedProperty, value); } + /// + /// Gets or sets a value indicating whether rounding of color component values is enabled. + /// + /// + /// This is applicable for the HSV color model only. The struct uses double + /// values while the struct uses byte. Only double types need rounding. + /// + public bool IsRoundingEnabled + { + get => GetValue(IsRoundingEnabledProperty); + set => SetValue(IsRoundingEnabledProperty, value); + } + /// /// Gets or sets a value indicating whether the saturation and value components are always forced to maximum values /// when using the HSVA color model. Only component values other than will be changed. diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 641516c474..957e5e7b77 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -115,6 +115,21 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Rounds the component values of the given . + /// This is useful for user-display and to ensure a color matches user selection exactly. + /// + /// The to round component values for. + /// A new with rounded component values. + private HsvColor RoundComponentValues(HsvColor hsvColor) + { + return new HsvColor( + Math.Round(hsvColor.A, 2, MidpointRounding.AwayFromZero), + Math.Round(hsvColor.H, 0, MidpointRounding.AwayFromZero), + Math.Round(hsvColor.S, 2, MidpointRounding.AwayFromZero), + Math.Round(hsvColor.V, 2, MidpointRounding.AwayFromZero)); + } + /// /// Updates the slider property values by applying the current color. /// @@ -130,6 +145,11 @@ namespace Avalonia.Controls.Primitives if (ColorModel == ColorModel.Hsva) { + if (IsRoundingEnabled) + { + hsvColor = RoundComponentValues(hsvColor); + } + // Note: Components converted into a usable range for the user switch (component) { @@ -222,7 +242,7 @@ namespace Avalonia.Controls.Primitives } } - return (hsvColor.ToRgb(), hsvColor); + rgbColor = hsvColor.ToRgb(); } else { @@ -244,8 +264,15 @@ namespace Avalonia.Controls.Primitives break; } - return (rgbColor, rgbColor.ToHsv()); + hsvColor = rgbColor.ToHsv(); } + + if (IsRoundingEnabled) + { + hsvColor = RoundComponentValues(hsvColor); + } + + return (rgbColor, hsvColor); } /// @@ -363,6 +390,10 @@ namespace Avalonia.Controls.Primitives disableUpdates = false; } + else if (change.Property == IsRoundingEnabledProperty) + { + SetColorToSliderValues(); + } else if (change.Property == BoundsProperty) { UpdateBackground(); diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 9800b1b3a5..1edc2768a5 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -416,6 +416,9 @@ Grid.Column="2" Grid.Row="2" Orientation="Horizontal" + IsRoundingEnabled="True" + IsSnapToTickEnabled="True" + TickFrequency="1" ColorComponent="Component1" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" HsvColor="{Binding $parent[ColorView].HsvColor}" @@ -459,6 +462,9 @@ Grid.Column="2" Grid.Row="3" Orientation="Horizontal" + IsRoundingEnabled="True" + IsSnapToTickEnabled="True" + TickFrequency="1" ColorComponent="Component2" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" HsvColor="{Binding $parent[ColorView].HsvColor}" @@ -502,6 +508,9 @@ Grid.Column="2" Grid.Row="4" Orientation="Horizontal" + IsRoundingEnabled="True" + IsSnapToTickEnabled="True" + TickFrequency="1" ColorComponent="Component3" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" HsvColor="{Binding $parent[ColorView].HsvColor}" @@ -540,6 +549,9 @@ Grid.Column="2" Grid.Row="5" Orientation="Horizontal" + IsRoundingEnabled="True" + IsSnapToTickEnabled="True" + TickFrequency="1" ColorComponent="Alpha" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" HsvColor="{Binding $parent[ColorView].HsvColor}" From 0824c874fc7c5bbc8b98f758e47f5e6194f7b595 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 16:42:58 -0400 Subject: [PATCH 035/171] Add initial ColorPicker control --- .../ControlCatalog/Pages/ColorPickerPage.xaml | 5 +- .../ColorPicker/ColorPicker.cs | 15 ++++ .../Themes/Fluent/ColorPicker.xaml | 71 +++++++++++++++++++ .../Themes/Fluent/Fluent.xaml | 1 + 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs create mode 100644 src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index 47a407821c..eca52e796a 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -13,7 +13,10 @@ - + + diff --git a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs new file mode 100644 index 0000000000..cb84c77d20 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Controls +{ + /// + /// + /// + public class ColorPicker : ColorView + { + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml new file mode 100644 index 0000000000..3fa3ab5ead --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml index c55766e07c..03e8238a44 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml @@ -26,6 +26,7 @@ + From 5cd0ea68053394bb85f7db96c65e7203448dd1cf Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 30 May 2022 20:39:17 -0400 Subject: [PATCH 036/171] Complete initial ColorPicker --- .../ColorPicker/ColorPicker.cs | 3 +- .../Themes/Fluent/ColorPicker.xaml | 31 +++++++++++++------ .../Themes/Fluent/ColorView.xaml | 16 +++++----- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs index cb84c77d20..140a24d6a1 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs @@ -7,7 +7,8 @@ using System.Threading.Tasks; namespace Avalonia.Controls { /// - /// + /// Presents a color for user editing using a spectrum, palette and component sliders within a drop down. + /// Editing is available when the drop down flyout is opened; otherwise, only the preview color is shown. /// public class ColorPicker : ColorView { diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 3fa3ab5ead..a4f52f111c 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -2,10 +2,11 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:Avalonia.Controls" xmlns:converters="using:Avalonia.Controls.Converters" - x:CompileBindings="True"> + x:CompileBindings="False"> + - + @@ -150,7 +150,8 @@ HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" HorizontalAlignment="Center" VerticalAlignment="Stretch" - Margin="0,0,12,0" /> + Margin="0,0,12,0" + IsVisible="{TemplateBinding IsColorSpectrumSliderVisible}"/> - + @@ -254,7 +255,7 @@ - + @@ -590,7 +591,7 @@ From adfac7b69dbcdc79a676b04aaebe1db4d4d63b40 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 01:22:39 -0400 Subject: [PATCH 040/171] Implement tab selection validation and automatic width --- .../ColorView/ColorView.cs | 93 ++++++++++++++++++- .../Themes/Fluent/ColorView.xaml | 23 +++-- 2 files changed, 105 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 0363d9c182..96de734cc7 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Converters; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Media; +using Avalonia.Threading; namespace Avalonia.Controls { @@ -12,6 +13,7 @@ namespace Avalonia.Controls /// Presents a color for user editing using a spectrum, palette and component sliders. /// [TemplatePart("PART_HexTextBox", typeof(TextBox))] + [TemplatePart("PART_TabControl", typeof(TabControl))] public partial class ColorView : TemplatedControl { /// @@ -20,7 +22,8 @@ namespace Avalonia.Controls public event EventHandler? ColorChanged; // XAML template parts - private TextBox? _hexTextBox; + private TextBox? _hexTextBox; + private TabControl? _tabControl; private ObservableCollection _customPaletteColors = new ObservableCollection(); private ColorToHexConverter colorToHexConverter = new ColorToHexConverter(); @@ -31,7 +34,7 @@ namespace Avalonia.Controls /// public ColorView() : base() { - this.CustomPalette = new FluentColorPalette(); + CustomPalette = new FluentColorPalette(); } /// @@ -66,6 +69,77 @@ namespace Avalonia.Controls } } + /// + /// Validates the selected subview/tab taking into account the visibility of each subview/tab + /// as well as the current selection. + /// + private void ValidateSelectedTab() + { + if (_tabControl != null && + _tabControl.Items != null) + { + // Determine if any item is visible + bool isAnyItemVisible = false; + foreach (var item in _tabControl.Items) + { + if (item is Control control && + control.IsVisible) + { + isAnyItemVisible = true; + break; + } + } + + if (isAnyItemVisible) + { + object? selectedItem = null; + + if (_tabControl.SelectedItem == null && + _tabControl.ItemCount > 0) + { + // As a failsafe, forcefully select the first item + foreach (var item in _tabControl.Items) + { + selectedItem = item; + break; + } + } + else + { + selectedItem = _tabControl.SelectedItem; + } + + if (selectedItem is Control selectedControl && + selectedControl.IsVisible == false) + { + // Select the first visible item instead + foreach (var item in _tabControl.Items) + { + if (item is Control control && + control.IsVisible) + { + selectedItem = item; + break; + } + } + } + + _tabControl.SelectedItem = selectedItem; + _tabControl.IsVisible = true; + } + else + { + // Special case when all items are hidden + // If TabControl ever properly supports no selected item / + // all items hidden this can be removed + _tabControl.SelectedItem = null; + _tabControl.IsVisible = false; + } + } + + return; + } + /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { @@ -76,6 +150,8 @@ namespace Avalonia.Controls } _hexTextBox = e.NameScope.Find("PART_HexTextBox"); + _tabControl = e.NameScope.Find("PART_TabControl"); + SetColorToHexTextBox(); if (_hexTextBox != null) @@ -85,6 +161,7 @@ namespace Avalonia.Controls } base.OnApplyTemplate(e); + ValidateSelectedTab(); } /// @@ -143,6 +220,18 @@ namespace Avalonia.Controls } } } + else if (change.Property == IsColorComponentsVisibleProperty || + change.Property == IsColorPaletteVisibleProperty || + change.Property == IsColorSpectrumVisibleProperty) + { + // When the property changed notification is received here the visibility + // of individual tab items has not yet been updated though the bindings. + // Therefore, the validation is delayed until after bindings update. + Dispatcher.UIThread.Post(() => + { + ValidateSelectedTab(); + }, DispatcherPriority.Background); + } base.OnPropertyChanged(change); } diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index afc8682a66..420e3b2ae9 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -109,22 +109,29 @@ BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}" BorderThickness="0,1,0,0" />--> - 0,0,0,0 - - + @@ -181,8 +188,7 @@ - + @@ -257,8 +263,7 @@ - + From 55de1523c42ae438ef8997c0a72cc89b338ff2db Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 20:20:43 -0400 Subject: [PATCH 041/171] Support ignoring property changes in derived Color controls --- .../ColorPicker/ColorPicker.cs | 9 ++++++++ .../ColorSlider/ColorSlider.cs | 22 +++++++++---------- .../ColorView/ColorView.cs | 12 +++++----- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs index 140a24d6a1..d34a91d1bb 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs @@ -12,5 +12,14 @@ namespace Avalonia.Controls /// public class ColorPicker : ColorView { + /// + /// Initializes a new instance of the class. + /// + public ColorPicker() : base() + { + // Completely ignore property changes here + // The ColorView in the control template is responsible to manage this + base.ignorePropertyChanged = true; + } } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 957e5e7b77..a9ba5a20fa 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -29,7 +29,7 @@ namespace Avalonia.Controls.Primitives /// private const double MaxHue = 359; - private bool disableUpdates = false; + protected bool ignorePropertyChanged = false; /// /// Initializes a new instance of the class. @@ -135,7 +135,7 @@ namespace Avalonia.Controls.Primitives /// /// /// Warning: This will trigger property changed updates. - /// Consider using externally. + /// Consider using externally. /// private void SetColorToSliderValues() { @@ -341,7 +341,7 @@ namespace Avalonia.Controls.Primitives /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (disableUpdates) + if (ignorePropertyChanged) { base.OnPropertyChanged(change); return; @@ -350,7 +350,7 @@ namespace Avalonia.Controls.Primitives // Always keep the two color properties in sync if (change.Property == ColorProperty) { - disableUpdates = true; + ignorePropertyChanged = true; HsvColor = Color.ToHsv(); @@ -362,21 +362,21 @@ namespace Avalonia.Controls.Primitives change.GetOldValue(), change.GetNewValue())); - disableUpdates = false; + ignorePropertyChanged = false; } else if (change.Property == ColorModelProperty) { - disableUpdates = true; + ignorePropertyChanged = true; SetColorToSliderValues(); UpdateBackground(); UpdatePseudoClasses(); - disableUpdates = false; + ignorePropertyChanged = false; } else if (change.Property == HsvColorProperty) { - disableUpdates = true; + ignorePropertyChanged = true; Color = HsvColor.ToRgb(); @@ -388,7 +388,7 @@ namespace Avalonia.Controls.Primitives change.GetOldValue().ToRgb(), change.GetNewValue().ToRgb())); - disableUpdates = false; + ignorePropertyChanged = false; } else if (change.Property == IsRoundingEnabledProperty) { @@ -402,7 +402,7 @@ namespace Avalonia.Controls.Primitives change.Property == MinimumProperty || change.Property == MaximumProperty) { - disableUpdates = true; + ignorePropertyChanged = true; Color oldColor = Color; (var color, var hsvColor) = GetColorFromSliderValues(); @@ -421,7 +421,7 @@ namespace Avalonia.Controls.Primitives UpdatePseudoClasses(); OnColorChanged(new ColorChangedEventArgs(oldColor, Color)); - disableUpdates = false; + ignorePropertyChanged = false; } base.OnPropertyChanged(change); diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 96de734cc7..c19daf5f40 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -27,7 +27,7 @@ namespace Avalonia.Controls private ObservableCollection _customPaletteColors = new ObservableCollection(); private ColorToHexConverter colorToHexConverter = new ColorToHexConverter(); - private bool disableUpdates = false; + protected bool ignorePropertyChanged = false; /// /// Initializes a new instance of the class. @@ -167,7 +167,7 @@ namespace Avalonia.Controls /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (disableUpdates) + if (ignorePropertyChanged) { base.OnPropertyChanged(change); return; @@ -176,7 +176,7 @@ namespace Avalonia.Controls // Always keep the two color properties in sync if (change.Property == ColorProperty) { - disableUpdates = true; + ignorePropertyChanged = true; HsvColor = Color.ToHsv(); SetColorToHexTextBox(); @@ -185,11 +185,11 @@ namespace Avalonia.Controls change.GetOldValue(), change.GetNewValue())); - disableUpdates = false; + ignorePropertyChanged = false; } else if (change.Property == HsvColorProperty) { - disableUpdates = true; + ignorePropertyChanged = true; Color = HsvColor.ToRgb(); SetColorToHexTextBox(); @@ -198,7 +198,7 @@ namespace Avalonia.Controls change.GetOldValue().ToRgb(), change.GetNewValue().ToRgb())); - disableUpdates = false; + ignorePropertyChanged = false; } else if (change.Property == CustomPaletteProperty) { From 36225e9132719d209d727cd4a6f7ef42c5b7f054 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 20:21:06 -0400 Subject: [PATCH 042/171] Use more standard binding in ColorPicker This doesn't currently work for some reason --- .../Themes/Fluent/ColorPicker.xaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index b3424d2cc1..6e5228039d 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -36,7 +36,7 @@ HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="1,1,0,1" /> - Date: Thu, 2 Jun 2022 20:21:16 -0400 Subject: [PATCH 043/171] Small improvements --- src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index c19daf5f40..d0ee5f9acd 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -54,7 +54,7 @@ namespace Avalonia.Controls // Re-apply the hex value // This ensure the hex color value is always valid and formatted correctly - _hexTextBox.Text = colorToHexConverter.Convert(Color, typeof(string), null, CultureInfo.CurrentCulture) as string; + SetColorToHexTextBox(); } } @@ -225,7 +225,7 @@ namespace Avalonia.Controls change.Property == IsColorSpectrumVisibleProperty) { // When the property changed notification is received here the visibility - // of individual tab items has not yet been updated though the bindings. + // of individual tab items has not yet been updated through the bindings. // Therefore, the validation is delayed until after bindings update. Dispatcher.UIThread.Post(() => { From b8be7ba4cb59eb1c406d794d76a6ad74d1d21a3a Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 21:05:47 -0400 Subject: [PATCH 044/171] Implement SelectedTabIndex in ColorView --- .../ColorView/ColorView.Properties.cs | 18 +++++++++++++++++- .../ColorView/ColorView.cs | 16 +++++++++++++++- .../ColorView/ColorViewTab.cs | 15 +++++++++------ .../Themes/Fluent/ColorView.xaml | 3 ++- 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index a3267d88f2..a4897c99a2 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -8,7 +8,6 @@ namespace Avalonia.Controls /// public partial class ColorView { - // SelectedTabIndex // IsColorModelSelectorVisible // IsComponentSliderVisible @@ -198,6 +197,14 @@ namespace Avalonia.Controls nameof(MinValue), 0); + /// + /// Defines the property. + /// + public static readonly StyledProperty SelectedTabIndexProperty = + AvaloniaProperty.Register( + nameof(SelectedTabIndex), + (int)ColorViewTab.Spectrum); + /// /// Defines the property. /// @@ -413,6 +420,15 @@ namespace Avalonia.Controls set => SetValue(MinValueProperty, value); } + /// + /// Gets or sets the index of the selected subview/tab. + /// + public int SelectedTabIndex + { + get => GetValue(SelectedTabIndexProperty); + set => SetValue(SelectedTabIndexProperty, value); + } + /// public bool ShowAccentColors { diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index d0ee5f9acd..9809f1312b 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -73,7 +73,10 @@ namespace Avalonia.Controls /// Validates the selected subview/tab taking into account the visibility of each subview/tab /// as well as the current selection. /// - private void ValidateSelectedTab() + /// + /// Derived controls may re-implement this based on their default style / control template. + /// + protected virtual void ValidateSelectedTab() { if (_tabControl != null && _tabControl.Items != null) @@ -135,6 +138,8 @@ namespace Avalonia.Controls _tabControl.SelectedItem = null; _tabControl.IsVisible = false; } + + SelectedTabIndex = _tabControl.SelectedIndex; } return; @@ -232,6 +237,15 @@ namespace Avalonia.Controls ValidateSelectedTab(); }, DispatcherPriority.Background); } + else if (change.Property == SelectedTabIndexProperty) + { + // Again, it is necessary to wait for the SelectedTabIndex value to + // be applied to the TabControl through binding before validation occurs. + Dispatcher.UIThread.Post(() => + { + ValidateSelectedTab(); + }, DispatcherPriority.Background); + } base.OnPropertyChanged(change); } diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs index 534e833631..8e3433d819 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs @@ -1,23 +1,26 @@ namespace Avalonia.Controls { /// - /// Defines a specific subview (tab) within the . + /// Defines a specific subview/tab within the . /// + /// + /// This is indexed to match the default control template ordering. + /// public enum ColorViewTab { /// - /// The components subview with sliders and numeric input boxes. + /// The color spectrum subview with a box/ring spectrum and sliders. /// - Components, + Spectrum = 0, /// /// The color palette subview with a grid of selectable colors. /// - Palette, + Palette = 1, /// - /// The color spectrum subview with a box/ring spectrum and sliders. + /// The components subview with sliders and numeric input boxes. /// - Spectrum, + Components = 2, } } diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 420e3b2ae9..d6bf83ff44 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -112,7 +112,8 @@ + Width="350" + SelectedIndex="{Binding SelectedTabIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"> 0,0,0,0 From 10bc7b38cbb4795ef4d7a2ad8a32e13e4194d125 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 22:20:44 -0400 Subject: [PATCH 045/171] Rename 'EnumToBooleanConverter' to 'EnumToBoolConverter' Bool is shorter and is also slightly more commonly used in other converters. Bool matches the type as written in C# as well. --- .../Themes/Default/ColorSpectrum.xaml | 14 +++++++------- .../Themes/Fluent/ColorSpectrum.xaml | 14 +++++++------- .../Themes/Fluent/ColorView.xaml | 18 +++++++++--------- ...leanConverter.cs => EnumToBoolConverter.cs} | 13 ++++++------- 4 files changed, 29 insertions(+), 30 deletions(-) rename src/Avalonia.Controls/Converters/{EnumToBooleanConverter.cs => EnumToBoolConverter.cs} (82%) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml index 891e040e9f..c29f8f51e5 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml @@ -5,7 +5,7 @@ x:CompileBindings="True"> - + @@ -25,26 +25,26 @@ IsHitTestVisible="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" - IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}" + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}" RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}" RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml index 779f228b97..6dd7ddd373 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml @@ -5,7 +5,7 @@ x:CompileBindings="True"> - + @@ -25,26 +25,26 @@ IsHitTestVisible="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" - IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}" + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}" RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}" RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index d6bf83ff44..4b01de15a0 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -12,7 +12,7 @@ - + 48 @@ -351,12 +351,12 @@ Grid.Column="0" Content="RGB" CornerRadius="4,0,0,4" - IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static local:ColorModel.Rgba}, Mode=TwoWay}" /> + IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Rgba}, Mode=TwoWay}" /> + IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Hsva}, Mode=TwoWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Rgba}, Mode=OneWay}"/> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Hsva}, Mode=OneWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Rgba}, Mode=OneWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Hsva}, Mode=OneWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Rgba}, Mode=OneWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Hsva}, Mode=OneWay}" /> /// This converter is useful to enable binding of radio buttons with a selected enum value. /// - public class EnumToBooleanConverter : IValueConverter + public class EnumToBoolConverter : IValueConverter { /// public object? Convert( @@ -44,14 +44,13 @@ namespace Avalonia.Controls.Converters object? parameter, CultureInfo culture) { - if (value is bool boolValue) + if (value is bool boolValue && + boolValue == true) { - return boolValue ? parameter : BindingOperations.DoNothing; - } - else - { - return BindingOperations.DoNothing; + return parameter; } + + return BindingOperations.DoNothing; } } } From e0c936dbb4e2d48bc3367e0f13e3e2ac1d15002e Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 22:37:54 -0400 Subject: [PATCH 046/171] Add FlatColorPalette --- .../ColorPalette/FlatColorPalette.cs | 284 ++++++++++++++++++ .../ColorPalette/FluentColorPalette.cs | 6 - 2 files changed, 284 insertions(+), 6 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPalette/FlatColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/FlatColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalette/FlatColorPalette.cs new file mode 100644 index 0000000000..130d7e0edd --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPalette/FlatColorPalette.cs @@ -0,0 +1,284 @@ +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + /// + /// Implements a reduced flat design or flat UI color palette. + /// + /// + /// See: + /// - https://htmlcolorcodes.com/color-chart/ + /// - https://htmlcolorcodes.com/color-chart/flat-design-color-chart/ + /// - http://designmodo.github.io/Flat-UI/ + /// + /// The GitHub project is licensed as MIT: https://github.com/designmodo/Flat-UI. + /// + /// + public class FlatColorPalette : IColorPalette + { + // The full Flat UI color chart has 10 rows and 20 columns + // See: https://htmlcolorcodes.com/assets/downloads/flat-design-colors/flat-design-color-chart.png + // This is a reduced palette for usability + private static Color[,] colorChart = new Color[,] + { + // Pomegranate + { + Color.FromArgb(0xFF, 0xF9, 0xEB, 0xEA), + Color.FromArgb(0xFF, 0xE6, 0xB0, 0xAA), + Color.FromArgb(0xFF, 0xCD, 0x61, 0x55), + Color.FromArgb(0xFF, 0xA9, 0x32, 0x26), + Color.FromArgb(0xFF, 0x7B, 0x24, 0x1C), + }, + + // Amethyst + { + Color.FromArgb(0xFF, 0xF5, 0xEE, 0xF8), + Color.FromArgb(0xFF, 0xD7, 0xBD, 0xE2), + Color.FromArgb(0xFF, 0xAF, 0x7A, 0xC5), + Color.FromArgb(0xFF, 0x88, 0x4E, 0xA0), + Color.FromArgb(0xFF, 0x63, 0x39, 0x74), + }, + + // Belize Hole + { + Color.FromArgb(0xFF, 0xEA, 0xF2, 0xF8), + Color.FromArgb(0xFF, 0xA9, 0xCC, 0xE3), + Color.FromArgb(0xFF, 0x54, 0x99, 0xC7), + Color.FromArgb(0xFF, 0x24, 0x71, 0xA3), + Color.FromArgb(0xFF, 0x1A, 0x52, 0x76), + }, + + // Turquoise + { + Color.FromArgb(0xFF, 0xE8, 0xF8, 0xF5), + Color.FromArgb(0xFF, 0xA3, 0xE4, 0xD7), + Color.FromArgb(0xFF, 0x48, 0xC9, 0xB0), + Color.FromArgb(0xFF, 0x17, 0xA5, 0x89), + Color.FromArgb(0xFF, 0x11, 0x78, 0x64), + }, + + // Nephritis + { + Color.FromArgb(0xFF, 0xE9, 0xF7, 0xEF), + Color.FromArgb(0xFF, 0xA9, 0xDF, 0xBF), + Color.FromArgb(0xFF, 0x52, 0xBE, 0x80), + Color.FromArgb(0xFF, 0x22, 0x99, 0x54), + Color.FromArgb(0xFF, 0x19, 0x6F, 0x3D), + }, + + // Sunflower + { + Color.FromArgb(0xFF, 0xFE, 0xF9, 0xE7), + Color.FromArgb(0xFF, 0xF9, 0xE7, 0x9F), + Color.FromArgb(0xFF, 0xF4, 0xD0, 0x3F), + Color.FromArgb(0xFF, 0xD4, 0xAC, 0x0D), + Color.FromArgb(0xFF, 0x9A, 0x7D, 0x0A), + }, + + // Carrot + { + Color.FromArgb(0xFF, 0xFD, 0xF2, 0xE9), + Color.FromArgb(0xFF, 0xF5, 0xCB, 0xA7), + Color.FromArgb(0xFF, 0xEB, 0x98, 0x4E), + Color.FromArgb(0xFF, 0xCA, 0x6F, 0x1E), + Color.FromArgb(0xFF, 0x93, 0x51, 0x16), + }, + + // Clouds + { + Color.FromArgb(0xFF, 0xFD, 0xFE, 0xFE), + Color.FromArgb(0xFF, 0xF7, 0xF9, 0xF9), + Color.FromArgb(0xFF, 0xF0, 0xF3, 0xF4), + Color.FromArgb(0xFF, 0xD0, 0xD3, 0xD4), + Color.FromArgb(0xFF, 0x97, 0x9A, 0x9A), + }, + + // Concrete + { + Color.FromArgb(0xFF, 0xF4, 0xF6, 0xF6), + Color.FromArgb(0xFF, 0xD5, 0xDB, 0xDB), + Color.FromArgb(0xFF, 0xAA, 0xB7, 0xB8), + Color.FromArgb(0xFF, 0x83, 0x91, 0x92), + Color.FromArgb(0xFF, 0x5F, 0x6A, 0x6A), + }, + + // Wet Asphalt + { + Color.FromArgb(0xFF, 0xEB, 0xED, 0xEF), + Color.FromArgb(0xFF, 0xAE, 0xB6, 0xBF), + Color.FromArgb(0xFF, 0x5D, 0x6D, 0x7E), + Color.FromArgb(0xFF, 0x2E, 0x40, 0x53), + Color.FromArgb(0xFF, 0x21, 0x2F, 0x3C), + }, + }; + + /// + /// Gets the index of the default shade of colors in this palette. + /// + public const int DefaultShadeIndex = 2; + + /// + /// The index in the color palette of the 'Pomegranate' color. + /// This index can correspond to multiple color shades. + /// + public const int PomegranateIndex = 0; + + /// + /// The index in the color palette of the 'Amethyst' color. + /// This index can correspond to multiple color shades. + /// + public const int AmethystIndex = 1; + + /// + /// The index in the color palette of the 'BelizeHole' color. + /// This index can correspond to multiple color shades. + /// + public const int BelizeHoleIndex = 2; + + /// + /// The index in the color palette of the 'Turquoise' color. + /// This index can correspond to multiple color shades. + /// + public const int TurquoiseIndex = 3; + + /// + /// The index in the color palette of the 'Nephritis' color. + /// This index can correspond to multiple color shades. + /// + public const int NephritisIndex = 4; + + /// + /// The index in the color palette of the 'Sunflower' color. + /// This index can correspond to multiple color shades. + /// + public const int SunflowerIndex = 5; + + /// + /// The index in the color palette of the 'Carrot' color. + /// This index can correspond to multiple color shades. + /// + public const int CarrotIndex = 6; + + /// + /// The index in the color palette of the 'Clouds' color. + /// This index can correspond to multiple color shades. + /// + public const int CloudsIndex = 7; + + /// + /// The index in the color palette of the 'Concrete' color. + /// This index can correspond to multiple color shades. + /// + public const int ConcreteIndex = 8; + + /// + /// The index in the color palette of the 'WetAsphalt' color. + /// This index can correspond to multiple color shades. + /// + public const int WetAsphaltIndex = 9; + + /// + public int ColorCount + { + // Table is transposed compared to the reference chart + get => colorChart.GetLength(0); + } + + /// + public int ShadeCount + { + // Table is transposed compared to the reference chart + get => colorChart.GetLength(1); + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFC0392B. + /// + public static Color Pomegranate + { + get => colorChart[PomegranateIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF9B59B6. + /// + public static Color Amethyst + { + get => colorChart[AmethystIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF2980B9. + /// + public static Color BelizeHole + { + get => colorChart[BelizeHoleIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF1ABC9C. + /// + public static Color Turquoise + { + get => colorChart[TurquoiseIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF27AE60. + /// + public static Color Nephritis + { + get => colorChart[NephritisIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFF1C40F. + /// + public static Color Sunflower + { + get => colorChart[SunflowerIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFE67E22. + /// + public static Color Carrot + { + get => colorChart[CarrotIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFECF0F1. + /// + public static Color Clouds + { + get => colorChart[CloudsIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF95A5A6. + /// + public static Color Concrete + { + get => colorChart[ConcreteIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF34495E. + /// + public static Color WetAsphalt + { + get => colorChart[WetAsphaltIndex, DefaultShadeIndex]; + } + + /// + public Color GetColor(int colorIndex, int shadeIndex) + { + // Table is transposed compared to the reference chart + return colorChart[ + MathUtilities.Clamp(colorIndex, 0, colorChart.GetLength(0) - 1), + MathUtilities.Clamp(shadeIndex, 0, colorChart.GetLength(1) - 1)]; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs index b6f9a244b1..013e69ce20 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs @@ -103,12 +103,6 @@ namespace Avalonia.Controls } }; - /// - /// Gets the index of the default shade of colors in this palette. - /// This has little meaning in this palette as colors are not strictly separated by shade. - /// - public const int DefaultShadeIndex = 0; - /// /// Gets the total number of colors in this palette. /// A color is not necessarily a single value and may be composed of several shades. From fe2d51b111696298e35c3db629751006f8e5ceec Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 22:42:56 -0400 Subject: [PATCH 047/171] Add SixteenColorPalette --- .../ColorPalette/SixteenColorPalette.cs | 302 ++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPalette/SixteenColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/SixteenColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalette/SixteenColorPalette.cs new file mode 100644 index 0000000000..f3abfdfd7f --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPalette/SixteenColorPalette.cs @@ -0,0 +1,302 @@ +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + /// + /// Implements the standard sixteen color palette from the HTML 4.01 specification. + /// + /// + /// See https://en.wikipedia.org/wiki/Web_colors#HTML_color_names. + /// + public class SixteenColorPalette : IColorPalette + { + // The 16 standard colors from HTML and early Windows computers + // https://en.wikipedia.org/wiki/List_of_software_palettes + // https://en.wikipedia.org/wiki/Web_colors#HTML_color_names + private static Color[,] colorChart = new Color[,] + { + { + Colors.White, + Colors.Silver + }, + { + Colors.Gray, + Colors.Black + }, + { + Colors.Red, + Colors.Maroon + }, + { + Colors.Yellow, + Colors.Olive + }, + { + Colors.Lime, + Colors.Green + }, + { + Colors.Aqua, + Colors.Teal + }, + { + Colors.Blue, + Colors.Navy + }, + { + Colors.Fuchsia, + Colors.Purple + } + }; + + /// + /// Gets the index of the default shade of colors in this palette. + /// + public const int DefaultShadeIndex = 0; + + /// + /// The index in the color palette of the 'White' color. + /// This index can correspond to multiple color shades. + /// + public const int WhiteIndex = 0; + + /// + /// The index in the color palette of the 'Silver' color. + /// This index can correspond to multiple color shades. + /// + public const int SilverIndex = 1; + + /// + /// The index in the color palette of the 'Gray' color. + /// This index can correspond to multiple color shades. + /// + public const int GrayIndex = 2; + + /// + /// The index in the color palette of the 'Black' color. + /// This index can correspond to multiple color shades. + /// + public const int BlackIndex = 3; + + /// + /// The index in the color palette of the 'Red' color. + /// This index can correspond to multiple color shades. + /// + public const int RedIndex = 4; + + /// + /// The index in the color palette of the 'Maroon' color. + /// This index can correspond to multiple color shades. + /// + public const int MaroonIndex = 5; + + /// + /// The index in the color palette of the 'Yellow' color. + /// This index can correspond to multiple color shades. + /// + public const int YellowIndex = 6; + + /// + /// The index in the color palette of the 'Olive' color. + /// This index can correspond to multiple color shades. + /// + public const int OliveIndex = 7; + + /// + /// The index in the color palette of the 'Lime' color. + /// This index can correspond to multiple color shades. + /// + public const int LimeIndex = 8; + + /// + /// The index in the color palette of the 'Green' color. + /// This index can correspond to multiple color shades. + /// + public const int GreenIndex = 9; + + /// + /// The index in the color palette of the 'Aqua' color. + /// This index can correspond to multiple color shades. + /// + public const int AquaIndex = 10; + + /// + /// The index in the color palette of the 'Teal' color. + /// This index can correspond to multiple color shades. + /// + public const int TealIndex = 11; + + /// + /// The index in the color palette of the 'Blue' color. + /// This index can correspond to multiple color shades. + /// + public const int BlueIndex = 12; + + /// + /// The index in the color palette of the 'Navy' color. + /// This index can correspond to multiple color shades. + /// + public const int NavyIndex = 13; + + /// + /// The index in the color palette of the 'Fuchsia' color. + /// This index can correspond to multiple color shades. + /// + public const int FuchsiaIndex = 14; + + /// + /// The index in the color palette of the 'Purple' color. + /// This index can correspond to multiple color shades. + /// + public const int PurpleIndex = 15; + + /// + public int ColorCount + { + get => colorChart.GetLength(0); + } + + /// + public int ShadeCount + { + get => colorChart.GetLength(1); + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFFFFFFF. + /// + public static Color White + { + get => colorChart[WhiteIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFC0C0C0. + /// + public static Color Silver + { + get => colorChart[SilverIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF808080. + /// + public static Color Gray + { + get => colorChart[GrayIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF000000. + /// + public static Color Black + { + get => colorChart[BlackIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFFF0000. + /// + public static Color Red + { + get => colorChart[RedIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF800000. + /// + public static Color Maroon + { + get => colorChart[MaroonIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFFFFF00. + /// + public static Color Yellow + { + get => colorChart[YellowIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF808000. + /// + public static Color Olive + { + get => colorChart[OliveIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF00FF00. + /// + public static Color Lime + { + get => colorChart[LimeIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF008000. + /// + public static Color Green + { + get => colorChart[GreenIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF00FFFF. + /// + public static Color Aqua + { + get => colorChart[AquaIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF008080. + /// + public static Color Teal + { + get => colorChart[TealIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF0000FF. + /// + public static Color Blue + { + get => colorChart[BlueIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF000080. + /// + public static Color Navy + { + get => colorChart[NavyIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFFF00FF. + /// + public static Color Fuchsia + { + get => colorChart[FuchsiaIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF800080. + /// + public static Color Purple + { + get => colorChart[PurpleIndex, DefaultShadeIndex]; + } + + /// + public Color GetColor(int colorIndex, int shadeIndex) + { + return colorChart[ + MathUtilities.Clamp(colorIndex, 0, colorChart.GetLength(0) - 1), + MathUtilities.Clamp(shadeIndex, 0, colorChart.GetLength(1) - 1)]; + } + } +} From 5e8f0fb70119e88335a555c80082311c9ef26664 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 23:00:08 -0400 Subject: [PATCH 048/171] Add comment explaining rounding value selection --- src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs index 32a898ee71..7dc340ea16 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs @@ -64,6 +64,10 @@ namespace Avalonia.Controls.Primitives // It is also needlessly large as there are only ~140 known/named colors. // Therefore, rounding of the input color's component values is done to // reduce the color space into something more useful. + // + // The rounding value of 5 is specially chosen. + // It is a factor of 255 and therefore evenly divisible which improves + // the quality of the calculations. double rounding = 5; var roundedColor = new Color( 0xFF, From 4dcf13623d7e78698141f5f64742ec949e62afbb Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 23:02:18 -0400 Subject: [PATCH 049/171] Make ColorPalettes directory plural --- .../{ColorPalette => ColorPalettes}/FlatColorPalette.cs | 0 .../{ColorPalette => ColorPalettes}/FluentColorPalette.cs | 0 .../{ColorPalette => ColorPalettes}/IColorPalette.cs | 0 .../{ColorPalette => ColorPalettes}/SixteenColorPalette.cs | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename src/Avalonia.Controls.ColorPicker/{ColorPalette => ColorPalettes}/FlatColorPalette.cs (100%) rename src/Avalonia.Controls.ColorPicker/{ColorPalette => ColorPalettes}/FluentColorPalette.cs (100%) rename src/Avalonia.Controls.ColorPicker/{ColorPalette => ColorPalettes}/IColorPalette.cs (100%) rename src/Avalonia.Controls.ColorPicker/{ColorPalette => ColorPalettes}/SixteenColorPalette.cs (100%) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/FlatColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorPalette/FlatColorPalette.cs rename to src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/FluentColorPalette.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs rename to src/Avalonia.Controls.ColorPicker/ColorPalettes/FluentColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/IColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/IColorPalette.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorPalette/IColorPalette.cs rename to src/Avalonia.Controls.ColorPicker/ColorPalettes/IColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/SixteenColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/SixteenColorPalette.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorPalette/SixteenColorPalette.cs rename to src/Avalonia.Controls.ColorPicker/ColorPalettes/SixteenColorPalette.cs From 63d1bdec3c3b05964b4b3f158d3dfde6ddbc4bcf Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 23:08:24 -0400 Subject: [PATCH 050/171] Add resources to control accent color section size in ColorPreviewer --- .../Themes/Default/ColorPreviewer.xaml | 10 ++++++---- .../Themes/Fluent/ColorPreviewer.xaml | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml index 15e5ca1655..ac1531fe7a 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml @@ -9,6 +9,8 @@ + 80 + 40 - + - + - + + @@ -316,40 +316,40 @@ VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" /> - - + - + - + - + - + - From 58f9b443d9a40aff415d1021835794f003b5c025 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 27 Jun 2022 00:45:54 -0400 Subject: [PATCH 092/171] Implement IsAlphaEnabled using coercion --- .../ColorView/ColorView.Properties.cs | 16 +++-- .../ColorView/ColorView.cs | 66 +++++++++++++++++++ .../Themes/Fluent/ColorView.xaml | 18 ++++- 3 files changed, 92 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index c7652833bf..e1da427542 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -15,7 +15,8 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(Color), Colors.White, - defaultBindingMode: BindingMode.TwoWay); + defaultBindingMode: BindingMode.TwoWay, + coerce: CoerceColor) ; /// /// Defines the property. @@ -48,7 +49,8 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(HsvColor), Colors.White.ToHsv(), - defaultBindingMode: BindingMode.TwoWay); + defaultBindingMode: BindingMode.TwoWay, + coerce: CoerceHsvColor); /// /// Defines the property. @@ -267,7 +269,8 @@ namespace Avalonia.Controls /// /// Gets or sets a value indicating whether the alpha component is enabled. - /// When disabled (set to false) the alpha component will be fixed to maximum. + /// When disabled (set to false) the alpha component will be fixed to maximum and + /// editing controls hidden. /// public bool IsAlphaEnabled { @@ -277,7 +280,8 @@ namespace Avalonia.Controls /// /// Gets or sets a value indicating whether the alpha component editing controls - /// (both Slider and TextBox) are visible. + /// (Slider(s) and TextBox) are visible. When hidden, the existing alpha component + /// value is maintained. /// /// /// Note that also controls the alpha @@ -353,7 +357,7 @@ namespace Avalonia.Controls /// /// /// All color components are controlled by this property but alpha can also be - /// controlled with . + /// controlled with and . /// public bool IsComponentSliderVisible { @@ -366,7 +370,7 @@ namespace Avalonia.Controls /// /// /// All color components are controlled by this property but alpha can also be - /// controlled with . + /// controlled with and . /// public bool IsComponentTextInputVisible { diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 4f95b4acfe..8fb80980d5 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -237,6 +237,12 @@ namespace Avalonia.Controls PaletteColors = newPaletteColors; } } + else if (change.Property == IsAlphaEnabledProperty) + { + // Manually coerce the HsvColor value + // (Color will be coerced automatically if HsvColor changes) + HsvColor = OnCoerceHsvColor(HsvColor); + } else if (change.Property == IsColorComponentsVisibleProperty || change.Property == IsColorPaletteVisibleProperty || change.Property == IsColorSpectrumVisibleProperty) @@ -271,6 +277,66 @@ namespace Avalonia.Controls ColorChanged?.Invoke(this, e); } + /// + /// Called when the property has to be coerced. + /// + /// The value to coerce. + protected virtual Color OnCoerceColor(Color value) + { + if (IsAlphaEnabled == false) + { + return new Color(255, value.R, value.G, value.B); + } + + return value; + } + + /// + /// Called when the property has to be coerced. + /// + /// The value to coerce. + protected virtual HsvColor OnCoerceHsvColor(HsvColor value) + { + if (IsAlphaEnabled == false) + { + return new HsvColor(1.0, value.H, value.S, value.V); + } + + return value; + } + + /// + /// Coerces/validates the property value. + /// + /// The instance. + /// The value to coerce. + /// The coerced/validated value. + private static Color CoerceColor(IAvaloniaObject instance, Color value) + { + if (instance is ColorView colorView) + { + return colorView.OnCoerceColor(value); + } + + return value; + } + + /// + /// Coerces/validates the property value. + /// + /// The instance. + /// The value to coerce. + /// The coerced/validated value. + private static HsvColor CoerceHsvColor(IAvaloniaObject instance, HsvColor value) + { + if (instance is ColorView colorView) + { + return colorView.OnCoerceHsvColor(value); + } + + return value; + } + /// /// Event handler for when a key is pressed within the Hex RGB value TextBox. /// This is used to trigger re-evaluation of the color based on the TextBox value. diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index af2c8e061f..97760ba94a 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -187,8 +187,16 @@ HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" HorizontalAlignment="Center" VerticalAlignment="Stretch" - Margin="12,0,0,0" - IsVisible="{TemplateBinding IsAlphaVisible}" /> + Margin="12,0,0,0"> + + + + + + + @@ -565,6 +573,8 @@ VerticalAlignment="Center" /> + + + Date: Mon, 27 Jun 2022 00:49:59 -0400 Subject: [PATCH 093/171] Only set slider IsVisible once --- src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 97760ba94a..d43bc7e004 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -618,8 +618,7 @@ ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" HorizontalAlignment="Stretch" - VerticalAlignment="Center" - IsVisible="{TemplateBinding IsAlphaVisible}"> + VerticalAlignment="Center"> Date: Mon, 27 Jun 2022 00:53:49 -0400 Subject: [PATCH 094/171] Only disable alpha editing controls when IsAlphaEnabled is false Rely on IsAlphaVisible for visibility --- .../ColorView/ColorView.Properties.cs | 6 +++--- .../Themes/Fluent/ColorView.xaml | 20 ++++++++----------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index e1da427542..d59c4d544a 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -270,7 +270,7 @@ namespace Avalonia.Controls /// /// Gets or sets a value indicating whether the alpha component is enabled. /// When disabled (set to false) the alpha component will be fixed to maximum and - /// editing controls hidden. + /// editing controls disabled. /// public bool IsAlphaEnabled { @@ -357,7 +357,7 @@ namespace Avalonia.Controls /// /// /// All color components are controlled by this property but alpha can also be - /// controlled with and . + /// controlled with . /// public bool IsComponentSliderVisible { @@ -370,7 +370,7 @@ namespace Avalonia.Controls /// /// /// All color components are controlled by this property but alpha can also be - /// controlled with and . + /// controlled with . /// public bool IsComponentTextInputVisible { diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index d43bc7e004..563f230cc9 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -187,11 +187,10 @@ HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" HorizontalAlignment="Center" VerticalAlignment="Stretch" - Margin="12,0,0,0"> + Margin="12,0,0,0" + IsEnabled="{TemplateBinding IsAlphaEnabled}"> - @@ -564,7 +563,8 @@ BorderBrush="{DynamicResource TextControlBorderBrush}" BorderThickness="1,1,0,1" CornerRadius="4,0,0,4" - VerticalAlignment="Center"> + VerticalAlignment="Center" + IsEnabled="{TemplateBinding IsAlphaEnabled}"> - + Value="{Binding Value, ElementName=AlphaComponentSlider}" + IsEnabled="{TemplateBinding IsAlphaEnabled}"> - + VerticalAlignment="Center" + IsEnabled="{TemplateBinding IsAlphaEnabled}"> - Date: Mon, 27 Jun 2022 00:56:56 -0400 Subject: [PATCH 095/171] Remove unused ValueConverterGroup --- .../Converters/ValueConverterGroup.cs | 50 ------------------- 1 file changed, 50 deletions(-) delete mode 100644 src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs b/src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs deleted file mode 100644 index 2710c220f4..0000000000 --- a/src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using Avalonia.Data.Converters; - -namespace Avalonia.Controls.Primitives.Converters -{ - /// - /// Converter to chain together multiple converters. - /// - public class ValueConverterGroup : List, IValueConverter - { - /// - /// - public object? Convert( - object? value, - Type targetType, - object? parameter, - CultureInfo culture) - { - object? curValue; - - curValue = value; - for (int i = 0; i < Count; i++) - { - curValue = this[i].Convert(curValue, targetType, parameter, culture); - } - - return curValue; - } - - /// - public object? ConvertBack( - object? value, - Type targetType, - object? parameter, - CultureInfo culture) - { - object? curValue; - - curValue = value; - for (int i = (Count - 1); i >= 0; i--) - { - curValue = this[i].ConvertBack(curValue, targetType, parameter, culture); - } - - return curValue; - } - } -} From 1849ca4caa660a172fa16a3e616a58be84d12340 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 28 Jun 2022 23:50:22 +0300 Subject: [PATCH 096/171] Fixed matrix multiplication order --- .../Rendering/Composition/Server/DrawingContextProxy.cs | 4 ++-- .../Rendering/Composition/Server/ServerVisual.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 2fd87f6620..e261507f60 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -33,7 +33,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont set => _visualBrushRenderer.VisualBrushDrawList = value; } - public Matrix PreTransform { get; set; } = Matrix.Identity; + public Matrix PostTransform { get; set; } = Matrix.Identity; public void Dispose() { @@ -44,7 +44,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont public Matrix Transform { get => _transform; - set => _impl.Transform = PreTransform * (_transform = value); + set => _impl.Transform = (_transform = value) * PostTransform; } public void Clear(Color color) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 06f087258b..3b36dfb87e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -41,7 +41,7 @@ namespace Avalonia.Rendering.Composition.Server Root!.RenderedVisuals++; var transform = GlobalTransformMatrix; - canvas.PreTransform = MatrixUtils.ToMatrix(transform); + canvas.PostTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; if (Opacity != 1) canvas.PushOpacity(Opacity); @@ -56,7 +56,7 @@ namespace Avalonia.Rendering.Composition.Server RenderCore(canvas, currentTransformedClip); // Hack to force invalidation of SKMatrix - canvas.PreTransform = MatrixUtils.ToMatrix(transform); + canvas.PostTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; if (OpacityMaskBrush != null) From f6506f19e83d6f8456a86d749324b18bb1cba4ee Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 29 Jun 2022 23:57:03 +0300 Subject: [PATCH 097/171] Fixed Ref finalizer --- src/Avalonia.Base/Utilities/Ref.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Utilities/Ref.cs b/src/Avalonia.Base/Utilities/Ref.cs index 7209f02720..95a1c23883 100644 --- a/src/Avalonia.Base/Utilities/Ref.cs +++ b/src/Avalonia.Base/Utilities/Ref.cs @@ -159,7 +159,7 @@ namespace Avalonia.Utilities ~Ref() { - _counter?.Release(); + Dispose(); } public T Item From b4512e0da8f2334b1a1b2826e600f2386a30a4af Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 30 Jun 2022 00:05:10 +0300 Subject: [PATCH 098/171] Clear the draw list before letting go of the visual --- src/Avalonia.Base/Visual.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 7db218d2df..716b5f261d 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -495,7 +495,12 @@ namespace Avalonia DisableTransitions(); OnDetachedFromVisualTree(e); - CompositionVisual = null; + if (CompositionVisual != null) + { + CompositionVisual.DrawList = null; + CompositionVisual = null; + } + DetachedFromVisualTree?.Invoke(this, e); e.Root?.Renderer?.AddDirty(this); From 77dca2c5356b544e57dab77ca563885e00989f0f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 30 Jun 2022 15:53:07 +0300 Subject: [PATCH 099/171] VerifyAccess --- src/Avalonia.Base/Rendering/Composition/Compositor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 28c81dfb91..1bdae44cb9 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -62,6 +62,7 @@ namespace Avalonia.Rendering.Composition /// A task that completes when sent changes are applied and rendered on the render thread public Task RequestCommitAsync() { + Dispatcher.UIThread.VerifyAccess(); var batch = new Batch(); using (var writer = new BatchStreamWriter(batch.Changes, _batchMemoryPool, _batchObjectPool)) @@ -126,6 +127,7 @@ namespace Avalonia.Rendering.Composition internal void RegisterForSerialization(CompositionObject compositionObject) { + Dispatcher.UIThread.VerifyAccess(); _objectsForSerialization.Add(compositionObject); QueueImplicitBatchCommit(); } From 5b77b7d24bab22929c38f5303cd5bd34262ff474 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 2 Jul 2022 13:55:09 +0300 Subject: [PATCH 100/171] UseComposition -> UseCompositor --- samples/ControlCatalog.NetCore/Program.cs | 2 +- src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs | 2 +- src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index df7ae67ef4..7423428a0a 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -56,7 +56,7 @@ namespace ControlCatalog.NetCore .UseHeadless(new AvaloniaHeadlessPlatformOptions { UseHeadlessDrawing = true, - UseComposition = true + UseCompositor = true }) .AfterSetup(_ => { diff --git a/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs b/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs index 02a1e57701..cc7d5ef30d 100644 --- a/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs +++ b/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs @@ -22,7 +22,7 @@ namespace Avalonia return builder .UseHeadless(new AvaloniaHeadlessPlatformOptions { - UseComposition = true, + UseCompositor = true, UseHeadlessDrawing = false }) .AfterSetup(_ => diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index d957b99747..fe9283b26f 100644 --- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -73,7 +73,7 @@ namespace Avalonia.Headless .Bind().ToSingleton() .Bind().ToConstant(new HeadlessWindowingPlatform()) .Bind().ToSingleton(); - if (opts.UseComposition) + if (opts.UseCompositor) Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), null); } @@ -89,7 +89,7 @@ namespace Avalonia.Headless public class AvaloniaHeadlessPlatformOptions { - public bool UseComposition { get; set; } = true; + public bool UseCompositor { get; set; } = true; public bool UseHeadlessDrawing { get; set; } = true; } From aa5dc3e0f59e673bb089707008ad94c1da807112 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 2 Jul 2022 23:01:37 +0200 Subject: [PATCH 101/171] Added class selector style benchmarks. --- .../Styling/Style_ClassSelector.cs | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs diff --git a/tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs b/tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs new file mode 100644 index 0000000000..f242e95966 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Avalonia.Controls; +using Avalonia.Styling; +using BenchmarkDotNet.Attributes; + +#nullable enable + +namespace Avalonia.Benchmarks.Styling +{ + [MemoryDiagnoser] + public class Style_ClassSelector + { + private Style _style = null!; + + public Style_ClassSelector() + { + RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle); + } + + [GlobalSetup] + public void Setup() + { + _style = new Style(x => x.OfType().Class("foo")) + { + Setters = { new Setter(TestClass.StringProperty, "foo") } + }; + } + + [Benchmark(OperationsPerInvoke = 50)] + public void Apply() + { + var target = new TestClass(); + + target.BeginBatchUpdate(); + + for (var i = 0; i < 50; ++i) + _style.TryAttach(target, null); + + target.EndBatchUpdate(); + } + + [Benchmark(OperationsPerInvoke = 50)] + public void Apply_Toggle() + { + var target = new TestClass(); + + target.BeginBatchUpdate(); + + for (var i = 0; i < 50; ++i) + _style.TryAttach(target, null); + + target.EndBatchUpdate(); + + target.Classes.Add("foo"); + target.Classes.Remove("foo"); + } + + [Benchmark(OperationsPerInvoke = 50)] + public void Apply_Detach() + { + var target = new TestClass(); + + target.BeginBatchUpdate(); + + for (var i = 0; i < 50; ++i) + _style.TryAttach(target, null); + + target.EndBatchUpdate(); + + target.DetachStyles(); + } + + private class TestClass : Control + { + public static readonly StyledProperty StringProperty = + AvaloniaProperty.Register("String"); + public void DetachStyles() => InvalidateStyles(); + } + + private class TestClass2 : Control + { + } + } +} From e63aa46458022a4a8974afe1eb1792ec7d1968c3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 3 Jul 2022 00:14:04 +0200 Subject: [PATCH 102/171] Add a more efficient way to listen to classes changes. Improves the situation in e.g. #8389 drastically. --- src/Avalonia.Base/Controls/Classes.cs | 53 ++++++++++++++++++- .../Controls/IClassesChangedListener.cs | 14 +++++ .../Styling/Activators/StyleClassActivator.cs | 26 ++++----- .../Styling/TypeNameAndClassSelector.cs | 3 +- .../Styling/SelectorTests_Template.cs | 5 +- tests/Avalonia.LeakTests/ControlTests.cs | 2 +- 6 files changed, 80 insertions(+), 23 deletions(-) create mode 100644 src/Avalonia.Base/Controls/IClassesChangedListener.cs diff --git a/src/Avalonia.Base/Controls/Classes.cs b/src/Avalonia.Base/Controls/Classes.cs index 50605661fa..e64209c3cb 100644 --- a/src/Avalonia.Base/Controls/Classes.cs +++ b/src/Avalonia.Base/Controls/Classes.cs @@ -14,6 +14,8 @@ namespace Avalonia.Controls /// public class Classes : AvaloniaList, IPseudoClasses { + private List? _listeners; + /// /// Initializes a new instance of the class. /// @@ -39,6 +41,11 @@ namespace Avalonia.Controls { } + /// + /// Gets the number of listeners subscribed to this collection for unit testing purposes. + /// + internal int ListenerCount => _listeners?.Count ?? 0; + /// /// Parses a classes string. /// @@ -62,6 +69,7 @@ namespace Avalonia.Controls if (!Contains(name)) { base.Add(name); + NotifyChanged(); } } @@ -89,6 +97,7 @@ namespace Avalonia.Controls } base.AddRange(c); + NotifyChanged(); } /// @@ -103,6 +112,8 @@ namespace Avalonia.Controls RemoveAt(i); } } + + NotifyChanged(); } /// @@ -122,6 +133,7 @@ namespace Avalonia.Controls if (!Contains(name)) { base.Insert(index, name); + NotifyChanged(); } } @@ -154,6 +166,7 @@ namespace Avalonia.Controls if (toInsert != null) { base.InsertRange(index, toInsert); + NotifyChanged(); } } @@ -169,7 +182,14 @@ namespace Avalonia.Controls public override bool Remove(string name) { ThrowIfPseudoclass(name, "removed"); - return base.Remove(name); + + if (base.Remove(name)) + { + NotifyChanged(); + return true; + } + + return false; } /// @@ -197,6 +217,7 @@ namespace Avalonia.Controls if (toRemove != null) { base.RemoveAll(toRemove); + NotifyChanged(); } } @@ -214,6 +235,7 @@ namespace Avalonia.Controls var name = this[index]; ThrowIfPseudoclass(name, "removed"); base.RemoveAt(index); + NotifyChanged(); } /// @@ -224,6 +246,7 @@ namespace Avalonia.Controls public override void RemoveRange(int index, int count) { base.RemoveRange(index, count); + NotifyChanged(); } /// @@ -255,6 +278,7 @@ namespace Avalonia.Controls } base.AddRange(source); + NotifyChanged(); } /// @@ -263,13 +287,38 @@ namespace Avalonia.Controls if (!Contains(name)) { base.Add(name); + NotifyChanged(); } } /// bool IPseudoClasses.Remove(string name) { - return base.Remove(name); + if (base.Remove(name)) + { + NotifyChanged(); + return true; + } + + return false; + } + + internal void AddListener(IClassesChangedListener listener) + { + (_listeners ??= new()).Add(listener); + } + + internal void RemoveListener(IClassesChangedListener listener) + { + _listeners?.Remove(listener); + } + + private void NotifyChanged() + { + if (_listeners is null) + return; + foreach (var listener in _listeners) + listener.Changed(); } private void ThrowIfPseudoclass(string name, string operation) diff --git a/src/Avalonia.Base/Controls/IClassesChangedListener.cs b/src/Avalonia.Base/Controls/IClassesChangedListener.cs new file mode 100644 index 0000000000..b4de893c97 --- /dev/null +++ b/src/Avalonia.Base/Controls/IClassesChangedListener.cs @@ -0,0 +1,14 @@ +namespace Avalonia.Controls +{ + /// + /// Internal interface for listening to changes in in a more + /// performant manner than subscribing to CollectionChanged. + /// + internal interface IClassesChangedListener + { + /// + /// Notifies the listener that the collection has changed. + /// + void Changed(); + } +} diff --git a/src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs b/src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs index 98d3f16a0a..3f70ff50b3 100644 --- a/src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using Avalonia.Collections; +using Avalonia.Controls; #nullable enable @@ -10,21 +11,17 @@ namespace Avalonia.Styling.Activators /// An which is active when a set of classes match those on a /// control. /// - internal sealed class StyleClassActivator : StyleActivatorBase + internal sealed class StyleClassActivator : StyleActivatorBase, IClassesChangedListener { private readonly IList _match; - private readonly IAvaloniaReadOnlyList _classes; - private NotifyCollectionChangedEventHandler? _classesChangedHandler; + private readonly Classes _classes; - public StyleClassActivator(IAvaloniaReadOnlyList classes, IList match) + public StyleClassActivator(Classes classes, IList match) { _classes = classes; _match = match; } - private NotifyCollectionChangedEventHandler ClassesChangedHandler => - _classesChangedHandler ??= ClassesChanged; - public static bool AreClassesMatching(IReadOnlyList classes, IList toMatch) { int remainingMatches = toMatch.Count; @@ -55,23 +52,20 @@ namespace Avalonia.Styling.Activators return remainingMatches == 0; } - protected override void Initialize() + void IClassesChangedListener.Changed() { PublishNext(IsMatching()); - _classes.CollectionChanged += ClassesChangedHandler; } - protected override void Deinitialize() + protected override void Initialize() { - _classes.CollectionChanged -= ClassesChangedHandler; + PublishNext(IsMatching()); + _classes.AddListener(this); } - private void ClassesChanged(object? sender, NotifyCollectionChangedEventArgs e) + protected override void Deinitialize() { - if (e.Action != NotifyCollectionChangedAction.Move) - { - PublishNext(IsMatching()); - } + _classes.RemoveListener(this); } private bool IsMatching() => AreClassesMatching(_classes, _match); diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index 24d5d6bbbf..d52c8c7d5c 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using Avalonia.Controls; using Avalonia.Styling.Activators; #nullable enable @@ -125,7 +126,7 @@ namespace Avalonia.Styling { if (subscribe) { - var observable = new StyleClassActivator(control.Classes, _classes.Value); + var observable = new StyleClassActivator((Classes)control.Classes, _classes.Value); return new SelectorMatch(observable); } diff --git a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs index 8b4c988037..176fa07f19 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs @@ -142,14 +142,13 @@ namespace Avalonia.Base.UnitTests.Styling var border = (Border)target.Object.VisualChildren.Single(); var selector = default(Selector).OfType(templatedControl.Object.GetType()).Class("foo").Template().OfType(); var activator = selector.Match(border).Activator; - var inccDebug = (INotifyCollectionChangedDebug)styleable.Object.Classes; using (activator.Subscribe(_ => { })) { - Assert.Single(inccDebug.GetCollectionChangedSubscribers()); + Assert.Equal(1, ((Classes)styleable.Object.Classes).ListenerCount); } - Assert.Null(inccDebug.GetCollectionChangedSubscribers()); + Assert.Equal(0, ((Classes)styleable.Object.Classes).ListenerCount); } private void BuildVisualTree(Mock templatedControl) where T : class, IVisual diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 8c05f2a0a7..8651409af1 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -313,7 +313,7 @@ namespace Avalonia.LeakTests // The TextBox should have subscriptions to its Classes collection from the // default theme. - Assert.NotEmpty(((INotifyCollectionChangedDebug)textBox.Classes).GetCollectionChangedSubscribers()); + Assert.NotEqual(0, textBox.Classes.ListenerCount); // Clear the content and ensure the TextBox is removed. window.Content = null; From 6370ae38de78ab9c4bafa343e8870ee6d531b49c Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 5 Jul 2022 21:58:26 -0400 Subject: [PATCH 103/171] Switch to TemplateBinding in ColorPicker where possible --- .../Themes/Fluent/ColorPicker.xaml | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 878d7819eb..46d627b3a1 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -49,31 +49,31 @@ + Color="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" + ColorModel="{Binding ColorModel, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" + ColorSpectrumComponents="{TemplateBinding ColorSpectrumComponents}" + ColorSpectrumShape="{TemplateBinding ColorSpectrumShape}" + HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" + IsAlphaEnabled="{TemplateBinding IsAlphaEnabled}" + IsAlphaVisible="{TemplateBinding IsAlphaVisible}" + IsColorComponentsVisible="{TemplateBinding IsColorComponentsVisible}" + IsColorPaletteVisible="{TemplateBinding IsColorPaletteVisible}" + IsColorPreviewVisible="{TemplateBinding IsColorPreviewVisible}" + IsColorSpectrumVisible="{TemplateBinding IsColorSpectrumVisible}" + IsColorSpectrumSliderVisible="{TemplateBinding IsColorSpectrumSliderVisible}" + IsComponentTextInputVisible="{TemplateBinding IsComponentTextInputVisible}" + IsHexInputVisible="{TemplateBinding IsHexInputVisible}" + MaxHue="{TemplateBinding MaxHue}" + MaxSaturation="{TemplateBinding MaxSaturation}" + MaxValue="{TemplateBinding MaxValue}" + MinHue="{TemplateBinding MinHue}" + MinSaturation="{TemplateBinding MinSaturation}" + MinValue="{TemplateBinding MinValue}" + PaletteColors="{TemplateBinding PaletteColors}" + PaletteColumnCount="{TemplateBinding PaletteColumnCount}" + Palette="{TemplateBinding Palette}" + SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" + ShowAccentColors="{TemplateBinding ShowAccentColors}" /> From a24720816f593e24bda2dd58ff6b6a56cdb23fd1 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 5 Jul 2022 22:12:18 -0400 Subject: [PATCH 104/171] Standardize ColorControl resource names This follows the Fluent standard where the resource is prefixed with the control itself --- .../Themes/Default/ColorPreviewer.xaml | 18 +++++------ .../Themes/Default/ColorSlider.xaml | 4 +-- .../Themes/Default/Default.xaml | 2 +- .../Themes/Fluent/ColorPicker.xaml | 2 +- .../Themes/Fluent/ColorPreviewer.xaml | 18 +++++------ .../Themes/Fluent/ColorSlider.xaml | 4 +-- .../Themes/Fluent/ColorView.xaml | 30 +++++++++---------- .../Themes/Fluent/Fluent.xaml | 2 +- 8 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml index 24242fc251..6ac0982408 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml @@ -9,8 +9,8 @@ - 80 - 40 + 80 + 40 + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 867e0f223f..9e795d81a2 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -13,6 +13,8 @@ + + 48 30 @@ -90,29 +92,26 @@ - - + + - - - + CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource BottomCornerRadiusFilterConverter}}" + Background="Transparent" + BorderBrush="Transparent" + BorderThickness="0,1,0,0" /> Date: Tue, 5 Jul 2022 23:32:35 -0400 Subject: [PATCH 106/171] Add code to hide the tab strip when only one tab is visible --- .../ColorView/ColorView.cs | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 8fb80980d5..bea982b7ea 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -4,9 +4,11 @@ using System.Collections.ObjectModel; using System.Globalization; using Avalonia.Controls.Converters; using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Media; using Avalonia.Threading; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -82,19 +84,19 @@ namespace Avalonia.Controls if (_tabControl != null && _tabControl.Items != null) { - // Determine if any item is visible - bool isAnyItemVisible = false; + // Determine the number of visible tab items + int numVisibleItems = 0; foreach (var item in _tabControl.Items) { if (item is Control control && control.IsVisible) { - isAnyItemVisible = true; - break; + numVisibleItems++; } } - if (isAnyItemVisible) + // Verify the selection + if (numVisibleItems > 0) { object? selectedItem = null; @@ -140,6 +142,23 @@ namespace Avalonia.Controls _tabControl.IsVisible = false; } + // Hide the "tab strip" if there is only one tab + // This allows, for example, to view only the palette + /* + var itemsPresenter = _tabControl.FindDescendantOfType(); + if (itemsPresenter != null) + { + if (numVisibleItems == 1) + { + itemsPresenter.IsVisible = false; + } + else + { + itemsPresenter.IsVisible = true; + } + } + */ + // Note that if externally the SelectedIndex is set to 4 or something // outside the valid range, the TabControl will ignore it and replace it // with a valid SelectedIndex. This however is not propagated back through From 0c81cb24801b7c7761c0b00f0675c8ff08754822 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 9 Jun 2022 10:56:45 +0200 Subject: [PATCH 107/171] fix: DBus Null annotations --- Avalonia.sln | 1 + .../Avalonia.FreeDesktop.csproj | 4 ++ src/Avalonia.FreeDesktop/DBusCallQueue.cs | 20 +++----- src/Avalonia.FreeDesktop/DBusHelper.cs | 4 +- .../DBusIme/DBusTextInputMethodBase.cs | 46 ++++++++++--------- .../DBusIme/Fcitx/FcitxDBus.cs | 28 +++++------ .../DBusIme/Fcitx/FcitxICWrapper.cs | 31 +++++++------ .../DBusIme/Fcitx/FcitxX11TextInputMethod.cs | 38 ++++++++------- .../DBusIme/IBus/IBusDBus.cs | 38 +++++++-------- .../DBusIme/IBus/IBusX11TextInputMethod.cs | 30 ++++++++---- .../DBusIme/X11DBusImeHelper.cs | 2 +- src/Avalonia.FreeDesktop/DBusMenu.cs | 16 +++---- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 36 +++++++-------- src/Avalonia.FreeDesktop/DBusRequest.cs | 2 +- .../LinuxMountedVolumeInfoProvider.cs | 2 +- .../Avalonia.SourceGenerator.csproj | 1 + .../Output/DrmOutput.cs | 2 +- .../IsExternalInit.cs | 0 18 files changed, 161 insertions(+), 140 deletions(-) rename src/{Avalonia.SourceGenerator => Shared}/IsExternalInit.cs (100%) diff --git a/Avalonia.sln b/Avalonia.sln index 25c7daf080..8d2479a663 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -38,6 +38,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + src\Shared\IsExternalInit.cs = src\Shared\IsExternalInit.cs src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs EndProjectSection diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index a5cb207223..3b1c6cc7b1 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -5,6 +5,10 @@ enable + + + + diff --git a/src/Avalonia.FreeDesktop/DBusCallQueue.cs b/src/Avalonia.FreeDesktop/DBusCallQueue.cs index 5cd748be02..e7c07dcbf9 100644 --- a/src/Avalonia.FreeDesktop/DBusCallQueue.cs +++ b/src/Avalonia.FreeDesktop/DBusCallQueue.cs @@ -8,10 +8,9 @@ namespace Avalonia.FreeDesktop { private readonly Func _errorHandler; - class Item + record Item(Func Callback) { - public Func Callback; - public Action OnFinish; + public Action? OnFinish; } private Queue _q = new Queue(); private bool _processing; @@ -23,19 +22,15 @@ namespace Avalonia.FreeDesktop public void Enqueue(Func cb) { - _q.Enqueue(new Item - { - Callback = cb - }); + _q.Enqueue(new Item(cb)); Process(); } public Task EnqueueAsync(Func cb) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - _q.Enqueue(new Item + _q.Enqueue(new Item(cb) { - Callback = cb, OnFinish = e => { if (e == null) @@ -51,13 +46,12 @@ namespace Avalonia.FreeDesktop public Task EnqueueAsync(Func> cb) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - _q.Enqueue(new Item - { - Callback = async () => + _q.Enqueue(new Item(async () => { var res = await cb(); tcs.TrySetResult(res); - }, + }) + { OnFinish = e => { if (e != null) diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs index 7204e51dbd..9f9d75b411 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -17,7 +17,7 @@ namespace Avalonia.FreeDesktop private readonly object _lock = new(); private SynchronizationContext? _ctx; - public override void Post(SendOrPostCallback d, object state) + public override void Post(SendOrPostCallback d, object? state) { lock (_lock) { @@ -29,7 +29,7 @@ namespace Avalonia.FreeDesktop } } - public override void Send(SendOrPostCallback d, object state) + public override void Send(SendOrPostCallback d, object? state) { lock (_lock) { diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs index 864c579319..eef865d458 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Reflection; using System.Threading.Tasks; -using Avalonia.FreeDesktop.DBusIme.Fcitx; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Avalonia.Logging; @@ -26,7 +24,7 @@ namespace Avalonia.FreeDesktop.DBusIme return (im, im); } } - + internal abstract class DBusTextInputMethodBase : IX11InputMethodControl, ITextInputMethodImpl { private List _disposables = new List(); @@ -34,7 +32,7 @@ namespace Avalonia.FreeDesktop.DBusIme protected Connection Connection { get; } private readonly string[] _knownNames; private bool _connecting; - private string _currentName; + private string? _currentName; private DBusCallQueue _queue; private bool _controlActive, _windowActive; private bool? _imeActive; @@ -42,9 +40,9 @@ namespace Avalonia.FreeDesktop.DBusIme private PixelRect? _lastReportedRect; private double _scaling = 1; private PixelPoint _windowPosition; - + protected bool IsConnected => _currentName != null; - + public DBusTextInputMethodBase(Connection connection, params string[] knownNames) { _queue = new DBusCallQueue(QueueOnError); @@ -58,18 +56,18 @@ namespace Avalonia.FreeDesktop.DBusIme foreach (var name in _knownNames) _disposables.Add(await Connection.ResolveServiceOwnerAsync(name, OnNameChange)); } - + protected abstract Task Connect(string name); protected string GetAppName() => - Application.Current.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia"; - + Application.Current?.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia"; + private async void OnNameChange(ServiceOwnerChangedEventArgs args) { if (args.NewOwner != null && _currentName == null) { _onlineNamesQueue.Enqueue(args.ServiceName); - if(!_connecting) + if (!_connecting) { _connecting = true; try @@ -98,25 +96,25 @@ namespace Avalonia.FreeDesktop.DBusIme _connecting = false; } } - + } - + // IME has crashed if (args.NewOwner == null && args.ServiceName == _currentName) { _currentName = null; - foreach(var s in _disposables) + foreach (var s in _disposables) s.Dispose(); _disposables.Clear(); - + OnDisconnected(); Reset(); - + // Watch again Watch(); } } - + protected virtual Task Disconnect() { return Task.CompletedTask; @@ -124,7 +122,7 @@ namespace Avalonia.FreeDesktop.DBusIme protected virtual void OnDisconnected() { - + } protected virtual void Reset() @@ -149,10 +147,14 @@ namespace Avalonia.FreeDesktop.DBusIme OnDisconnected(); _currentName = null; } - + protected void Enqueue(Func cb) => _queue.Enqueue(cb); - protected void AddDisposable(IDisposable d) => _disposables.Add(d); + protected void AddDisposable(IDisposable? d) + { + if(d is { }) + _disposables.Add(d); + } public void Dispose() { @@ -198,7 +200,7 @@ namespace Avalonia.FreeDesktop.DBusIme UpdateActive(); } - void ITextInputMethodImpl.SetClient(ITextInputMethodClient client) + void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) { _controlActive = client is { }; UpdateActive(); @@ -225,7 +227,7 @@ namespace Avalonia.FreeDesktop.DBusIme } } - private Action _onCommit; + private Action? _onCommit; event Action IX11InputMethodControl.Commit { add => _onCommit += value; @@ -234,7 +236,7 @@ namespace Avalonia.FreeDesktop.DBusIme protected void FireCommit(string s) => _onCommit?.Invoke(s); - private Action _onForward; + private Action? _onForward; event Action IX11InputMethodControl.ForwardKey { add => _onForward += value; diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs index 7ce2339763..06afacaa29 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs @@ -31,15 +31,15 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); Task DestroyICAsync(); Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, int Type, uint Time); - Task WatchEnableIMAsync(Action handler, Action onError = null); - Task WatchCloseIMAsync(Action handler, Action onError = null); - Task WatchCommitStringAsync(Action handler, Action onError = null); - Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action onError = null); - Task WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action onError = null); - Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action onError = null); - Task WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action onError = null); - Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action onError = null); - Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action onError = null); + Task WatchEnableIMAsync(Action handler, Action? onError = null); + Task WatchCloseIMAsync(Action handler, Action? onError = null); + Task WatchCommitStringAsync(Action handler, Action? onError = null); + Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action? onError = null); + Task WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action? onError = null); + Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action? onError = null); + Task WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action? onError = null); + Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action? onError = null); + Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action? onError = null); } [DBusInterface("org.fcitx.Fcitx.InputContext1")] @@ -54,11 +54,11 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); Task DestroyICAsync(); Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, bool Type, uint Time); - Task WatchCommitStringAsync(Action handler, Action onError = null); - Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action onError = null); - Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action onError = null); - Task WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action onError = null); - Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action onError = null); + Task WatchCommitStringAsync(Action handler, Action? onError = null); + Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action? onError = null); + Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action? onError = null); + Task WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action? onError = null); + Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action? onError = null); } [DBusInterface("org.fcitx.Fcitx.InputMethod1")] diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs index a03ea213aa..6c503edb41 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs @@ -5,8 +5,8 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx { internal class FcitxICWrapper { - private readonly IFcitxInputContext1 _modern; - private readonly IFcitxInputContext _old; + private readonly IFcitxInputContext1? _modern; + private readonly IFcitxInputContext? _old; public FcitxICWrapper(IFcitxInputContext old) { @@ -18,34 +18,37 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx _modern = modern; } - public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern.FocusInAsync(); + public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern?.FocusInAsync() ?? Task.CompletedTask; - public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern.FocusOutAsync(); + public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern?.FocusOutAsync() ?? Task.CompletedTask; - public Task ResetAsync() => _old?.ResetAsync() ?? _modern.ResetAsync(); + public Task ResetAsync() => _old?.ResetAsync() ?? _modern?.ResetAsync() ?? Task.CompletedTask; public Task SetCursorRectAsync(int x, int y, int w, int h) => - _old?.SetCursorRectAsync(x, y, w, h) ?? _modern.SetCursorRectAsync(x, y, w, h); - public Task DestroyICAsync() => _old?.DestroyICAsync() ?? _modern.DestroyICAsync(); + _old?.SetCursorRectAsync(x, y, w, h) ?? _modern?.SetCursorRectAsync(x, y, w, h) ?? Task.CompletedTask; + public Task DestroyICAsync() => _old?.DestroyICAsync() ?? _modern?.DestroyICAsync() ?? Task.CompletedTask; public async Task ProcessKeyEventAsync(uint keyVal, uint keyCode, uint state, int type, uint time) { if(_old!=null) return await _old.ProcessKeyEventAsync(keyVal, keyCode, state, type, time) != 0; - return await _modern.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time); + return await (_modern?.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time) ?? Task.FromResult(false)); } - public Task WatchCommitStringAsync(Action handler) => - _old?.WatchCommitStringAsync(handler) ?? _modern.WatchCommitStringAsync(handler); + public Task WatchCommitStringAsync(Action handler) => + _old?.WatchCommitStringAsync(handler) + ?? _modern?.WatchCommitStringAsync(handler) + ?? Task.FromResult(default(IDisposable?)); - public Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler) + public Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler) { return _old?.WatchForwardKeyAsync(handler) - ?? _modern.WatchForwardKeyAsync(ev => - handler((ev.keyval, ev.state, ev.type ? 1 : 0))); + ?? _modern?.WatchForwardKeyAsync(ev => + handler((ev.keyval, ev.state, ev.type ? 1 : 0))) + ?? Task.FromResult(default(IDisposable?)); } public Task SetCapacityAsync(uint flags) => - _old?.SetCapacityAsync(flags) ?? _modern.SetCapabilityAsync(flags); + _old?.SetCapacityAsync(flags) ?? _modern?.SetCapabilityAsync(flags) ?? Task.CompletedTask; } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs index 0b85965de7..791431dfa7 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs @@ -12,7 +12,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx { internal class FcitxX11TextInputMethod : DBusTextInputMethodBase { - private FcitxICWrapper _context; + private FcitxICWrapper? _context; private FcitxCapabilityFlags? _lastReportedFlags; public FcitxX11TextInputMethod(Connection connection) : base(connection, @@ -49,7 +49,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx return true; } - protected override Task Disconnect() => _context.DestroyICAsync(); + protected override Task Disconnect() => _context?.DestroyICAsync() ?? Task.CompletedTask; protected override void OnDisconnected() => _context = null; @@ -60,18 +60,18 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx } protected override Task SetCursorRectCore(PixelRect cursorRect) => - _context.SetCursorRectAsync(cursorRect.X, cursorRect.Y, Math.Max(1, cursorRect.Width), - Math.Max(1, cursorRect.Height)); - - protected override Task SetActiveCore(bool active) - { - if (active) - return _context.FocusInAsync(); - else - return _context.FocusOutAsync(); - } + _context?.SetCursorRectAsync(cursorRect.X, cursorRect.Y, Math.Max(1, cursorRect.Width), + Math.Max(1, cursorRect.Height)) + ?? Task.CompletedTask; + + protected override Task SetActiveCore(bool active)=> (active + ? _context?.FocusInAsync() + : _context?.FocusOutAsync()) + ?? Task.CompletedTask; + - protected override Task ResetContextCore() => _context.ResetAsync(); + protected override Task ResetContextCore() => _context?.ResetAsync() + ?? Task.CompletedTask; protected override async Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode) { @@ -88,9 +88,15 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx var type = args.Type == RawKeyEventType.KeyDown ? FcitxKeyEventType.FCITX_PRESS_KEY : FcitxKeyEventType.FCITX_RELEASE_KEY; - - return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type, - (uint)args.Timestamp).ConfigureAwait(false); + if (_context is { }) + { + return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type, + (uint)args.Timestamp).ConfigureAwait(false); + } + else + { + return false; + } } public override void SetOptions(TextInputOptions options) => diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs index 26c0d249f3..4ef034adb9 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs @@ -22,25 +22,25 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus Task GetEngineAsync(); Task DestroyAsync(); Task SetSurroundingTextAsync(object Text, uint CursorPos, uint AnchorPos); - Task WatchCommitTextAsync(Action cb, Action onError = null); - Task WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action onError = null); - Task WatchRequireSurroundingTextAsync(Action handler, Action onError = null); - Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action onError = null); - Task WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action onError = null); - Task WatchShowPreeditTextAsync(Action handler, Action onError = null); - Task WatchHidePreeditTextAsync(Action handler, Action onError = null); - Task WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action onError = null); - Task WatchShowAuxiliaryTextAsync(Action handler, Action onError = null); - Task WatchHideAuxiliaryTextAsync(Action handler, Action onError = null); - Task WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action onError = null); - Task WatchShowLookupTableAsync(Action handler, Action onError = null); - Task WatchHideLookupTableAsync(Action handler, Action onError = null); - Task WatchPageUpLookupTableAsync(Action handler, Action onError = null); - Task WatchPageDownLookupTableAsync(Action handler, Action onError = null); - Task WatchCursorUpLookupTableAsync(Action handler, Action onError = null); - Task WatchCursorDownLookupTableAsync(Action handler, Action onError = null); - Task WatchRegisterPropertiesAsync(Action handler, Action onError = null); - Task WatchUpdatePropertyAsync(Action handler, Action onError = null); + Task WatchCommitTextAsync(Action cb, Action? onError = null); + Task WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action? onError = null); + Task WatchRequireSurroundingTextAsync(Action handler, Action? onError = null); + Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action? onError = null); + Task WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action? onError = null); + Task WatchShowPreeditTextAsync(Action handler, Action? onError = null); + Task WatchHidePreeditTextAsync(Action handler, Action? onError = null); + Task WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action? onError = null); + Task WatchShowAuxiliaryTextAsync(Action handler, Action? onError = null); + Task WatchHideAuxiliaryTextAsync(Action handler, Action? onError = null); + Task WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action? onError = null); + Task WatchShowLookupTableAsync(Action handler, Action? onError = null); + Task WatchHideLookupTableAsync(Action handler, Action? onError = null); + Task WatchPageUpLookupTableAsync(Action handler, Action? onError = null); + Task WatchPageDownLookupTableAsync(Action handler, Action? onError = null); + Task WatchCursorUpLookupTableAsync(Action handler, Action? onError = null); + Task WatchCursorDownLookupTableAsync(Action handler, Action? onError = null); + Task WatchRegisterPropertiesAsync(Action handler, Action? onError = null); + Task WatchUpdatePropertyAsync(Action handler, Action? onError = null); } diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs index 1397eaa57b..2324ca44a7 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs @@ -9,7 +9,7 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus { internal class IBusX11TextInputMethod : DBusTextInputMethodBase { - private IIBusInputContext _context; + private IIBusInputContext? _context; public IBusX11TextInputMethod(Connection connection) : base(connection, "org.freedesktop.portal.IBus") @@ -53,16 +53,16 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus private void OnCommitText(object wtf) { // Hello darkness, my old friend - var prop = wtf.GetType().GetField("Item3"); - if (prop != null) + if (wtf.GetType().GetField("Item3") is { } prop) { - var text = (string)prop.GetValue(wtf); + var text = prop.GetValue(wtf) as string; if (!string.IsNullOrEmpty(text)) - FireCommit(text); + FireCommit(text!); } } - protected override Task Disconnect() => _context.DestroyAsync(); + protected override Task Disconnect() => _context?.DestroyAsync() + ?? Task.CompletedTask; protected override void OnDisconnected() { @@ -71,13 +71,15 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus } protected override Task SetCursorRectCore(PixelRect rect) - => _context.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height); + => _context?.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height) + ?? Task.CompletedTask; protected override Task SetActiveCore(bool active) - => active ? _context.FocusInAsync() : _context.FocusOutAsync(); + => (active ? _context?.FocusInAsync() : _context?.FocusOutAsync()) + ?? Task.CompletedTask; protected override Task ResetContextCore() - => _context.ResetAsync(); + => _context?.ResetAsync() ?? Task.CompletedTask; protected override Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode) { @@ -94,7 +96,15 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus if (args.Type == RawKeyEventType.KeyUp) state |= IBusModifierMask.ReleaseMask; - return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state); + if(_context is { }) + { + return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state); + } + else + { + return Task.FromResult(false); + } + } public override void SetOptions(TextInputOptions options) diff --git a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs index 7f71ecf0ff..86978c8b60 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs @@ -17,7 +17,7 @@ namespace Avalonia.FreeDesktop.DBusIme new DBusInputMethodFactory(_ => new IBusX11TextInputMethod(conn)) }; - static Func DetectInputMethod() + static Func? DetectInputMethod() { foreach (var name in new[] { "AVALONIA_IM_MODULE", "GTK_IM_MODULE", "QT_IM_MODULE" }) { diff --git a/src/Avalonia.FreeDesktop/DBusMenu.cs b/src/Avalonia.FreeDesktop/DBusMenu.cs index 7180345386..3a1c65e7c9 100644 --- a/src/Avalonia.FreeDesktop/DBusMenu.cs +++ b/src/Avalonia.FreeDesktop/DBusMenu.cs @@ -28,18 +28,18 @@ namespace Avalonia.FreeDesktop.DBusMenu Task EventGroupAsync((int id, string eventId, object data, uint timestamp)[] events); Task AboutToShowAsync(int Id); Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids); - Task WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action onError = null); - Task WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action onError = null); - Task WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action onError = null); + Task WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action? onError = null); + Task WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action? onError = null); + Task WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action? onError = null); } [Dictionary] class DBusMenuProperties { public uint Version { get; set; } = default (uint); - public string TextDirection { get; set; } = default (string); - public string Status { get; set; } = default (string); - public string[] IconThemePath { get; set; } = default (string[]); + public string? TextDirection { get; set; } = default (string); + public string? Status { get; set; } = default (string); + public string[]? IconThemePath { get; set; } = default (string[]); } @@ -50,7 +50,7 @@ namespace Avalonia.FreeDesktop.DBusMenu Task UnregisterWindowAsync(uint WindowId); Task<(string service, ObjectPath menuObjectPath)> GetMenuForWindowAsync(uint WindowId); Task<(uint, string, ObjectPath)[]> GetMenusAsync(); - Task WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action onError = null); - Task WatchWindowUnregisteredAsync(Action handler, Action onError = null); + Task WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action? onError = null); + Task WatchWindowUnregisteredAsync(Action handler, Action? onError = null); } } diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 206c24ad5e..c0511420a6 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -17,7 +17,7 @@ namespace Avalonia.FreeDesktop { public class DBusMenuExporter { - public static ITopLevelNativeMenuExporter TryCreateTopLevelNativeMenu(IntPtr xid) + public static ITopLevelNativeMenuExporter? TryCreateTopLevelNativeMenu(IntPtr xid) { if (DBusHelper.Connection == null) return null; @@ -37,10 +37,10 @@ namespace Avalonia.FreeDesktop { private readonly Connection _dbus; private readonly uint _xid; - private IRegistrar _registrar; + private IRegistrar? _registrar; private bool _disposed; private uint _revision = 1; - private NativeMenu _menu; + private NativeMenu? _menu; private readonly Dictionary _idsToItems = new Dictionary(); private readonly Dictionary _itemsToIds = new Dictionary(); private readonly HashSet _menus = new HashSet(); @@ -73,10 +73,10 @@ namespace Avalonia.FreeDesktop if (_appMenu) { await _dbus.RegisterObjectAsync(this); - _registrar = DBusHelper.Connection.CreateProxy( + _registrar = DBusHelper.Connection?.CreateProxy( "com.canonical.AppMenu.Registrar", "/com/canonical/AppMenu/Registrar"); - if (!_disposed) + if (!_disposed && _registrar is { }) await _registrar.RegisterWindowAsync(_xid, ObjectPath); } else @@ -109,9 +109,9 @@ namespace Avalonia.FreeDesktop public bool IsNativeMenuExported { get; private set; } - public event EventHandler OnIsNativeMenuExportedChanged; + public event EventHandler? OnIsNativeMenuExportedChanged; - public void SetNativeMenu(NativeMenu menu) + public void SetNativeMenu(NativeMenu? menu) { if (menu == null) menu = new NativeMenu(); @@ -153,7 +153,7 @@ namespace Avalonia.FreeDesktop Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background); } - private (NativeMenuItemBase item, NativeMenu menu) GetMenu(int id) + private (NativeMenuItemBase? item, NativeMenu? menu) GetMenu(int id) { if (id == 0) return (null, _menu); @@ -161,7 +161,7 @@ namespace Avalonia.FreeDesktop return (item, (item as NativeMenuItem)?.Menu); } - private void EnsureSubscribed(NativeMenu menu) + private void EnsureSubscribed(NativeMenu? menu) { if(menu!=null && _menus.Add(menu)) ((INotifyCollectionChanged)menu.Items).CollectionChanged += OnMenuItemsChanged; @@ -180,12 +180,12 @@ namespace Avalonia.FreeDesktop return id; } - private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e) + private void OnMenuItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) { QueueReset(); } - private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + private void OnItemPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { QueueReset(); } @@ -216,7 +216,7 @@ namespace Avalonia.FreeDesktop "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data" }; - object GetProperty((NativeMenuItemBase item, NativeMenu menu) i, string name) + object? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) { var (it, menu) = i; @@ -302,7 +302,7 @@ namespace Avalonia.FreeDesktop } private List> _reusablePropertyList = new List>(); - KeyValuePair[] GetProperties((NativeMenuItemBase item, NativeMenu menu) i, string[] names) + KeyValuePair[] GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) { if (names?.Length > 0 != true) names = AllProperties; @@ -336,7 +336,7 @@ namespace Avalonia.FreeDesktop return Task.FromResult(rv); } - (int, KeyValuePair[], object[]) GetLayout(NativeMenuItemBase item, NativeMenu menu, int depth, string[] propertyNames) + (int, KeyValuePair[], object[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames) { var id = item == null ? 0 : GetId(item); var props = GetProperties((item, menu), propertyNames); @@ -414,22 +414,22 @@ namespace Avalonia.FreeDesktop private event Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> ItemsPropertiesUpdated { add { } remove { } } - private event Action<(uint revision, int parent)> LayoutUpdated; + private event Action<(uint revision, int parent)>? LayoutUpdated; private event Action<(int id, uint timestamp)> ItemActivationRequested { add { } remove { } } private event Action PropertiesChanged { add { } remove { } } - async Task IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action onError) + async Task IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action? onError) { ItemsPropertiesUpdated += handler; return Disposable.Create(() => ItemsPropertiesUpdated -= handler); } - async Task IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action onError) + async Task IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action? onError) { LayoutUpdated += handler; return Disposable.Create(() => LayoutUpdated -= handler); } - async Task IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action onError) + async Task IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action? onError) { ItemActivationRequested+= handler; return Disposable.Create(() => ItemActivationRequested -= handler); diff --git a/src/Avalonia.FreeDesktop/DBusRequest.cs b/src/Avalonia.FreeDesktop/DBusRequest.cs index 940a476916..d84905324f 100644 --- a/src/Avalonia.FreeDesktop/DBusRequest.cs +++ b/src/Avalonia.FreeDesktop/DBusRequest.cs @@ -11,6 +11,6 @@ namespace Avalonia.FreeDesktop internal interface IRequest : IDBusObject { Task CloseAsync(); - Task WatchResponseAsync(Action<(uint response, IDictionary results)> handler, Action onError = null); + Task WatchResponseAsync(Action<(uint response, IDictionary results)> handler, Action? onError = null); } } diff --git a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs index d68c02bfd6..b69ea68a76 100644 --- a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs @@ -10,7 +10,7 @@ namespace Avalonia.FreeDesktop public IDisposable Listen(ObservableCollection mountedDrives) { Contract.Requires(mountedDrives != null); - return new LinuxMountedVolumeInfoListener(ref mountedDrives); + return new LinuxMountedVolumeInfoListener(ref mountedDrives!); } } } diff --git a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj index 97e58f8a64..b5c955a8a6 100644 --- a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj +++ b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj @@ -11,6 +11,7 @@ all + diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index ee4125101c..46a985c0e8 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -133,7 +133,7 @@ namespace Avalonia.LinuxFramebuffer.Output var device = gbm_create_device(card.Fd); _gbmTargetSurface = gbm_surface_create(device, modeInfo.Resolution.Width, modeInfo.Resolution.Height, GbmColorFormats.GBM_FORMAT_XRGB8888, GbmBoFlags.GBM_BO_USE_SCANOUT | GbmBoFlags.GBM_BO_USE_RENDERING); - if(_gbmTargetSurface == null) + if(_gbmTargetSurface == IntPtr.Zero) throw new InvalidOperationException("Unable to create GBM surface"); _eglDisplay = new EglDisplay(new EglInterface(eglGetProcAddress), false, 0x31D7, device, null); diff --git a/src/Avalonia.SourceGenerator/IsExternalInit.cs b/src/Shared/IsExternalInit.cs similarity index 100% rename from src/Avalonia.SourceGenerator/IsExternalInit.cs rename to src/Shared/IsExternalInit.cs From b63b84860ca69e453b0c719873be381a1dba7099 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 6 Jul 2022 09:58:36 +0200 Subject: [PATCH 108/171] fix(DataGrid): CS8073 The result of the expression is always 'true' --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index aaac3f8f9c..d42468f47e 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -5990,15 +5990,14 @@ namespace Avalonia.Controls /// The formatted string. private string FormatClipboardContent(DataGridRowClipboardEventArgs e) { - StringBuilder text = new StringBuilder(); - for (int cellIndex = 0; cellIndex < e.ClipboardRowContent.Count; cellIndex++) - { - DataGridClipboardCellContent cellContent = e.ClipboardRowContent[cellIndex]; - if (cellContent != null) - { - text.Append(cellContent.Content); - } - if (cellIndex < e.ClipboardRowContent.Count - 1) + var text = new StringBuilder(); + var clipboardRowContent = e.ClipboardRowContent; + var numberOfItem = clipboardRowContent.Count; + for (int cellIndex = 0; cellIndex < numberOfItem; cellIndex++) + { + var cellContent = clipboardRowContent[cellIndex]; + text.Append(cellContent.Content); + if (cellIndex < numberOfItem - 1) { text.Append('\t'); } From 230166d0bc894dc802dc85fc25ef19452caa414c Mon Sep 17 00:00:00 2001 From: Lubomir Tetak Date: Wed, 6 Jul 2022 14:54:33 +0200 Subject: [PATCH 109/171] EffectiveViewportChangedListeners thread safety improved --- src/Avalonia.Base/Layout/LayoutManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index fc988a8d6c..dd2db9304d 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -340,7 +340,7 @@ namespace Avalonia.Layout { for (var i = 0; i < count; ++i) { - var l = _effectiveViewportChangedListeners[i]; + var l = listeners[i]; if (!l.Listener.IsAttachedToVisualTree) { @@ -352,7 +352,7 @@ namespace Avalonia.Layout if (viewport != l.Viewport) { l.Listener.EffectiveViewportChanged(new EffectiveViewportChangedEventArgs(viewport)); - _effectiveViewportChangedListeners[i] = new EffectiveViewportChangedListener(l.Listener, viewport); + l.Viewport = viewport; } } } @@ -404,7 +404,7 @@ namespace Avalonia.Layout } } - private readonly struct EffectiveViewportChangedListener + private struct EffectiveViewportChangedListener { public EffectiveViewportChangedListener(ILayoutable listener, Rect viewport) { @@ -413,7 +413,7 @@ namespace Avalonia.Layout } public ILayoutable Listener { get; } - public Rect Viewport { get; } + public Rect Viewport { get; set; } } } } From 22971f56cb3eabc56d87d94ac8059aed78977aab Mon Sep 17 00:00:00 2001 From: Lubomir Tetak Date: Thu, 7 Jul 2022 10:20:30 +0200 Subject: [PATCH 110/171] EffectiveViewportChangedListeners must be reference type --- src/Avalonia.Base/Layout/LayoutManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index 91358adc05..b9ca6bfbd7 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -414,7 +414,7 @@ namespace Avalonia.Layout } } - private struct EffectiveViewportChangedListener + private class EffectiveViewportChangedListener { public EffectiveViewportChangedListener(ILayoutable listener, Rect viewport) { From 1812540ad9c722bb995e1cc2a9b60ca1b269cc8b Mon Sep 17 00:00:00 2001 From: Lubomir Tetak Date: Thu, 7 Jul 2022 10:44:19 +0200 Subject: [PATCH 111/171] unit test --- ...ayoutableTests_EffectiveViewportChanged.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/Layout/LayoutableTests_EffectiveViewportChanged.cs b/tests/Avalonia.Base.UnitTests/Layout/LayoutableTests_EffectiveViewportChanged.cs index 7c1a525002..a58d28004f 100644 --- a/tests/Avalonia.Base.UnitTests/Layout/LayoutableTests_EffectiveViewportChanged.cs +++ b/tests/Avalonia.Base.UnitTests/Layout/LayoutableTests_EffectiveViewportChanged.cs @@ -312,6 +312,30 @@ namespace Avalonia.Base.UnitTests.Layout }); } + [Fact] + public async Task Event_Unsubscribed_While_Inside_Callback() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas(); + var raised = 0; + + void OnTargetOnEffectiveViewportChanged(object s, EffectiveViewportChangedEventArgs e) + { + target.EffectiveViewportChanged -= OnTargetOnEffectiveViewportChanged; + ++raised; + } + target.EffectiveViewportChanged += OnTargetOnEffectiveViewportChanged; + + root.Child = target; + + await ExecuteInitialLayoutPass(root); + + Assert.Equal(1, raised); + }); + } + private TestRoot CreateRoot() => new TestRoot { Width = 1200, Height = 900 }; private Task ExecuteInitialLayoutPass(TestRoot root) From 6483b5edc778e8fbeb1a0c29d55c150f9d60965e Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Thu, 7 Jul 2022 10:46:06 +0200 Subject: [PATCH 112/171] Add Has method to IPseudoClasses --- src/Avalonia.Base/Controls/IPseudoClasses.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Avalonia.Base/Controls/IPseudoClasses.cs b/src/Avalonia.Base/Controls/IPseudoClasses.cs index eda521727f..45013ee069 100644 --- a/src/Avalonia.Base/Controls/IPseudoClasses.cs +++ b/src/Avalonia.Base/Controls/IPseudoClasses.cs @@ -19,5 +19,12 @@ namespace Avalonia.Controls /// /// The pseudoclass name. bool Remove(string name); + + /// + /// Returns whether a pseudoclass is present in the collection. + /// + /// The pseudoclass name. + /// Whether the pseudoclass is present. + bool Has(string name); } } From f80cf2d355f813d9294523984c6323398b4ef65d Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Thu, 7 Jul 2022 23:01:38 +0300 Subject: [PATCH 113/171] add base.OnAttachedToVisualTree(e) call to repeater --- src/Avalonia.Controls/Repeater/ItemsRepeater.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 3f42d95deb..ae663defd3 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -421,6 +421,7 @@ namespace Avalonia.Controls protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { + base.OnAttachedToVisualTree(e); InvalidateMeasure(); _viewportManager.ResetScrollers(); } From 012d3763693d332e05c12e6ba6ae0e06cf3b72e4 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 7 Jul 2022 20:48:37 -0400 Subject: [PATCH 114/171] Standardize converters --- .../Themes/Default/ColorPreviewer.xaml | 16 +++----- .../Themes/Default/ColorSlider.xaml | 22 ++++------- .../Themes/Default/ColorSpectrum.xaml | 31 ++++++--------- .../Themes/Default/Default.xaml | 14 ++++++- .../Themes/Fluent/ColorPicker.xaml | 13 +++---- .../Themes/Fluent/ColorPreviewer.xaml | 16 +++----- .../Themes/Fluent/ColorSlider.xaml | 22 ++++------- .../Themes/Fluent/ColorSpectrum.xaml | 31 ++++++--------- .../Themes/Fluent/ColorView.xaml | 39 +++++++++---------- .../Themes/Fluent/Fluent.xaml | 14 ++++++- 10 files changed, 101 insertions(+), 117 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml index 6ac0982408..ad07da2b16 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml @@ -1,14 +1,10 @@  - - - - + 80 40 @@ -36,11 +32,11 @@ Grid.Column="0" CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource LeftCornerRadiusFilterConverter}}" Tag="-2" - Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='-2'}" /> + Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='-2'}" /> + Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='-1'}" /> + Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='1'}" /> + Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='2'}" /> diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml index bf2f632fd8..729c5be313 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml @@ -1,13 +1,7 @@  - - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml index 8d77f8d3c1..84a94486c8 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml @@ -1,14 +1,10 @@  - - - - + 80 40 @@ -36,11 +32,11 @@ Grid.Column="0" CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource LeftCornerRadiusFilterConverter}}" Tag="-2" - Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='-2'}" /> + Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='-2'}" /> + Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='-1'}" /> + Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='1'}" /> + Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='2'}" /> diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml index bb6e026f9e..594a2c694f 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml @@ -1,13 +1,7 @@  - - - - - @@ -255,8 +252,8 @@ - @@ -363,12 +360,12 @@ Grid.Column="0" Content="RGB" CornerRadius="4,0,0,4" - IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Rgba}, Mode=TwoWay}" /> + IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" /> + IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=OneWay}"/> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=OneWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=OneWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=OneWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=OneWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=OneWay}" /> + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:converters="using:Avalonia.Controls.Converters"> + + + + + + + + + + + From 38bdb8a7b34051e4898aa9c0a1dc3a3fff25ae52 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 7 Jul 2022 20:53:21 -0400 Subject: [PATCH 115/171] Remove unused code --- .../ColorPicker/ColorPicker.cs | 8 +------- src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs index d34a91d1bb..39369bcbdb 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Avalonia.Controls +namespace Avalonia.Controls { /// /// Presents a color for user editing using a spectrum, palette and component sliders within a drop down. diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index bea982b7ea..89f1afb1ac 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -28,7 +28,6 @@ namespace Avalonia.Controls private TextBox? _hexTextBox; private TabControl? _tabControl; - private ObservableCollection _paletteColors = new ObservableCollection(); private ColorToHexConverter colorToHexConverter = new ColorToHexConverter(); protected bool ignorePropertyChanged = false; From 443977b85d1985dadf9f5248b4ca7f194503ab76 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 7 Jul 2022 21:10:02 -0400 Subject: [PATCH 116/171] Specify TargetType and DataType in most templates --- .../Themes/Default/ColorPreviewer.xaml | 2 +- .../Themes/Default/ColorSlider.xaml | 4 ++-- .../Themes/Default/ColorSpectrum.xaml | 2 +- .../Themes/Fluent/ColorPicker.xaml | 2 +- .../Themes/Fluent/ColorPreviewer.xaml | 2 +- .../Themes/Fluent/ColorSlider.xaml | 4 ++-- .../Themes/Fluent/ColorSpectrum.xaml | 2 +- .../Themes/Fluent/ColorView.xaml | 10 ++++++---- 8 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml index ad07da2b16..3a4efaa42a 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml @@ -13,7 +13,7 @@ - + - + @@ -93,7 +93,7 @@ - + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml index 95af16d12e..0e57f6b483 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml @@ -6,7 +6,7 @@ - + + + +"; + + var contentControl = AvaloniaRuntimeXamlLoader.Parse(xaml); + contentControl.Measure(new Size(10, 10)); + + var result = contentControl.GetTemplateChildren().OfType().First(); + + Assert.Equal("Hello", result.Content); + } + } + [Fact] public void Binds_To_Source() { From 142b878984db2f3be0977b4073a83ff3247ab2f0 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:05:25 -0400 Subject: [PATCH 120/171] Fix special case of compiled binding with Path child --- .../XamlIlBindingPathHelper.cs | 8 +++-- .../CompiledBindingExtensionTests.cs | 35 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index dd809956cd..8a115bf8fc 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -47,7 +47,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return startTypeResolver(); } - if (bindingPathAssignment.Values[0] is ParsedBindingPathNode bindingPathNode) + if (bindingPathAssignment.Values[0] is XamlIlBindingPathNode pathNode) + { + bindingResultType = pathNode.BindingResultType; + } + else if (bindingPathAssignment.Values[0] is ParsedBindingPathNode bindingPathNode) { var transformed = TransformBindingPath( context, @@ -63,7 +67,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } else { - throw new InvalidOperationException(); + throw new InvalidOperationException("Invalid state of Path property"); } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index e6df80f17e..754a819182 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -102,6 +102,41 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal(dataContext.StringProperty, textBlock.Text); } } + + [Fact] + public void ResolvesPathPassedByPropertyWithInnerItemTemplate() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("itemsControl"); + + var dataContext = new TestDataContext + { + ListProperty = + { + "Hello" + } + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.ListProperty, textBlock.Items); + } + } [Fact] public void ResolvesStreamTaskBindingCorrectly() From 2619ab17595b6317fe4afdf1bc0917cb325c5a1d Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:06:13 -0400 Subject: [PATCH 121/171] Fix compiled binding with TemplatedParent relative source --- .../AvaloniaXamlIlCompiler.cs | 6 ++-- .../AvaloniaXamlIlBindingPathParser.cs | 25 ++++++++++------ .../XamlIlBindingPathHelper.cs | 1 + .../CompiledBindingExtensionTests.cs | 30 +++++++++++++++++-- 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 04a61e5f10..e3a55feac9 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -45,10 +45,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlReorderClassesPropertiesTransformer() ); - InsertBefore( - new AvaloniaXamlIlBindingPathParser(), + InsertBefore( new AvaloniaXamlIlSelectorTransformer(), - new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), + new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), + new AvaloniaXamlIlBindingPathParser(), new AvaloniaXamlIlPropertyPathTransformer(), new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs index 890cbb69bf..60c880c6aa 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs @@ -121,11 +121,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { throw new XamlParseException("Only one of ElementName, Source, or RelativeSource specified as a binding source. Only one property is allowed.", binding); } - - var mode = relativeSourceObject.Children + + var modeProperty = relativeSourceObject.Children .OfType() - .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Mode") - ?.Values[0] is XamlAstTextNode modeAssignedValue ? modeAssignedValue.Text : null; + .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Mode")? + .Values.FirstOrDefault() as XamlAstTextNode + ?? relativeSourceObject.Arguments.OfType().FirstOrDefault(); + + var mode = modeProperty?.Text; if (relativeSourceObject.Arguments.Count == 0 && mode == null) { mode = "FindAncestor"; @@ -212,16 +215,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } else if (mode == "TemplatedParent") { - var parentType = context.ParentNodes().OfType() + var contentTemplateNode = context.ParentNodes().OfType() .FirstOrDefault(x => - x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate) - ?.TargetType.GetClrType(); - - if (parentType is null) + x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate); + if (contentTemplateNode is null) { throw new XamlParseException("A binding with a TemplatedParent RelativeSource has to be in a ControlTemplate.", binding); } + var parentType = contentTemplateNode.TargetType.GetClrType(); + if (parentType is null) + { + throw new XamlParseException("TargetType has to be set on ControlTemplate.", binding); + } + convertedNode = new TemplatedParentBindingExpressionNode { Type = parentType }; } else diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index 8a115bf8fc..ae29dcf9cb 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -244,6 +244,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions case TemplatedParentBindingExpressionNode templatedParent: var templatedParentField = context.GetAvaloniaTypes().StyledElement.GetAllFields() .FirstOrDefault(f => f.IsStatic && f.IsPublic && f.Name == "TemplatedParentProperty"); + nodes.Add(new SelfPathElementNode(selfType)); nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode( templatedParentField, templatedParent.Type)); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 754a819182..5ca25a176c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Globalization; +using System.Linq; using System.Reactive.Subjects; -using System.Text; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Presenters; @@ -17,8 +17,6 @@ using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.UnitTests; -using JetBrains.Annotations; -using XamlX; using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions @@ -676,6 +674,32 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void ResolvesRelativeSourceBindingFromTemplate() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + +"; + + var contentControl = AvaloniaRuntimeXamlLoader.Parse(xaml); + contentControl.Measure(new Size(10, 10)); + + var result = contentControl.GetTemplateChildren().OfType().First(); + Assert.Equal("Hello", result.Content); + } + } [Fact] public void ResolvesElementNameInTemplate() From 5d234cd49c7e7da9c446d7450f06267053a71999 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:06:30 -0400 Subject: [PATCH 122/171] Remove reflection bindings from standard controls --- .../Diagnostics/Controls/FilterTextBox.axaml | 2 +- src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml | 2 +- src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml index b7995c38e3..a1a2ab34bf 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml @@ -19,7 +19,7 @@ Classes="textBoxClearButton" ToolTip.Tip="Clear" Cursor="Hand" - Command="{ReflectionBinding $parent[TextBox].Clear}" + Command="{Binding $parent[TextBox].Clear}" Opacity="0.5" /> diff --git a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml index a463334a76..482b219589 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml @@ -21,7 +21,7 @@ - + @@ -32,7 +32,7 @@ - + From ff3b23b2d6eb3e874ad2c71393181b7756ffb5ae Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:17:05 -0400 Subject: [PATCH 123/171] Formatting --- .../Transformers/AvaloniaXamlIlBindingPathParser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs index 60c880c6aa..6150265cf8 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs @@ -121,7 +121,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { throw new XamlParseException("Only one of ElementName, Source, or RelativeSource specified as a binding source. Only one property is allowed.", binding); } - + var modeProperty = relativeSourceObject.Children .OfType() .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Mode")? @@ -226,7 +226,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers var parentType = contentTemplateNode.TargetType.GetClrType(); if (parentType is null) { - throw new XamlParseException("TargetType has to be set on ControlTemplate.", binding); + throw new XamlParseException("TargetType has to be set on ControlTemplate or it should be defined inside of a Style.", binding); } convertedNode = new TemplatedParentBindingExpressionNode { Type = parentType }; From 34f8f3d5564d3d7b1584746c8dcaa887a5e235f5 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:57:06 -0400 Subject: [PATCH 124/171] Fix DataType set on Binding extension + add tests --- .../AvaloniaXamlIlBindingPathTransformer.cs | 5 + .../CompiledBindings/CompiledBindingPath.cs | 2 + .../CompiledBindingExtensionTests.cs | 95 +++++++++++++++++++ 3 files changed, 102 insertions(+) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs index 3cc3504e16..e509d47932 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs @@ -105,6 +105,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { startType = TypeReferenceResolver.ResolveType(context, text.Text, isMarkupExtension: false, text, strict: true).Type; } + + if (dataTypeProperty?.Values.Count is 1 && dataTypeProperty.Values[0] is XamlTypeExtensionNode typeNode) + { + startType = typeNode.Value.GetClrType(); + } Func startTypeResolver = startType is not null ? () => startType : () => { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs index 73a14fd437..f78d4867ff 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs @@ -74,6 +74,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings return pathRoot ?? new EmptyExpressionNode(); } + internal IEnumerable Elements => _elements; + internal SourceMode SourceMode => _elements.Count > 0 && _elements[0] is IControlSourceBindingPathElement ? SourceMode.Control : SourceMode.Data; internal object RawSource { get; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 5ca25a176c..215ae4d54f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -9,10 +9,13 @@ using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Data.Converters; using Avalonia.Data.Core; using Avalonia.Input; using Avalonia.Markup.Data; +using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings; using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; using Avalonia.Metadata; @@ -135,6 +138,56 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal(dataContext.ListProperty, textBlock.Items); } } + + [Fact] + public void ResolvesDataTypeFromBindingProperty() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } + + [Fact] + public void ResolvesDataTypeFromBindingProperty_TypeExtension() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } [Fact] public void ResolvesStreamTaskBindingCorrectly() @@ -1479,6 +1532,43 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void ResolvesDataTypeForAssignBinding() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" +"; + var control = (AssignBindingControl)AvaloniaRuntimeXamlLoader.Load(xaml); + var compiledPath = ((CompiledBindingExtension)control.X).Path; + + var node = Assert.IsType(Assert.Single(compiledPath.Elements)); + Assert.Equal(typeof(string), node.Property.PropertyType); + } + } + + [Fact] + public void ResolvesDataTypeForAssignBinding_FromBindingProperty() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" +"; + var control = (AssignBindingControl)AvaloniaRuntimeXamlLoader.Load(xaml); + var compiledPath = ((CompiledBindingExtension)control.X).Path; + + var node = Assert.IsType(Assert.Single(compiledPath.Elements)); + Assert.Equal(typeof(string), node.Property.PropertyType); + } + } + void Throws(string type, Action cb) { try @@ -1656,4 +1746,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } public class CustomDataTemplateInherit : CustomDataTemplate { } + + public class AssignBindingControl : Control + { + [AssignBinding] public IBinding X { get; set; } + } } From 8e57d0b7c6b52e6598570795e206a4a27e7e13b9 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Fri, 8 Jul 2022 11:52:55 +0300 Subject: [PATCH 125/171] Fix bindings to Types. --- .../Data/Core/Plugins/InpcPropertyAccessorPlugin.cs | 2 +- tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index b93bf87fdf..91d69b5d3d 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -55,7 +55,7 @@ namespace Avalonia.Data.Core.Plugins private PropertyInfo? GetFirstPropertyWithName(object instance, string propertyName) { - if (instance is IReflectableType reflectableType) + if (instance is IReflectableType reflectableType && instance is not Type) return reflectableType.GetTypeInfo().GetProperty(propertyName, PropertyBindingFlags); var type = instance.GetType(); diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index 11a22f0dec..f4a4a2934f 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -637,6 +637,17 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("baz", source["Foo"]); } + [Fact] + public void Binding_To_Types_Should_Work() + { + var type = typeof(string); + var textBlock = new TextBlock() { DataContext = type }; + using (textBlock.Bind(TextBlock.TextProperty, new Binding("Name"))) + { + Assert.Equal("String", textBlock.Text); + }; + } + private class StyledPropertyClass : AvaloniaObject { public static readonly StyledProperty DoubleValueProperty = From 0553c7ceabf01199a0c45cf21c3e6126fde1e660 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 8 Jul 2022 15:32:09 +0100 Subject: [PATCH 126/171] add a failing unit test --- .../WindowTests.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index a8c9b68d12..ab4fd566c8 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -540,6 +540,51 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(window.Position, expectedPosition); } } + + [Fact] + public void Window_Should_Be_Centered_When_WindowStartupLocation_Is_CenterScreen_1080_175() + { + var screen1 = new Mock(1.75, new PixelRect(new PixelSize(1920, 1080)), new PixelRect(new PixelSize(1920, 966)), true); + + var screens = new Mock(); + screens.Setup(x => x.AllScreens).Returns(new Screen[] { screen1.Object }); + screens.Setup(x => x.ScreenFromPoint(It.IsAny())).Returns(screen1.Object); + + + var windowImpl = MockWindowingPlatform.CreateWindowMock(); + windowImpl.Setup(x => x.DesktopScaling).Returns(1.75); + windowImpl.Setup(x => x.RenderScaling).Returns(1.75); + windowImpl.Setup(x => x.Screen).Returns(screens.Object); + + var clientSize = new Size(801.142, 366); + + windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize); + + windowImpl.Setup(x => x.Resize(It.IsAny(), It.IsAny())) + .Callback((x, y) => + { + clientSize = x;//.Constrain(screen1.Object.Bounds.Size.ToSize(1.75)); + windowImpl.Object.Resized?.Invoke(clientSize, y); + }); + + windowImpl.Setup(x => x.FrameSize).Returns(() => clientSize.Inflate(new Thickness(5, 25, 5, 5))); + + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(windowImpl.Object); + window.WindowStartupLocation = WindowStartupLocation.CenterScreen; + window.MinWidth = 720; + window.MinHeight = 480; + //window.Width = 720; + //window.Height = 480; + + window.Show(); + + var expectedPosition = new PixelPoint(321, 36); + + Assert.Equal(window.Position, expectedPosition); + } + } [Fact] public void Window_Should_Be_Centered_Relative_To_Owner_When_WindowStartupLocation_Is_CenterOwner() From b9aa49bc05d039b01a52e0a10d0494be6bdb92d9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 8 Jul 2022 18:31:49 +0100 Subject: [PATCH 127/171] Add failing unit test. --- .../Avalonia.Controls.UnitTests/WindowTests.cs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index ab4fd566c8..3677d62050 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -542,28 +542,25 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Window_Should_Be_Centered_When_WindowStartupLocation_Is_CenterScreen_1080_175() + public void Window_Should_Be_Sized_To_MinSize_If_InitialSize_Less_Than_MinSize() { var screen1 = new Mock(1.75, new PixelRect(new PixelSize(1920, 1080)), new PixelRect(new PixelSize(1920, 966)), true); - var screens = new Mock(); screens.Setup(x => x.AllScreens).Returns(new Screen[] { screen1.Object }); screens.Setup(x => x.ScreenFromPoint(It.IsAny())).Returns(screen1.Object); - var windowImpl = MockWindowingPlatform.CreateWindowMock(); windowImpl.Setup(x => x.DesktopScaling).Returns(1.75); windowImpl.Setup(x => x.RenderScaling).Returns(1.75); windowImpl.Setup(x => x.Screen).Returns(screens.Object); - var clientSize = new Size(801.142, 366); + var clientSize = new Size(400.142, 366); windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize); - windowImpl.Setup(x => x.Resize(It.IsAny(), It.IsAny())) .Callback((x, y) => { - clientSize = x;//.Constrain(screen1.Object.Bounds.Size.ToSize(1.75)); + clientSize = x; windowImpl.Object.Resized?.Invoke(clientSize, y); }); @@ -575,14 +572,10 @@ namespace Avalonia.Controls.UnitTests window.WindowStartupLocation = WindowStartupLocation.CenterScreen; window.MinWidth = 720; window.MinHeight = 480; - //window.Width = 720; - //window.Height = 480; window.Show(); - - var expectedPosition = new PixelPoint(321, 36); - - Assert.Equal(window.Position, expectedPosition); + + Assert.Equal(new Size(720, 480), window.Bounds.Size); } } From 05f4bcad80fdbf621f57c34303b2207eb35ea519 Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 8 Jul 2022 23:40:08 -0400 Subject: [PATCH 128/171] Enable compiled bindings for ColorPicker and ColorView --- .../Themes/Fluent/ColorPicker.xaml | 2 +- .../Themes/Fluent/ColorView.xaml | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index b9133d6bfb..b34ac68be5 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -1,7 +1,7 @@  + x:CompileBindings="True"> diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index a0687ed972..bcdb72d96c 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -5,7 +5,7 @@ xmlns:primitives="using:Avalonia.Controls.Primitives" xmlns:pc="clr-namespace:Avalonia.Controls.Primitives.Converters;assembly=Avalonia.Controls.ColorPicker" xmlns:globalization="clr-namespace:System.Globalization;assembly=mscorlib" - x:CompileBindings="False"> + x:CompileBindings="True"> @@ -244,12 +244,20 @@ - - + From 8f0b3911a5216dee5d44b5d5719f34b3a0ef2973 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Jul 2022 12:22:57 +0200 Subject: [PATCH 129/171] Added failing tests for MenuItem.TemplatedParent. Seems #8412 caused `Menu.TemplatedParent` to get set to the parent `MenuItem` when re-opening a popup. --- .../MenuItemTests.cs | 44 ++++++ .../Primitives/PopupTests.cs | 147 +++++++++++++++++- 2 files changed, 190 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index db31d22b4f..d25a790fde 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -8,6 +8,7 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Platform; using Avalonia.UnitTests; +using Avalonia.VisualTree; using Moq; using Xunit; @@ -301,6 +302,49 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(3, canExecuteCallCount); } } + + + [Fact] + public void TemplatedParent_Should_Not_Be_Applied_To_Submenus() + { + using (Application()) + { + MenuItem topLevelMenu; + MenuItem childMenu1; + MenuItem childMenu2; + var menu = new Menu + { + Items = new[] + { + (topLevelMenu = new MenuItem + { + Header = "Foo", + Items = new[] + { + (childMenu1 = new MenuItem { Header = "Bar" }), + (childMenu2 = new MenuItem { Header = "Baz" }), + } + }), + } + }; + + var window = new Window { Content = menu }; + window.LayoutManager.ExecuteInitialLayoutPass(); + + topLevelMenu.IsSubMenuOpen = true; + + Assert.True(((IVisual)childMenu1).IsAttachedToVisualTree); + Assert.Null(childMenu1.TemplatedParent); + Assert.Null(childMenu2.TemplatedParent); + + topLevelMenu.IsSubMenuOpen = false; + topLevelMenu.IsSubMenuOpen = true; + + Assert.Null(childMenu1.TemplatedParent); + Assert.Null(childMenu2.TemplatedParent); + } + } + private IDisposable Application() { var screen = new PixelRect(new PixelPoint(), new PixelSize(100, 100)); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index d9cb40d1cc..5f91f2e2a1 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -295,7 +295,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent() + public void ContentControl_With_Popup_In_Template_Should_Set_TemplatedParent() { // Test uses OverlayPopupHost default template using (CreateServices()) @@ -384,6 +384,134 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void ItemsControl_With_Popup_In_Template_Should_Set_TemplatedParent() + { + // Test uses OverlayPopupHost default template + using (CreateServices()) + { + PopupItemsControl target; + var item = new Border(); + var root = PreparedWindow(target = new PopupItemsControl + { + Items = new[] { item }, + Template = new FuncControlTemplate(PopupItemsControlTemplate), + }); ; + root.Show(); + + target.ApplyTemplate(); + + var popup = (Popup)target.GetTemplateChildren().First(x => x.Name == "popup"); + popup.Open(); + + var popupRoot = (Control)popup.Host; + popupRoot.Measure(Size.Infinity); + popupRoot.Arrange(new Rect(popupRoot.DesiredSize)); + + var children = popupRoot.GetVisualDescendants().ToList(); + var types = children.Select(x => x.GetType().Name).ToList(); + + if (UsePopupHost) + { + Assert.Equal( + new[] + { + "LayoutTransformControl", + "VisualLayerManager", + "ContentPresenter", + "ItemsPresenter", + "StackPanel", + "Border", + }, + types); + } + else + { + Assert.Equal( + new[] + { + "LayoutTransformControl", + "Panel", + "Border", + "VisualLayerManager", + "ContentPresenter", + "ItemsPresenter", + "StackPanel", + "Border", + }, + types); + } + + var templatedParents = children + .OfType() + .Select(x => x.TemplatedParent).ToList(); + + if (UsePopupHost) + { + Assert.Equal( + new object[] + { + popupRoot, + popupRoot, + popupRoot, + target, + target, + null, + }, + templatedParents); + } + else + { + Assert.Equal( + new object[] + { + popupRoot, + popupRoot, + popupRoot, + popupRoot, + popupRoot, + target, + target, + null, + }, + templatedParents); + } + } + } + + [Fact] + public void Should_Not_Overwrite_TemplatedParent_Of_Item_In_ItemsControl_With_Popup_On_Second_Open() + { + // Test uses OverlayPopupHost default template + using (CreateServices()) + { + PopupItemsControl target; + var item = new Border(); + var root = PreparedWindow(target = new PopupItemsControl + { + Items = new[] { item }, + Template = new FuncControlTemplate(PopupItemsControlTemplate), + }); + root.Show(); + + target.ApplyTemplate(); + + var popup = (Popup)target.GetTemplateChildren().First(x => x.Name == "popup"); + popup.Open(); + + var popupRoot = (Control)popup.Host; + popupRoot.Measure(Size.Infinity); + popupRoot.Arrange(new Rect(popupRoot.DesiredSize)); + + Assert.Null(item.TemplatedParent); + + popup.Close(); + popup.Open(); + + Assert.Null(item.TemplatedParent); + } + } + [Fact] public void DataContextBeginUpdate_Should_Not_Be_Called_For_Controls_That_Dont_Inherit() { @@ -979,10 +1107,27 @@ namespace Avalonia.Controls.UnitTests.Primitives }.RegisterInNameScope(scope); } + private static IControl PopupItemsControlTemplate(PopupItemsControl control, INameScope scope) + { + return new Popup + { + Name = "popup", + PlacementTarget = control, + Child = new ItemsPresenter + { + [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], + } + }.RegisterInNameScope(scope); + } + private class PopupContentControl : ContentControl { } + private class PopupItemsControl : ItemsControl + { + } + private class TestControl : Decorator { public event EventHandler DataContextBeginUpdate; From 78a7257a5a3a0cd33ad77382d4d0c29759263dfa Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Jul 2022 12:32:22 +0200 Subject: [PATCH 130/171] Don't apply templated parent if already set. When applying the `TemplatedParent`, don't move into trees that already have the `TemplatedParent` property set - in this case we can assume that the templated parent has already been set for this tree, and it fixes the problem with menus added in the previous commit. --- src/Avalonia.Controls/Primitives/TemplatedControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 4403bfce51..c3e0f3e523 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -396,7 +396,7 @@ namespace Avalonia.Controls.Primitives for (var i = 0; i < count; i++) { - if (children[i] is IStyledElement child) + if (children[i] is IStyledElement child && child.TemplatedParent is null) { ApplyTemplatedParent(child, templatedParent); } From 5da9d528c9a9901afa6cd6e856b9055022cdb706 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Jul 2022 14:50:52 +0200 Subject: [PATCH 131/171] Added missing param docs. --- src/Avalonia.Controls/Primitives/TemplatedControl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index c3e0f3e523..e50f991cdb 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -387,6 +387,7 @@ namespace Avalonia.Controls.Primitives /// Sets the TemplatedParent property for the created template children. /// /// The control. + /// The templated parent to apply. internal static void ApplyTemplatedParent(IStyledElement control, ITemplatedControl? templatedParent) { control.SetValue(TemplatedParentProperty, templatedParent); From 435419588a3702efc1f67eec7a6865c64a03cbb5 Mon Sep 17 00:00:00 2001 From: Eric M Date: Wed, 6 Jul 2022 21:18:40 +1000 Subject: [PATCH 132/171] Allow customisation of ProgressBar text format. --- .../ControlCatalog/Pages/ProgressBarPage.xaml | 27 ++++++++++++++----- .../Converters/StringFormatConverter.cs | 27 +++++++++++++++++++ src/Avalonia.Controls/ProgressBar.cs | 23 ++++++++++++++++ .../Controls/ProgressBar.xaml | 21 ++++++++++++--- .../Controls/ProgressBar.xaml | 16 ++++++++++- 5 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 src/Avalonia.Controls/Converters/StringFormatConverter.cs diff --git a/samples/ControlCatalog/Pages/ProgressBarPage.xaml b/samples/ControlCatalog/Pages/ProgressBarPage.xaml index db7d7d3280..8e73f1d0f5 100644 --- a/samples/ControlCatalog/Pages/ProgressBarPage.xaml +++ b/samples/ControlCatalog/Pages/ProgressBarPage.xaml @@ -1,22 +1,37 @@ A progress bar control - + + + Maximum + + + + Minimum + + + + Progress Text Format + + - + - + - - + + - + diff --git a/src/Avalonia.Controls/Converters/StringFormatConverter.cs b/src/Avalonia.Controls/Converters/StringFormatConverter.cs new file mode 100644 index 0000000000..ae920dac7e --- /dev/null +++ b/src/Avalonia.Controls/Converters/StringFormatConverter.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Avalonia.Data; +using Avalonia.Data.Converters; + +namespace Avalonia.Controls.Converters; + +/// +/// Calls on the passed in values, where the first element in the list +/// is the string, and everything after it is passed into the object array in order. +/// +public class StringFormatConverter : IMultiValueConverter +{ + public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) + { + try + { + return string.Format((string)values[0]!, values.Skip(1).ToArray()); + } + catch (Exception e) + { + return new BindingNotification(e, BindingErrorType.Error); + } + } +} diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 1075328c67..70edeadfd9 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -96,6 +96,7 @@ namespace Avalonia.Controls } } + private double _percentage; private double _indeterminateStartingOffset; private double _indeterminateEndingOffset; private Border? _indicator; @@ -106,9 +107,17 @@ namespace Avalonia.Controls public static readonly StyledProperty ShowProgressTextProperty = AvaloniaProperty.Register(nameof(ShowProgressText)); + public static readonly StyledProperty ProgressTextFormatProperty = + AvaloniaProperty.Register(nameof(ProgressTextFormat), "{1:0}%"); + public static readonly StyledProperty OrientationProperty = AvaloniaProperty.Register(nameof(Orientation), Orientation.Horizontal); + public static readonly DirectProperty PercentageProperty = + AvaloniaProperty.RegisterDirect( + nameof(Percentage), + o => o.Percentage); + [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] public static readonly DirectProperty IndeterminateStartingOffsetProperty = AvaloniaProperty.RegisterDirect( @@ -123,6 +132,12 @@ namespace Avalonia.Controls p => p.IndeterminateEndingOffset, (p, o) => p.IndeterminateEndingOffset = o); + public double Percentage + { + get { return _percentage; } + private set { SetAndRaise(PercentageProperty, ref _percentage, value); } + } + [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] public double IndeterminateStartingOffset { @@ -165,6 +180,12 @@ namespace Avalonia.Controls set => SetValue(ShowProgressTextProperty, value); } + public string ProgressTextFormat + { + get => GetValue(ProgressTextFormatProperty); + set => SetValue(ProgressTextFormatProperty, value); + } + public Orientation Orientation { get => GetValue(OrientationProperty); @@ -245,6 +266,8 @@ namespace Avalonia.Controls _indicator.Width = bounds.Width * percent; else _indicator.Height = bounds.Height * percent; + + Percentage = percent * 100; } } } diff --git a/src/Avalonia.Themes.Default/Controls/ProgressBar.xaml b/src/Avalonia.Themes.Default/Controls/ProgressBar.xaml index fd847b5d65..3f684f3936 100644 --- a/src/Avalonia.Themes.Default/Controls/ProgressBar.xaml +++ b/src/Avalonia.Themes.Default/Controls/ProgressBar.xaml @@ -1,4 +1,6 @@ - + @@ -11,10 +13,13 @@ From 4ca4986b4f6e6bf31e7e53a5de531895a33b8ee7 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Tue, 12 Jul 2022 15:58:58 +0200 Subject: [PATCH 147/171] rename --- src/Avalonia.Base/Controls/IPseudoClasses.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Controls/IPseudoClasses.cs b/src/Avalonia.Base/Controls/IPseudoClasses.cs index 45013ee069..438b05a8cf 100644 --- a/src/Avalonia.Base/Controls/IPseudoClasses.cs +++ b/src/Avalonia.Base/Controls/IPseudoClasses.cs @@ -25,6 +25,6 @@ namespace Avalonia.Controls /// /// The pseudoclass name. /// Whether the pseudoclass is present. - bool Has(string name); + bool Contains(string name); } } From d1f3c4d691c3e8336b3f98dc7f524b3fc30f515b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 12 Jul 2022 17:04:03 +0300 Subject: [PATCH 148/171] [mac] Don't update layer if IOSurface doesn't have any valid content yet --- native/Avalonia.Native/src/OSX/rendertarget.mm | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/rendertarget.mm b/native/Avalonia.Native/src/OSX/rendertarget.mm index 266d0345d1..2075cc85ab 100644 --- a/native/Avalonia.Native/src/OSX/rendertarget.mm +++ b/native/Avalonia.Native/src/OSX/rendertarget.mm @@ -13,6 +13,7 @@ { @public IOSurfaceRef surface; @public AvnPixelSize size; + @public bool hasContent; @public float scale; ComPtr _context; GLuint _framebuffer, _texture, _renderbuffer; @@ -41,6 +42,7 @@ self->scale = scale; self->size = size; self->_context = context; + self->hasContent = false; return self; } @@ -92,6 +94,7 @@ _context->MakeCurrent(release.getPPV()); glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); glFlush(); + self->hasContent = true; } -(void) dealloc @@ -170,6 +173,8 @@ static IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(IOSurfaceRenderTarget* ta @synchronized (lock) { if(_layer == nil) return; + if(!surface->hasContent) + return; [CATransaction begin]; [_layer setContents: nil]; if(surface != nil) @@ -213,6 +218,7 @@ static IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(IOSurfaceRenderTarget* ta memcpy(pSurface + y*sstride, pFb + y*fstride, wbytes); } IOSurfaceUnlock(surf, 0, nil); + surface->hasContent = true; [self updateLayer]; return S_OK; } From ecca63949f3377d566197712a512c42431fca3aa Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 12 Jul 2022 19:43:24 +0300 Subject: [PATCH 149/171] Use Blit when available --- .../Composition/Server/ServerCompositionTarget.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 3351caf0c6..0fde86e484 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -122,9 +122,12 @@ namespace Avalonia.Rendering.Composition.Server targetContext.Clear(Colors.Transparent); targetContext.Transform = Matrix.Identity; - targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1, - new Rect(_layerSize), - new Rect(Size), BitmapInterpolationMode.LowQuality); + if (_layer.CanBlit) + _layer.Blit(targetContext); + else + targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1, + new Rect(_layerSize), + new Rect(Size), BitmapInterpolationMode.LowQuality); if (DrawDirtyRects) From 551f35a760f08062be6e8d41f05c4d58495a8493 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 13 Jul 2022 07:41:55 +0300 Subject: [PATCH 150/171] Cleanup --- ...cs => ServerCompositionContainerVisual.cs} | 0 .../ServerCompositionLinearGradientBrush.cs | 22 ------------------- ...erverCompositionVisual.DirtyProperties.cs} | 0 ...erVisual.cs => ServerCompositionVisual.cs} | 0 4 files changed, 22 deletions(-) rename src/Avalonia.Base/Rendering/Composition/Server/{ServerContainerVisual.cs => ServerCompositionContainerVisual.cs} (100%) delete mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs rename src/Avalonia.Base/Rendering/Composition/Server/{ServerVisual.DirtyProperties.cs => ServerCompositionVisual.DirtyProperties.cs} (100%) rename src/Avalonia.Base/Rendering/Composition/Server/{ServerVisual.cs => ServerCompositionVisual.cs} (100%) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs similarity index 100% rename from src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs rename to src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs deleted file mode 100644 index c421cdcfb0..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Avalonia.Media; - -namespace Avalonia.Rendering.Composition.Server -{ - internal partial class ServerCompositionLinearGradientBrush - { - /* - protected override void UpdateBackendBrush(ICbBrush brush) - { - var stopColors = new Color[Stops.List.Count]; - var offsets = new float[Stops.List.Count]; - for (var c = 0; c < Stops.List.Count; c++) - { - stopColors[c] = Stops.List[c].Color; - offsets[c] = Stops.List[c].Offset; - } - - ((ICbLinearGradientBrush) brush).Update(StartPoint, EndPoint, stopColors, offsets, ExtendMode); - }*/ - - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs similarity index 100% rename from src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs rename to src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs similarity index 100% rename from src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs rename to src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs From 221a0f134148914ccaff159e60fbc1859fef5924 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 13 Jul 2022 07:44:05 +0300 Subject: [PATCH 151/171] Removed workaround from headless platform memory measurement code --- samples/ControlCatalog.NetCore/Program.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 07d514df6b..cfa016d814 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -60,12 +60,6 @@ namespace ControlCatalog.NetCore }) .AfterSetup(_ => { - static Task LowPriorityDelay(int ms) - { - return Task.Delay(ms).ContinueWith(_ => - Dispatcher.UIThread.InvokeAsync(() => { }, DispatcherPriority.Background)) - .Unwrap(); - } DispatcherTimer.RunOnce(async () => { var window = ((IClassicDesktopStyleApplicationLifetime)Application.Current.ApplicationLifetime) @@ -77,7 +71,7 @@ namespace ControlCatalog.NetCore continue; Console.WriteLine("Selecting " + page.Header); tc.SelectedItem = page; - await LowPriorityDelay(20); + await Task.Delay(50); } Console.WriteLine("Selecting the first page"); tc.SelectedItem = tc.Items.OfType().First(); @@ -86,7 +80,7 @@ namespace ControlCatalog.NetCore for (var c = 0; c < 3; c++) { GC.Collect(2, GCCollectionMode.Forced); - await Task.Delay(00); + await Task.Delay(50); } void FormatMem(string metric, long bytes) From 7746b79fb5d21a17d055cb2b7dbdbd1c1577a5bc Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 13 Jul 2022 10:46:31 +0100 Subject: [PATCH 152/171] close app after running --- tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs b/tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs index b3385d8ee7..70052b672c 100644 --- a/tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs +++ b/tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs @@ -57,6 +57,7 @@ namespace Avalonia.IntegrationTests.Appium { try { + Session.CloseApp(); Session.Close(); } catch From e0c1bae8422d052d55085bb24507e04480cc0fe5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 13 Jul 2022 10:56:15 +0100 Subject: [PATCH 153/171] use pkill to close app. --- tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs | 1 - .../Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs b/tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs index 70052b672c..b3385d8ee7 100644 --- a/tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs +++ b/tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs @@ -57,7 +57,6 @@ namespace Avalonia.IntegrationTests.Appium { try { - Session.CloseApp(); Session.Close(); } catch diff --git a/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh b/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh index 635c593f37..b3cba2c5ea 100755 --- a/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh +++ b/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh @@ -6,4 +6,6 @@ git clean -xdf ./build.sh CompileNative ./samples/IntegrationTestApp/bundle.sh open -n ./samples/IntegrationTestApp/bin/Debug/net6.0/osx-arm64/publish/IntegrationTestApp.app +pkill IntegrationTestApp dotnet test tests/Avalonia.IntegrationTests.Appium/ -l "console;verbosity=detailed" +pkill IntegrationTestApp From d59c9feac9a0d70494fef36a5718af37b1a7f075 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 13 Jul 2022 11:11:58 +0100 Subject: [PATCH 154/171] make sure any existing instances are closed. --- tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh b/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh index b3cba2c5ea..5018e78d68 100755 --- a/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh +++ b/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh @@ -3,6 +3,7 @@ SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) cd "$SCRIPT_DIR"/../.. || exit git clean -xdf +pkill IntegrationTestApp ./build.sh CompileNative ./samples/IntegrationTestApp/bundle.sh open -n ./samples/IntegrationTestApp/bin/Debug/net6.0/osx-arm64/publish/IntegrationTestApp.app From af0014b735115fea8f3cd7527a51295719a6a6b6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 13 Jul 2022 12:16:48 +0200 Subject: [PATCH 155/171] Added --debug switch to benchmarks. To be able to run benchmarks in the debugger, you need to pass `DebugInProcessConfig` to the benchmark switcher. There doesn't seem to be any inbuilt switch to enable this from the command-line, so added one to the benchmarks projects in order to be able to debug benchmarks more easily. --- tests/Avalonia.Benchmarks/Program.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Benchmarks/Program.cs b/tests/Avalonia.Benchmarks/Program.cs index 903778ff00..8f7aa3eb79 100644 --- a/tests/Avalonia.Benchmarks/Program.cs +++ b/tests/Avalonia.Benchmarks/Program.cs @@ -1,6 +1,8 @@ +using System.Collections.Generic; using System.Linq; using System.Reflection; using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; using BenchmarkDotNet.Running; namespace Avalonia.Benchmarks @@ -19,7 +21,17 @@ namespace Avalonia.Benchmarks .ThenBy(t => t.Name) .ToArray(); var benchmarkSwitcher = new BenchmarkSwitcher(benchmarks); - benchmarkSwitcher.Run(args); + IConfig config = null; + + if (args.Contains("--debug")) + { + config = new DebugInProcessConfig(); + var a = new List(args); + a.Remove("--debug"); + args = a.ToArray(); + } + + benchmarkSwitcher.Run(args, config); } } } From 5d69d1648da64674d2e3ae15707790c025933121 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 13 Jul 2022 15:57:11 +0100 Subject: [PATCH 156/171] run appium and restart it each time. --- .../Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh b/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh index 5018e78d68..b03a8a7a69 100755 --- a/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh +++ b/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh @@ -3,6 +3,8 @@ SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) cd "$SCRIPT_DIR"/../.. || exit git clean -xdf +pkill node +appium & pkill IntegrationTestApp ./build.sh CompileNative ./samples/IntegrationTestApp/bundle.sh @@ -10,3 +12,4 @@ open -n ./samples/IntegrationTestApp/bin/Debug/net6.0/osx-arm64/publish/Integrat pkill IntegrationTestApp dotnet test tests/Avalonia.IntegrationTests.Appium/ -l "console;verbosity=detailed" pkill IntegrationTestApp +pkill node From ebf464ad93173cccb46470c4363b0ea322101c9e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 14 Jul 2022 13:12:16 +0300 Subject: [PATCH 157/171] Enable compositing renderer by default for desktop platforms --- samples/ControlCatalog.NetCore/Program.cs | 6 ++---- src/Avalonia.X11/X11Platform.cs | 4 ++-- src/Windows/Avalonia.Win32/Win32Platform.cs | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index cfa016d814..d98a068d84 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -113,13 +113,11 @@ namespace ControlCatalog.NetCore { EnableMultiTouch = true, UseDBusMenu = true, - EnableIme = true, - UseCompositor = true + EnableIme = true }) .With(new Win32PlatformOptions { - EnableMultitouch = true, - UseCompositor = true + EnableMultitouch = true }) .UseSkia() .AfterSetup(builder => diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index b5fbebca4b..edb320d4f0 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -235,8 +235,8 @@ namespace Avalonia /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. /// public bool UseDeferredRendering { get; set; } = true; - - public bool UseCompositor { get; set; } + + public bool UseCompositor { get; set; } = true; /// /// Determines whether to use IME. diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index f71d514e38..73ef50052c 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -49,8 +49,8 @@ namespace Avalonia /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. /// public bool UseDeferredRendering { get; set; } = true; - - public bool UseCompositor { get; set; } + + public bool UseCompositor { get; set; } = true; /// /// Enables ANGLE for Windows. For every Windows version that is above Windows 7, the default is true otherwise it's false. From 3e5afd1fc3f6b89210bf78e25bc46739db63942e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 Jul 2022 20:19:34 +0100 Subject: [PATCH 158/171] appium v1 tests on mac --- azure-pipelines-integrationtests.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index c3f6292703..7954b970de 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -12,8 +12,23 @@ jobs: name: 'AvaloniaMacPool' steps: - - script: ./tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh - displayName: 'run integration tests' + - script: | + pkill node + appium & + pkill IntegrationTestApp + ./build.sh CompileNative + ./samples/IntegrationTestApp/bundle.sh + open -n ./samples/IntegrationTestApp/bin/Debug/net6.0/osx-arm64/publish/IntegrationTestApp.app + pkill IntegrationTestApp + + - task: DotNetCoreCLI@2 + inputs: + command: 'test' + projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj' + + - script: | + pkill IntegrationTestApp + pkill node - job: Windows From 121a6e31b129dfbd00bbec4bcca7ab5555622665 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 Jul 2022 20:29:06 +0100 Subject: [PATCH 159/171] print mac os display res. --- azure-pipelines-integrationtests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 7954b970de..10f098b607 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -12,6 +12,8 @@ jobs: name: 'AvaloniaMacPool' steps: + - script: system_profiler SPDisplaysDataType |grep Resolution + - script: | pkill node appium & From 0bf3a6d94487c0bf795bf5521c115d5b12a4ac8f Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 14 Jul 2022 23:57:27 -0400 Subject: [PATCH 160/171] Rename `ShowAccentColors` to `IsAccentColorsVisible` and disable drop shadow when false --- .../ControlCatalog/Pages/ColorPickerPage.xaml | 2 +- .../ColorPreviewer.Properties.cs | 14 +- .../ColorView/ColorView.Properties.cs | 32 ++--- .../Themes/Default/ColorPreviewer.xaml | 122 ++++++++++-------- .../Themes/Fluent/ColorPicker.xaml | 4 +- .../Themes/Fluent/ColorPreviewer.xaml | 122 ++++++++++-------- .../Themes/Fluent/ColorView.xaml | 2 +- 7 files changed, 167 insertions(+), 131 deletions(-) diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index 486a36956f..1590be25ba 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -47,7 +47,7 @@ ColorModel="Hsva" HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" /> - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty ShowAccentColorsProperty = + public static readonly StyledProperty IsAccentColorsVisibleProperty = AvaloniaProperty.Register( - nameof(ShowAccentColors), + nameof(IsAccentColorsVisible), true); /// @@ -38,13 +38,13 @@ namespace Avalonia.Controls.Primitives } /// - /// Gets or sets a value indicating whether accent colors are shown along + /// Gets or sets a value indicating whether accent colors are visible along /// with the preview color. /// - public bool ShowAccentColors + public bool IsAccentColorsVisible { - get => GetValue(ShowAccentColorsProperty); - set => SetValue(ShowAccentColorsProperty, value); + get => GetValue(IsAccentColorsVisibleProperty); + set => SetValue(IsAccentColorsVisibleProperty, value); } } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index d59c4d544a..b76059037b 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -52,6 +52,14 @@ namespace Avalonia.Controls defaultBindingMode: BindingMode.TwoWay, coerce: CoerceHsvColor); + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAccentColorsVisibleProperty = + AvaloniaProperty.Register( + nameof(IsAccentColorsVisible), + true); + /// /// Defines the property. /// @@ -220,14 +228,6 @@ namespace Avalonia.Controls nameof(SelectedIndex), (int)ColorViewTab.Spectrum); - /// - /// Defines the property. - /// - public static readonly StyledProperty ShowAccentColorsProperty = - AvaloniaProperty.Register( - nameof(ShowAccentColors), - true); - /// public Color Color { @@ -267,6 +267,13 @@ namespace Avalonia.Controls set => SetValue(HsvColorProperty, value); } + /// + public bool IsAccentColorsVisible + { + get => GetValue(IsAccentColorsVisibleProperty); + set => SetValue(IsAccentColorsVisibleProperty, value); + } + /// /// Gets or sets a value indicating whether the alpha component is enabled. /// When disabled (set to false) the alpha component will be fixed to maximum and @@ -325,7 +332,7 @@ namespace Avalonia.Controls /// /// /// Note that accent color visibility is controlled separately by - /// . + /// . /// public bool IsColorPreviewVisible { @@ -484,12 +491,5 @@ namespace Avalonia.Controls get => GetValue(SelectedIndexProperty); set => SetValue(SelectedIndexProperty, value); } - - /// - public bool ShowAccentColors - { - get => GetValue(ShowAccentColorsProperty); - set => SetValue(ShowAccentColorsProperty, value); - } } } diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml index 3a4efaa42a..c3bc7df4a4 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml @@ -14,69 +14,87 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + - - + BoxShadow="0 0 10 2 #BF000000" + CornerRadius="{TemplateBinding CornerRadius}" + Margin="10"> + + + + + - - + + - - + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index b34ac68be5..a4d5ff673f 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -53,6 +53,7 @@ ColorSpectrumComponents="{TemplateBinding ColorSpectrumComponents}" ColorSpectrumShape="{TemplateBinding ColorSpectrumShape}" HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" + IsAccentColorsVisible="{TemplateBinding IsAccentColorsVisible}" IsAlphaEnabled="{TemplateBinding IsAlphaEnabled}" IsAlphaVisible="{TemplateBinding IsAlphaVisible}" IsColorComponentsVisible="{TemplateBinding IsColorComponentsVisible}" @@ -73,8 +74,7 @@ PaletteColors="{TemplateBinding PaletteColors}" PaletteColumnCount="{TemplateBinding PaletteColumnCount}" Palette="{TemplateBinding Palette}" - SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" - ShowAccentColors="{TemplateBinding ShowAccentColors}" /> + SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" /> diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml index 77059f14fb..74f33d1258 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml @@ -14,69 +14,87 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + - - + BoxShadow="0 0 10 2 #BF000000" + CornerRadius="{TemplateBinding CornerRadius}" + Margin="10"> + + + + + - - + + - - + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index bcdb72d96c..e25e822f3f 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -638,8 +638,8 @@ From bb4287e22de38014cc6ba05e94c104188f68725e Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 15 Jul 2022 13:28:29 +0200 Subject: [PATCH 161/171] fix: RowDesiredWidth was missing the RowHeaderWidth --- .../Primitives/DataGridRowsPresenter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs index 5d82689eff..d906cd359c 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs @@ -97,7 +97,7 @@ namespace Avalonia.Controls.Primitives OwningGrid.OnFillerColumnWidthNeeded(finalSize.Width); - double rowDesiredWidth = OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth + OwningGrid.ColumnsInternal.FillerColumn.FillerWidth; + double rowDesiredWidth = OwningGrid.RowHeadersDesiredWidth + OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth + OwningGrid.ColumnsInternal.FillerColumn.FillerWidth; double topEdge = -OwningGrid.NegVerticalOffset; foreach (Control element in OwningGrid.DisplayData.GetScrollingElements()) { From c3bb062a3705341666649aa5a949ccdd4b543faa Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 15 Jul 2022 14:08:29 +0100 Subject: [PATCH 162/171] ensure bundle id is registered. --- .../Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh b/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh index b03a8a7a69..e60eb47a81 100755 --- a/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh +++ b/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh @@ -7,9 +7,12 @@ pkill node appium & pkill IntegrationTestApp ./build.sh CompileNative +rm -rf ./samples/IntegrationTestApp/bin/Debug/net6.0/osx-arm64/publish/IntegrationTestApp.app +open -b net.avaloniaui.avalonia.integrationtestapp ./samples/IntegrationTestApp/bundle.sh open -n ./samples/IntegrationTestApp/bin/Debug/net6.0/osx-arm64/publish/IntegrationTestApp.app pkill IntegrationTestApp +open -b net.avaloniaui.avalonia.integrationtestapp dotnet test tests/Avalonia.IntegrationTests.Appium/ -l "console;verbosity=detailed" pkill IntegrationTestApp pkill node From e8914c966d8f33be44df6007114569d4099072b4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 15 Jul 2022 14:17:55 +0100 Subject: [PATCH 163/171] reliably replace old bundle. --- azure-pipelines-integrationtests.yml | 2 ++ .../macos-clean-build-test.sh | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 10f098b607..0b79758c76 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -19,6 +19,8 @@ jobs: appium & pkill IntegrationTestApp ./build.sh CompileNative + rm -rf $(osascript -e "POSIX path of (path to application id \"net.avaloniaui.avalonia.integrationtestapp\")") + pkill IntegrationTestApp ./samples/IntegrationTestApp/bundle.sh open -n ./samples/IntegrationTestApp/bin/Debug/net6.0/osx-arm64/publish/IntegrationTestApp.app pkill IntegrationTestApp diff --git a/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh b/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh index e60eb47a81..14e765d16a 100755 --- a/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh +++ b/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh @@ -7,8 +7,8 @@ pkill node appium & pkill IntegrationTestApp ./build.sh CompileNative -rm -rf ./samples/IntegrationTestApp/bin/Debug/net6.0/osx-arm64/publish/IntegrationTestApp.app -open -b net.avaloniaui.avalonia.integrationtestapp +rm -rf $(osascript -e "POSIX path of (path to application id \"net.avaloniaui.avalonia.integrationtestapp\")") +pkill IntegrationTestApp ./samples/IntegrationTestApp/bundle.sh open -n ./samples/IntegrationTestApp/bin/Debug/net6.0/osx-arm64/publish/IntegrationTestApp.app pkill IntegrationTestApp From b9998dd4839b27516d2006c3633ec302c72495c5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 15 Jul 2022 15:56:59 +0200 Subject: [PATCH 164/171] Don't use TransformedBounds for automation. --- .../Automation/Peers/ControlAutomationPeer.cs | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index 28cb3e34b2..a93d3fa7dd 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -146,7 +146,7 @@ namespace Avalonia.Automation.Peers protected override string? GetAccessKeyCore() => AutomationProperties.GetAccessKey(Owner); protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Custom; protected override string? GetAutomationIdCore() => AutomationProperties.GetAutomationId(Owner) ?? Owner.Name; - protected override Rect GetBoundingRectangleCore() => GetBounds(Owner.TransformedBounds); + protected override Rect GetBoundingRectangleCore() => GetBounds(Owner); protected override string GetClassNameCore() => Owner.GetType().Name; protected override bool HasKeyboardFocusCore() => Owner.IsFocused; protected override bool IsContentElementCore() => AutomationProperties.GetAccessibilityView(Owner) >= AccessibilityView.Content; @@ -160,9 +160,19 @@ namespace Avalonia.Automation.Peers return AutomationProperties.GetControlTypeOverride(Owner) ?? GetAutomationControlTypeCore(); } - private static Rect GetBounds(TransformedBounds? bounds) + private static Rect GetBounds(Control control) { - return bounds?.Bounds.TransformToAABB(bounds!.Value.Transform) ?? default; + var root = control.GetVisualRoot(); + + if (root is null) + return default; + + var transform = control.TransformToVisual(root); + + if (!transform.HasValue) + return default; + + return new Rect(control.Bounds.Size).TransformToAABB(transform.Value); } private void Initialize() @@ -182,12 +192,14 @@ namespace Avalonia.Automation.Peers if (parent is Control c) (GetOrCreate(c) as ControlAutomationPeer)?.InvalidateChildren(); } - else if (e.Property == Visual.TransformedBoundsProperty) + else if (e.Property == Visual.BoundsProperty || + e.Property == Visual.RenderTransformProperty || + e.Property == Visual.RenderTransformOriginProperty) { RaisePropertyChangedEvent( AutomationElementIdentifiers.BoundingRectangleProperty, - GetBounds((TransformedBounds?)e.OldValue), - GetBounds((TransformedBounds?)e.NewValue)); + null, + GetBounds(Owner)); } else if (e.Property == Visual.VisualParentProperty) { From ac30a64d785b6c2c047c59faee33c3ad9555fa5a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 15 Jul 2022 15:24:15 +0100 Subject: [PATCH 165/171] dont use window size that could go off screen. --- tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 15ca78fdac..ecbdd5bade 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -137,7 +137,7 @@ namespace Avalonia.IntegrationTests.Appium { var mainWindow = _session.FindElementByAccessibilityId("MainWindow"); - using (OpenWindow(new PixelSize(1400, 100), ShowWindowMode.NonOwned, WindowStartupLocation.Manual)) + using (OpenWindow(new PixelSize(800, 100), ShowWindowMode.NonOwned, WindowStartupLocation.Manual)) { mainWindow.Click(); From c1b75357f1b24f578a33328be407ef19c3f90153 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 15 Jul 2022 16:56:15 +0200 Subject: [PATCH 166/171] Allow to pass custom menu interaction handler in MenuFlyoutPresenter --- src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs | 5 +++++ src/Avalonia.Controls/MenuBase.cs | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs b/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs index 6a7da87387..278e498a67 100644 --- a/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs +++ b/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs @@ -11,9 +11,14 @@ namespace Avalonia.Controls public MenuFlyoutPresenter() :base(new DefaultMenuInteractionHandler(true)) { + } + public MenuFlyoutPresenter(IMenuInteractionHandler menuInteractionHandler) + : base(menuInteractionHandler) + { } + public override void Close() { // DefaultMenuInteractionHandler calls this diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs index 122d45d033..54cbc46a36 100644 --- a/src/Avalonia.Controls/MenuBase.cs +++ b/src/Avalonia.Controls/MenuBase.cs @@ -40,7 +40,7 @@ namespace Avalonia.Controls /// /// Initializes a new instance of the class. /// - public MenuBase() + protected MenuBase() { InteractionHandler = new DefaultMenuInteractionHandler(false); } @@ -49,7 +49,7 @@ namespace Avalonia.Controls /// Initializes a new instance of the class. /// /// The menu interaction handler. - public MenuBase(IMenuInteractionHandler interactionHandler) + protected MenuBase(IMenuInteractionHandler interactionHandler) { InteractionHandler = interactionHandler ?? throw new ArgumentNullException(nameof(interactionHandler)); } From 0dca4bebc17e0cf640f066a4c1a4e6cc0f3b1dc1 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 16 Jul 2022 01:26:02 -0400 Subject: [PATCH 167/171] Disable additional aot flags --- samples/ControlCatalog.Android/ControlCatalog.Android.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index e52430f50b..54acdd9114 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -21,12 +21,12 @@ True - + True From c6e12a6e321d1e6a5dc6f46f31dab7cd45ed4a37 Mon Sep 17 00:00:00 2001 From: petris Date: Sat, 16 Jul 2022 20:23:49 +0200 Subject: [PATCH 168/171] Fix unaligned accesses --- .../Rendering/Composition/Transport/BatchStream.cs | 6 +++--- src/Avalonia.X11/X11Structs.cs | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs index 65237473fb..6db480a966 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs @@ -72,7 +72,7 @@ internal class BatchStreamWriter : IDisposable var size = Unsafe.SizeOf(); if (_currentDataSegment.Data == IntPtr.Zero || _currentDataSegment.ElementCount + size > _memoryPool.BufferSize) NextDataSegment(); - *(T*)((byte*)_currentDataSegment.Data + _currentDataSegment.ElementCount) = item; + Unsafe.WriteUnaligned((byte*)_currentDataSegment.Data + _currentDataSegment.ElementCount, item); _currentDataSegment.ElementCount += size; } @@ -123,7 +123,7 @@ internal class BatchStreamReader : IDisposable if (_memoryOffset + size > _currentDataSegment.ElementCount) throw new InvalidOperationException("Attempted to read more memory then left in the current segment"); - var rv = *(T*)((byte*)_currentDataSegment.Data + _memoryOffset); + var rv = Unsafe.ReadUnaligned((byte*)_currentDataSegment.Data + _memoryOffset); _memoryOffset += size; if (_memoryOffset == _currentDataSegment.ElementCount) { @@ -181,4 +181,4 @@ internal class BatchStreamReader : IDisposable while (_input.Objects.Count > 0) _objectPool.Return(_input.Objects.Dequeue().Data); } -} \ No newline at end of file +} diff --git a/src/Avalonia.X11/X11Structs.cs b/src/Avalonia.X11/X11Structs.cs index b1006b43ee..23abd31b2c 100644 --- a/src/Avalonia.X11/X11Structs.cs +++ b/src/Avalonia.X11/X11Structs.cs @@ -32,6 +32,7 @@ using System.Collections; using System.Drawing; using System.Diagnostics; using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // ReSharper disable FieldCanBeMadeReadOnly.Global // ReSharper disable IdentifierTypo @@ -545,7 +546,7 @@ namespace Avalonia.X11 { { if (data == null) throw new InvalidOperationException(); - return *(T*)data; + return Unsafe.ReadUnaligned(data); } } From 8fbfde96541fa85a6ee9cbd0a9ce1eb6945169b5 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 18 Jul 2022 07:55:57 +0200 Subject: [PATCH 169/171] `ComboBoxMinHeight` should be of Type `double`, not `Thickness` --- src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml b/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml index f0f3e5ea16..3b9c5038e6 100644 --- a/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml +++ b/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml @@ -15,7 +15,7 @@ 0,1,0,2 9,0,0,1 10,0,30,0 - 24 + 24 12,1,0,3 32 From fa17ee9bb991cc0b49326ea7b217daa12baefad2 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 18 Jul 2022 15:48:07 +0300 Subject: [PATCH 170/171] Introduced GtkInteropHelper.RunOnGlibThread, fixed demos --- .../NativeControls/Gtk/EmbedSample.Gtk.cs | 3 +-- .../NativeControls/Gtk/GtkHelper.cs | 9 ++----- src/Avalonia.X11/Interop/GtkInteropHelper.cs | 15 +++++++++++ src/Avalonia.X11/NativeDialogs/Gtk.cs | 27 ++++++++++++++++--- 4 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 src/Avalonia.X11/Interop/GtkInteropHelper.cs diff --git a/samples/ControlCatalog.NetCore/NativeControls/Gtk/EmbedSample.Gtk.cs b/samples/ControlCatalog.NetCore/NativeControls/Gtk/EmbedSample.Gtk.cs index 521d3674eb..81a5ba536f 100644 --- a/samples/ControlCatalog.NetCore/NativeControls/Gtk/EmbedSample.Gtk.cs +++ b/samples/ControlCatalog.NetCore/NativeControls/Gtk/EmbedSample.Gtk.cs @@ -22,8 +22,7 @@ public class EmbedSampleGtk : INativeDemoControl var control = createDefault(); var nodes = Path.GetFullPath(Path.Combine(typeof(EmbedSample).Assembly.GetModules()[0].FullyQualifiedName, - "..", - "nodes.mp4")); + "..", "NativeControls", "Gtk", "nodes.mp4")); _mplayer = Process.Start(new ProcessStartInfo("mplayer", $"-vo x11 -zoom -loop 0 -wid {control.Handle.ToInt64()} \"{nodes}\"") { diff --git a/samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs b/samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs index 456f77a44d..b1fef7c013 100644 --- a/samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs +++ b/samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs @@ -2,6 +2,7 @@ using System; using System.Threading.Tasks; using Avalonia.Controls.Platform; using Avalonia.Platform.Interop; +using Avalonia.X11.Interop; using Avalonia.X11.NativeDialogs; using static Avalonia.X11.NativeDialogs.Gtk; using static Avalonia.X11.NativeDialogs.Glib; @@ -10,8 +11,6 @@ namespace ControlCatalog.NetCore; internal class GtkHelper { - private static Task s_gtkTask; - class FileChooser : INativeControlHostDestroyableControlHandle { private readonly IntPtr _widget; @@ -38,11 +37,7 @@ internal class GtkHelper public static INativeControlHostDestroyableControlHandle CreateGtkFileChooser(IntPtr parentXid) { - if (s_gtkTask == null) - s_gtkTask = StartGtk(); - if (!s_gtkTask.Result) - return null; - return RunOnGlibThread(() => + return GtkInteropHelper.RunOnGlibThread(() => { using (var title = new Utf8Buffer("Embedded")) { diff --git a/src/Avalonia.X11/Interop/GtkInteropHelper.cs b/src/Avalonia.X11/Interop/GtkInteropHelper.cs new file mode 100644 index 0000000000..de0b755832 --- /dev/null +++ b/src/Avalonia.X11/Interop/GtkInteropHelper.cs @@ -0,0 +1,15 @@ +using System; +using System.ComponentModel; +using System.Threading.Tasks; + +namespace Avalonia.X11.Interop; + +public class GtkInteropHelper +{ + public static async Task RunOnGlibThread(Func cb) + { + if (!await NativeDialogs.Gtk.StartGtk().ConfigureAwait(false)) + throw new Win32Exception("Unable to initialize GTK"); + return await NativeDialogs.Glib.RunOnGlibThread(cb).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Avalonia.X11/NativeDialogs/Gtk.cs b/src/Avalonia.X11/NativeDialogs/Gtk.cs index 8e2d920bc7..c9e482db86 100644 --- a/src/Avalonia.X11/NativeDialogs/Gtk.cs +++ b/src/Avalonia.X11/NativeDialogs/Gtk.cs @@ -3,6 +3,8 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Avalonia.Platform.Interop; +using JetBrains.Annotations; + // ReSharper disable IdentifierTypo namespace Avalonia.X11.NativeDialogs { @@ -256,10 +258,19 @@ namespace Avalonia.X11.NativeDialogs public static IntPtr GetForeignWindow(IntPtr xid) => gdk_x11_window_foreign_new_for_display(s_display, xid); + static object s_startGtkLock = new(); + static Task s_startGtkTask; + public static Task StartGtk() { - var tcs = new TaskCompletionSource(); - new Thread(() => + return StartGtkCore(); + lock (s_startGtkLock) + return s_startGtkTask ??= StartGtkCore(); + } + + private static void GtkThread(TaskCompletionSource tcs) + { + try { try { @@ -293,7 +304,17 @@ namespace Avalonia.X11.NativeDialogs tcs.SetResult(true); while (true) gtk_main_iteration(); - }) {Name = "GTK3THREAD", IsBackground = true}.Start(); + } + catch + { + tcs.SetResult(false); + } + } + + private static Task StartGtkCore() + { + var tcs = new TaskCompletionSource(); + new Thread(() => GtkThread(tcs)) {Name = "GTK3THREAD", IsBackground = true}.Start(); return tcs.Task; } } From 78a49b3fb327e7e374edd6b13a7e7bb09b1e4160 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Mon, 18 Jul 2022 17:44:39 +0300 Subject: [PATCH 171/171] Fix typo in DataGrid. DataGridComparerSortDesctiption -> DataGridComparerSortDescription --- .../Collections/DataGridSortDescription.cs | 8 ++++---- src/Avalonia.Controls.DataGrid/DataGridColumn.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs index ca6020128c..ff222658db 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs @@ -268,11 +268,11 @@ namespace Avalonia.Collections public static DataGridSortDescription FromComparer(IComparer comparer, ListSortDirection direction = ListSortDirection.Ascending) { - return new DataGridComparerSortDesctiption(comparer, direction); + return new DataGridComparerSortDescription(comparer, direction); } } - public class DataGridComparerSortDesctiption : DataGridSortDescription + public class DataGridComparerSortDescription : DataGridSortDescription { private readonly IComparer _innerComparer; private readonly ListSortDirection _direction; @@ -281,7 +281,7 @@ namespace Avalonia.Collections public IComparer SourceComparer => _innerComparer; public override IComparer Comparer => _comparer; public override ListSortDirection Direction => _direction; - public DataGridComparerSortDesctiption(IComparer comparer, ListSortDirection direction) + public DataGridComparerSortDescription(IComparer comparer, ListSortDirection direction) { _innerComparer = comparer; _direction = direction; @@ -300,7 +300,7 @@ namespace Avalonia.Collections public override DataGridSortDescription SwitchSortDirection() { var newDirection = _direction == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending; - return new DataGridComparerSortDesctiption(_innerComparer, newDirection); + return new DataGridComparerSortDescription(_innerComparer, newDirection); } } diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index c415f477d4..e57d6bbde2 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -1091,7 +1091,7 @@ namespace Avalonia.Controls { return OwningGrid.DataConnection.SortDescriptions - .OfType() + .OfType() .FirstOrDefault(s => s.SourceComparer == CustomSortComparer); }