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