diff --git a/.ncrunch/ControlCatalog.v3.ncrunchproject b/.ncrunch/ControlCatalog.net6.0.v3.ncrunchproject
similarity index 100%
rename from .ncrunch/ControlCatalog.v3.ncrunchproject
rename to .ncrunch/ControlCatalog.net6.0.v3.ncrunchproject
diff --git a/.ncrunch/ControlCatalog.netstandard2.0.v3.ncrunchproject b/.ncrunch/ControlCatalog.netstandard2.0.v3.ncrunchproject
new file mode 100644
index 0000000000..319cd523ce
--- /dev/null
+++ b/.ncrunch/ControlCatalog.netstandard2.0.v3.ncrunchproject
@@ -0,0 +1,5 @@
+
+
+ True
+
+
\ No newline at end of file
diff --git a/Avalonia.sln b/Avalonia.sln
index 25c7daf080..071d0457b8 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
@@ -205,14 +206,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\S
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.SourceGenerator", "src\Avalonia.SourceGenerator\Avalonia.SourceGenerator.csproj", "{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevAnalyzers", "src\tools\DevAnalyzers\DevAnalyzers.csproj", "{2B390431-288C-435C-BB6B-A374033BD8D1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ColorPicker", "src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj", "{7BF6C69D-FC14-43EB-9ED0-782C16F3D5D9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Tests", "tests\Avalonia.DesignerSupport.Tests\Avalonia.DesignerSupport.Tests.csproj", "{EABE2161-989B-42BF-BD8D-1E34B20C21F1}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevGenerators", "src\tools\DevGenerators\DevGenerators.csproj", "{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -485,10 +486,6 @@ Global
{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|Any CPU.Build.0 = Release|Any CPU
- {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.Build.0 = Release|Any CPU
{2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2B390431-288C-435C-BB6B-A374033BD8D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -501,6 +498,10 @@ Global
{EABE2161-989B-42BF-BD8D-1E34B20C21F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EABE2161-989B-42BF-BD8D-1E34B20C21F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EABE2161-989B-42BF-BD8D-1E34B20C21F1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -557,6 +558,7 @@ Global
{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{EABE2161-989B-42BF-BD8D-1E34B20C21F1} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
+ {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}
diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml
index c3f6292703..0b79758c76 100644
--- a/azure-pipelines-integrationtests.yml
+++ b/azure-pipelines-integrationtests.yml
@@ -12,8 +12,27 @@ jobs:
name: 'AvaloniaMacPool'
steps:
- - script: ./tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh
- displayName: 'run integration tests'
+ - script: system_profiler SPDisplaysDataType |grep Resolution
+
+ - script: |
+ pkill node
+ 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
+
+ - task: DotNetCoreCLI@2
+ inputs:
+ command: 'test'
+ projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj'
+
+ - script: |
+ pkill IntegrationTestApp
+ pkill node
- job: Windows
diff --git a/build/SourceGenerators.props b/build/SourceGenerators.props
index d000af1bf6..4929578b60 100644
--- a/build/SourceGenerators.props
+++ b/build/SourceGenerators.props
@@ -1,7 +1,7 @@
diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm
index 01725ace03..4ae6ad5a00 100644
--- a/native/Avalonia.Native/src/OSX/AvnView.mm
+++ b/native/Avalonia.Native/src/OSX/AvnView.mm
@@ -127,7 +127,11 @@
[self updateRenderTarget];
auto reason = [self inLiveResize] ? ResizeUser : _resizeReason;
- _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason);
+
+ if(_parent->IsShown())
+ {
+ _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason);
+ }
}
}
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;
}
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
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/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs
index 13751b56b5..d98a068d84 100644
--- a/samples/ControlCatalog.NetCore/Program.cs
+++ b/samples/ControlCatalog.NetCore/Program.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
@@ -53,7 +53,11 @@ namespace ControlCatalog.NetCore
else if (args.Contains("--full-headless"))
{
return builder
- .UseHeadless(true)
+ .UseHeadless(new AvaloniaHeadlessPlatformOptions
+ {
+ UseHeadlessDrawing = true,
+ UseCompositor = true
+ })
.AfterSetup(_ =>
{
DispatcherTimer.RunOnce(async () =>
@@ -63,12 +67,11 @@ namespace ControlCatalog.NetCore
var tc = window.GetLogicalDescendants().OfType().First();
foreach (var page in tc.Items.Cast().ToList())
{
- // Skip DatePicker because of some layout bug in grid
- if (page.Header.ToString() == "DatePicker")
+ if (page.Header.ToString() == "DatePicker" || page.Header.ToString() == "TreeView")
continue;
Console.WriteLine("Selecting " + page.Header);
tc.SelectedItem = page;
- await Task.Delay(500);
+ await Task.Delay(50);
}
Console.WriteLine("Selecting the first page");
tc.SelectedItem = tc.Items.OfType
+
@@ -32,6 +35,11 @@
-
+
+
+
+
+
+
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 803b8d60dc..267c403ab7 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/Controls/Classes.cs b/src/Avalonia.Base/Controls/Classes.cs
index 50605661fa..c3d3fbca46 100644
--- a/src/Avalonia.Base/Controls/Classes.cs
+++ b/src/Avalonia.Base/Controls/Classes.cs
@@ -1,8 +1,7 @@
using System;
using System.Collections.Generic;
using Avalonia.Collections;
-
-#nullable enable
+using Avalonia.Utilities;
namespace Avalonia.Controls
{
@@ -14,6 +13,8 @@ namespace Avalonia.Controls
///
public class Classes : AvaloniaList, IPseudoClasses
{
+ private SafeEnumerableList? _listeners;
+
///
/// Initializes a new instance of the class.
///
@@ -39,6 +40,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 +68,7 @@ namespace Avalonia.Controls
if (!Contains(name))
{
base.Add(name);
+ NotifyChanged();
}
}
@@ -89,6 +96,7 @@ namespace Avalonia.Controls
}
base.AddRange(c);
+ NotifyChanged();
}
///
@@ -103,6 +111,8 @@ namespace Avalonia.Controls
RemoveAt(i);
}
}
+
+ NotifyChanged();
}
///
@@ -122,6 +132,7 @@ namespace Avalonia.Controls
if (!Contains(name))
{
base.Insert(index, name);
+ NotifyChanged();
}
}
@@ -154,6 +165,7 @@ namespace Avalonia.Controls
if (toInsert != null)
{
base.InsertRange(index, toInsert);
+ NotifyChanged();
}
}
@@ -169,7 +181,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 +216,7 @@ namespace Avalonia.Controls
if (toRemove != null)
{
base.RemoveAll(toRemove);
+ NotifyChanged();
}
}
@@ -214,6 +234,7 @@ namespace Avalonia.Controls
var name = this[index];
ThrowIfPseudoclass(name, "removed");
base.RemoveAt(index);
+ NotifyChanged();
}
///
@@ -224,6 +245,7 @@ namespace Avalonia.Controls
public override void RemoveRange(int index, int count)
{
base.RemoveRange(index, count);
+ NotifyChanged();
}
///
@@ -255,6 +277,7 @@ namespace Avalonia.Controls
}
base.AddRange(source);
+ NotifyChanged();
}
///
@@ -263,13 +286,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/Controls/IPseudoClasses.cs b/src/Avalonia.Base/Controls/IPseudoClasses.cs
index eda521727f..438b05a8cf 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 Contains(string name);
}
}
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/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs
index 446f135c83..b9ca6bfbd7 100644
--- a/src/Avalonia.Base/Layout/LayoutManager.cs
+++ b/src/Avalonia.Base/Layout/LayoutManager.cs
@@ -350,7 +350,7 @@ namespace Avalonia.Layout
{
for (var i = 0; i < count; ++i)
{
- var l = _effectiveViewportChangedListeners[i];
+ var l = listeners[i];
if (!l.Listener.IsAttachedToVisualTree)
{
@@ -362,7 +362,7 @@ namespace Avalonia.Layout
if (viewport != l.Viewport)
{
l.Listener.EffectiveViewportChanged(new EffectiveViewportChangedEventArgs(viewport));
- _effectiveViewportChangedListeners[i] = new EffectiveViewportChangedListener(l.Listener, viewport);
+ l.Viewport = viewport;
}
}
}
@@ -414,7 +414,7 @@ namespace Avalonia.Layout
}
}
- private readonly struct EffectiveViewportChangedListener
+ private class EffectiveViewportChangedListener
{
public EffectiveViewportChangedListener(ILayoutable listener, Rect viewport)
{
@@ -423,7 +423,7 @@ namespace Avalonia.Layout
}
public ILayoutable Listener { get; }
- public Rect Viewport { get; }
+ public Rect Viewport { get; set; }
}
}
}
diff --git a/src/Avalonia.Base/Platform/IPlatformGpu.cs b/src/Avalonia.Base/Platform/IPlatformGpu.cs
new file mode 100644
index 0000000000..0507dea1d7
--- /dev/null
+++ b/src/Avalonia.Base/Platform/IPlatformGpu.cs
@@ -0,0 +1,16 @@
+using System;
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform;
+
+[Unstable]
+public interface IPlatformGpu
+{
+ IPlatformGpuContext PrimaryContext { get; }
+}
+
+[Unstable]
+public interface IPlatformGpuContext : IDisposable
+{
+ IDisposable EnsureCurrent();
+}
\ 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..80e64118ee
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Rendering.Composition.Expressions;
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition.Animations;
+
+
+///
+/// The base class for both key-frame and expression animation instances
+/// Is responsible for activation tracking and for subscribing to properties used in dependencies
+///
+internal abstract class AnimationInstanceBase : IAnimationInstance
+{
+ private List<(ServerObject obj, CompositionProperty member)>? _trackedObjects;
+ protected PropertySetSnapshot Parameters { get; }
+ public ServerObject TargetObject { get; }
+ protected CompositionProperty Property { get; private set; } = null!;
+ private bool _invalidated;
+
+ public AnimationInstanceBase(ServerObject target, PropertySetSnapshot parameters)
+ {
+ Parameters = parameters;
+ TargetObject = target;
+ }
+
+ protected void Initialize(CompositionProperty property, 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.GetCompositionProperty(t.member);
+ if (off == null)
+#if DEBUG
+ throw new InvalidCastException("Attempting to subscribe to unknown field");
+#else
+ continue;
+#endif
+ _trackedObjects.Add((tracked, off));
+ }
+ }
+ }
+
+ Property = property;
+ }
+
+ public abstract void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property);
+ 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(Property);
+ }
+}
\ 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..c5102a2d7d
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs
@@ -0,0 +1,75 @@
+// 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
+{
+ ///
+ /// This is the base class for ExpressionAnimation and KeyFrameAnimation.
+ ///
+ ///
+ /// Use the method to start the animation.
+ /// Value parameters (as opposed to reference parameters which are set using )
+ /// are copied and "embedded" into an expression at the time CompositionObject.StartAnimation is called.
+ /// Changing the value of the variable after is called will not affect
+ /// the value of the ExpressionAnimation.
+ /// See the remarks section of ExpressionAnimation for additional information.
+ ///
+ public abstract class CompositionAnimation : CompositionObject, ICompositionAnimationBase
+ {
+ private readonly CompositionPropertySet _propertySet;
+ internal CompositionAnimation(Compositor compositor) : base(compositor, null!)
+ {
+ _propertySet = new CompositionPropertySet(compositor);
+ }
+
+ ///
+ /// Clears all of the parameters of the animation.
+ ///
+ public void ClearAllParameters() => _propertySet.ClearAll();
+
+ ///
+ /// Clears a parameter from the animation.
+ ///
+ public void ClearParameter(string key) => _propertySet.Clear(key);
+
+ void SetVariant(string key, ExpressionVariant value) => _propertySet.Set(key, value);
+
+ public void SetColorParameter(string key, 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);
+
+ public string? Target { get; set; }
+
+ internal abstract IAnimationInstance CreateInstance(ServerObject targetObject,
+ ExpressionVariant? finalValue);
+
+ internal PropertySetSnapshot CreateSnapshot() => _propertySet.Snapshot();
+
+ 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..89f8ba411d
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs
@@ -0,0 +1,24 @@
+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!)
+ {
+ }
+ }
+}
\ 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..163f4e99ba
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs
@@ -0,0 +1,53 @@
+// ReSharper disable CheckNamespace
+using System;
+using Avalonia.Rendering.Composition.Expressions;
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ ///
+ /// A Composition Animation that uses a mathematical equation to calculate the value for an animating property every frame.
+ ///
+ ///
+ /// The core of ExpressionAnimations allows a developer to define a mathematical equation that can be used to calculate the value
+ /// of a targeted animating property each frame.
+ /// This contrasts s, which use an interpolator to define how the animating
+ /// property changes over time. The mathematical equation can be defined using references to properties
+ /// of Composition objects, mathematical functions and operators and Input.
+ /// Use the method to start the animation.
+ ///
+ public class ExpressionAnimation : CompositionAnimation
+ {
+ private string? _expression;
+ private Expression? _parsedExpression;
+
+ internal ExpressionAnimation(Compositor compositor) : base(compositor)
+ {
+ }
+
+ ///
+ /// The mathematical equation specifying how the animated value is calculated each frame.
+ /// The Expression is the core of an and represents the equation
+ /// the system will use to calculate the value of the animation property each frame.
+ /// The equation is set on this property in the form of a string.
+ /// Although expressions can be defined by simple mathematical equations such as "2+2",
+ /// the real power lies in creating mathematical relationships where the input values can change frame over frame.
+ ///
+ 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());
+ }
+}
\ 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..764bac9931
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Rendering.Composition.Expressions;
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+
+ ///
+ /// Server-side counterpart of with values baked-in.
+ ///
+ internal class ExpressionAnimationInstance : AnimationInstanceBase, IAnimationInstance
+ {
+ private readonly Expression _expression;
+ private ExpressionVariant _startingValue;
+ private readonly ExpressionVariant? _finalValue;
+
+ protected override ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue)
+ {
+ var ctx = new ExpressionEvaluationContext
+ {
+ Parameters = Parameters,
+ Target = TargetObject,
+ ForeignFunctionInterface = BuiltInExpressionFfi.Instance,
+ StartingValue = _startingValue,
+ FinalValue = _finalValue ?? _startingValue,
+ CurrentValue = currentValue
+ };
+ return _expression.Evaluate(ref ctx);
+ }
+
+ public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property)
+ {
+ _startingValue = startingValue;
+ var hs = new HashSet<(string, string)>();
+ _expression.CollectReferences(hs);
+ base.Initialize(property, hs);
+ }
+
+ public ExpressionAnimationInstance(Expression expression,
+ ServerObject target,
+ ExpressionVariant? finalValue,
+ PropertySetSnapshot parameters) : base(target, parameters)
+ {
+ _expression = expression;
+ _finalValue = finalValue;
+ }
+ }
+}
\ 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..4e1972f2c6
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs
@@ -0,0 +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 Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property);
+ void Activate();
+ void Deactivate();
+ void Invalidate();
+ }
+}
\ 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..87e5ad757a
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs
@@ -0,0 +1,15 @@
+// ReSharper disable CheckNamespace
+
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ ///
+ /// Base class for 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..f4bcc6ff38
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Avalonia.Rendering.Composition.Transport;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ ///
+ /// A collection of animations triggered when a condition is met.
+ ///
+ ///
+ /// Implicit animations let you drive animations by specifying trigger conditions rather than requiring the manual definition of animation behavior.
+ /// They help decouple animation start logic from core app logic. You define animations and the events that should trigger these animations.
+ /// Currently the only available trigger is animated property change.
+ ///
+ /// When expression is used in ImplicitAnimationCollection a special keyword `this.FinalValue` will represent
+ /// the final value of the animated property that was changed
+ ///
+ public class ImplicitAnimationCollection : CompositionObject, IDictionary
+ {
+ private Dictionary _inner = new Dictionary();
+ private IDictionary _innerface;
+ internal ImplicitAnimationCollection(Compositor compositor) : base(compositor, null!)
+ {
+ _innerface = _inner;
+ }
+
+ 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..a4eeacef32
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Numerics;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ ///
+ /// An interface to define interpolation logic for a particular type
+ ///
+ 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..49b3ab753a
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs
@@ -0,0 +1,134 @@
+using System;
+using Avalonia.Animation;
+using Avalonia.Animation.Easings;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+
+ ///
+ /// A time-based animation with one or more key frames.
+ /// These frames are markers, allowing developers to specify values at specific times for the animating property.
+ /// KeyFrame animations can be further customized by specifying how the animation interpolates between keyframes.
+ ///
+ public abstract class KeyFrameAnimation : CompositionAnimation
+ {
+ private TimeSpan _duration = TimeSpan.FromMilliseconds(1);
+
+ internal KeyFrameAnimation(Compositor compositor) : base(compositor)
+ {
+ }
+
+ ///
+ /// The delay behavior of the key frame animation.
+ ///
+ public AnimationDelayBehavior DelayBehavior { get; set; }
+
+ ///
+ /// Delay before the animation starts after is called.
+ ///
+ public System.TimeSpan DelayTime { get; set; }
+
+ ///
+ /// The direction the animation is playing.
+ /// The Direction property allows you to drive your animation from start to end or end to start or alternate
+ /// between start and end or end to start if animation has an greater than one.
+ /// This gives an easy way for customizing animation definitions.
+ ///
+ public PlaybackDirection Direction { get; set; }
+
+ ///
+ /// The duration of the animation.
+ /// Minimum allowed value is 1ms and maximum allowed value is 24 days.
+ ///
+ public TimeSpan Duration
+ {
+ get => _duration;
+ set
+ {
+ if (_duration < TimeSpan.FromMilliseconds(1) || _duration > TimeSpan.FromDays(1))
+ throw new ArgumentException("Minimum allowed value is 1ms and maximum allowed value is 24 days.");
+ _duration = value;
+ }
+ }
+
+ ///
+ /// The iteration behavior for the key frame animation.
+ ///
+ public AnimationIterationBehavior IterationBehavior { get; set; }
+
+ ///
+ /// The number of times to repeat the key frame animation.
+ ///
+ public int IterationCount { get; set; } = 1;
+
+ ///
+ /// Specifies how to set the property value when animation is stopped
+ ///
+ public AnimationStopBehavior StopBehavior { get; set; }
+
+ private protected abstract IKeyFrames KeyFrames { get; }
+
+ ///
+ /// Inserts an expression keyframe.
+ ///
+ ///
+ /// The time the key frame should occur at, expressed as a percentage of the animation Duration. Allowed value is from 0.0 to 1.0.
+ ///
+ /// The expression used to calculate the value of the key frame.
+ /// The easing function to use when interpolating between frames.
+ public void InsertExpressionKeyFrame(float normalizedProgressKey, string value,
+ Easing? easingFunction = null) =>
+ KeyFrames.InsertExpressionKeyFrame(normalizedProgressKey, value, easingFunction ?? Compositor.DefaultEasing);
+ }
+
+ ///
+ /// Specifies the animation delay behavior.
+ ///
+ public enum AnimationDelayBehavior
+ {
+ ///
+ /// If a DelayTime is specified, it delays starting the animation according to delay time and after delay
+ /// has expired it applies animation to the object property.
+ ///
+ SetInitialValueAfterDelay,
+ ///
+ /// Applies the initial value of the animation (i.e. the value at Keyframe 0) to the object before the delay time
+ /// is elapsed (when there is a DelayTime specified), it then delays starting the animation according to the DelayTime.
+ ///
+ SetInitialValueBeforeDelay
+ }
+
+ ///
+ /// Specifies if the animation should loop.
+ ///
+ public enum AnimationIterationBehavior
+ {
+ ///
+ /// The animation should loop the specified number of times.
+ ///
+ Count,
+ ///
+ /// The animation should loop forever.
+ ///
+ Forever
+ }
+
+ ///
+ /// Specifies the behavior of an animation when it stops.
+ ///
+ public enum AnimationStopBehavior
+ {
+ ///
+ /// Leave the animation at its current value.
+ ///
+ LeaveCurrentValue,
+ ///
+ /// Reset the animation to its initial value.
+ ///
+ SetToInitialValue,
+ ///
+ /// Set the animation to its final value.
+ ///
+ 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..0c0fcfaf2b
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs
@@ -0,0 +1,178 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Animation;
+using Avalonia.Rendering.Composition.Expressions;
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ ///
+ /// Server-side counterpart of KeyFrameAnimation with values baked-in
+ ///
+ class KeyFrameAnimationInstance : AnimationInstanceBase, IAnimationInstance where T : struct
+ {
+ private readonly IInterpolator _interpolator;
+ private readonly ServerKeyFrame[] _keyFrames;
+ private readonly ExpressionVariant? _finalValue;
+ private readonly AnimationDelayBehavior _delayBehavior;
+ private readonly TimeSpan _delayTime;
+ private readonly PlaybackDirection _direction;
+ private readonly TimeSpan _duration;
+ private readonly AnimationIterationBehavior _iterationBehavior;
+ private readonly int _iterationCount;
+ 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,
+ ServerObject target,
+ AnimationDelayBehavior delayBehavior, TimeSpan delayTime,
+ PlaybackDirection direction, TimeSpan duration,
+ AnimationIterationBehavior iterationBehavior,
+ int iterationCount, AnimationStopBehavior stopBehavior) : base(target, snapshot)
+ {
+ _interpolator = interpolator;
+ _keyFrames = keyFrames;
+ _finalValue = finalValue;
+ _delayBehavior = delayBehavior;
+ _delayTime = delayTime;
+ _direction = direction;
+ _duration = duration;
+ _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");
+ }
+
+
+ protected override ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue)
+ {
+ var starting = ExpressionVariant.Create(_startingValue);
+ var ctx = new ExpressionEvaluationContext
+ {
+ 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)
+ 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 == PlaybackDirection.Alternate
+ ? !evenIterationNumber
+ : _direction == PlaybackDirection.AlternateReverse
+ ? evenIterationNumber
+ : _direction == PlaybackDirection.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 = (float)right.EasingFunction.Ease(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 override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property)
+ {
+ _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(property, 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
new file mode 100644
index 0000000000..369cc80b95
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Animation.Easings;
+using Avalonia.Rendering.Composition.Expressions;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+
+ ///
+ /// Collection of composition animation key frames
+ ///
+ ///
+ 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, IEasing easingFunction)
+ {
+ Validate(normalizedProgressKey);
+ Add(new KeyFrame
+ {
+ NormalizedProgressKey = normalizedProgressKey,
+ Expression = Expression.Parse(value),
+ EasingFunction = easingFunction
+ });
+ }
+
+ public void Insert(float normalizedProgressKey, T value, IEasing 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,
+ Key = f.NormalizedProgressKey
+ };
+ }
+ return frames;
+ }
+ }
+
+ ///
+ /// Composition animation key frame
+ ///
+ struct KeyFrame
+ {
+ public float NormalizedProgressKey;
+ public T Value;
+ public Expression Expression;
+ public IEasing EasingFunction;
+ }
+
+ ///
+ /// Server-side composition animation key frame
+ ///
+ struct ServerKeyFrame
+ {
+ public T Value;
+ public Expression? Expression;
+ public IEasing EasingFunction;
+ public float Key;
+ }
+
+ interface IKeyFrames
+ {
+ public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, IEasing 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..fc6cfc9f3d
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using Avalonia.Rendering.Composition.Expressions;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ ///
+ /// A snapshot of properties used by an animation
+ ///
+ 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..282973c26a
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
@@ -0,0 +1,278 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Runtime.InteropServices;
+using Avalonia.Collections;
+using Avalonia.Collections.Pooled;
+using Avalonia.Media;
+using Avalonia.Rendering.Composition.Drawing;
+using Avalonia.Rendering.Composition.Server;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.Composition;
+
+///
+/// A renderer that utilizes to render the visual tree
+///
+public class CompositingRenderer : IRendererWithCompositor
+{
+ private readonly IRenderRoot _root;
+ private readonly Compositor _compositor;
+ CompositionDrawingContext _recorder = new();
+ DrawingContext _recordingContext;
+ private HashSet _dirty = new();
+ private HashSet _recalculateChildren = new();
+ private readonly CompositionTarget _target;
+ private bool _queuedUpdate;
+ private Action _update;
+ private Action _invalidateScene;
+
+ ///
+ /// Asks the renderer to only draw frames on the render thread. Makes Paint to wait until frame is rendered.
+ ///
+ public bool RenderOnlyOnRenderThread { get; set; } = true;
+
+ public CompositingRenderer(IRenderRoot root,
+ Compositor compositor)
+ {
+ _root = root;
+ _compositor = compositor;
+ _recordingContext = new DrawingContext(_recorder);
+ _target = compositor.CreateCompositionTarget(root.CreateRenderTarget);
+ _target.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor);
+ _update = Update;
+ _invalidateScene = InvalidateScene;
+ }
+
+ ///
+ 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()
+ {
+ 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(p, filter);
+ 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();
+
+ PooledList<(IVisual visual, int index)>? sortedChildren = null;
+ if (v.HasNonUniformZIndexChildren && visualChildren.Count > 1)
+ {
+ sortedChildren = new (visualChildren.Count);
+ for (var c = 0; c < visualChildren.Count; c++)
+ sortedChildren.Add((visualChildren[c], c));
+
+ // Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements.
+ sortedChildren.Sort(static (lhs, rhs) =>
+ {
+ var result = lhs.visual.ZIndex.CompareTo(rhs.visual.ZIndex);
+ return result == 0 ? lhs.index.CompareTo(rhs.index) : result;
+ });
+ }
+
+ if (compositionChildren.Count == visualChildren.Count)
+ {
+ bool mismatch = false;
+ if (v.HasNonUniformZIndexChildren)
+ {
+
+
+ }
+
+ if (sortedChildren != null)
+ for (var c = 0; c < visualChildren.Count; c++)
+ {
+ if (!ReferenceEquals(compositionChildren[c], ((Visual)sortedChildren[c].visual).CompositionVisual))
+ {
+ mismatch = true;
+ break;
+ }
+ }
+ else
+ for (var c = 0; c < visualChildren.Count; c++)
+ if (!ReferenceEquals(compositionChildren[c], ((Visual)visualChildren[c]).CompositionVisual))
+ {
+ mismatch = true;
+ break;
+ }
+
+
+ if (!mismatch)
+ {
+ sortedChildren?.Dispose();
+ return;
+ }
+ }
+
+ compositionChildren.Clear();
+ if (sortedChildren != null)
+ {
+ foreach (var ch in sortedChildren)
+ {
+ var compositionChild = ((Visual)ch.visual).CompositionVisual;
+ if (compositionChild != null)
+ compositionChildren.Add(compositionChild);
+ }
+ sortedChildren.Dispose();
+ }
+ else
+ foreach (var ch in v.GetVisualChildren())
+ {
+ var compositionChild = ((Visual)ch).CompositionVisual;
+ if (compositionChild != null)
+ compositionChildren.Add(compositionChild);
+ }
+ }
+
+ private void InvalidateScene() =>
+ SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize)));
+
+ private void Update()
+ {
+ _queuedUpdate = false;
+ foreach (var visual in _dirty)
+ {
+ var comp = visual.CompositionVisual;
+ if(comp == null)
+ continue;
+
+ // TODO: Optimize all of that by moving to the Visual itself, so we won't have to recalculate every time
+ comp.Offset = new Vector3((float)visual.Bounds.Left, (float)visual.Bounds.Top, 0);
+ comp.Size = new Vector2((float)visual.Bounds.Width, (float)visual.Bounds.Height);
+ comp.Visible = visual.IsVisible;
+ comp.Opacity = (float)visual.Opacity;
+ comp.ClipToBounds = visual.ClipToBounds;
+ comp.Clip = visual.Clip?.PlatformImpl;
+ comp.OpacityMask = visual.OpacityMask;
+
+ var renderTransform = Matrix.Identity;
+
+ if (visual.HasMirrorTransform)
+ renderTransform = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0);
+
+ if (visual.RenderTransform != null)
+ {
+ var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
+ var offset = Matrix.CreateTranslation(origin);
+ renderTransform *= (-offset) * visual.RenderTransform.Value * (offset);
+ }
+
+
+
+ comp.TransformMatrix = MatrixUtils.ToMatrix4x4(renderTransform);
+
+ _recorder.BeginUpdate(comp.DrawList);
+ visual.Render(_recordingContext);
+ comp.DrawList = _recorder.EndUpdate();
+
+ SyncChildren(visual);
+ }
+ foreach(var v in _recalculateChildren)
+ if (!_dirty.Contains(v))
+ SyncChildren(v);
+ _dirty.Clear();
+ _recalculateChildren.Clear();
+ _target.Size = _root.ClientSize;
+ _target.Scaling = _root.RenderScaling;
+ Compositor.InvokeOnNextCommit(_invalidateScene);
+ }
+
+ public void Resized(Size size)
+ {
+ }
+
+ public void Paint(Rect rect)
+ {
+ Update();
+ _target.RequestRedraw();
+ if(RenderOnlyOnRenderThread && Compositor.Loop.RunsInBackground)
+ Compositor.RequestCommitAsync().Wait();
+ else
+ _target.ImmediateUIThreadRender();
+ }
+
+ public void Start() => _target.IsEnabled = true;
+
+ public void Stop()
+ {
+ _target.IsEnabled = false;
+ }
+
+ 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
+ if (Compositor.Loop.RunsInBackground)
+ _compositor.RequestCommitAsync().Wait();
+ }
+
+ ///
+ /// The associated object
+ ///
+ public Compositor Compositor => _compositor;
+}
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
new file mode 100644
index 0000000000..47cfcd325b
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
@@ -0,0 +1,68 @@
+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;
+
+
+///
+/// A composition visual that holds a list of drawing commands issued by
+///
+internal class CompositionDrawListVisual : CompositionContainerVisual
+{
+ ///
+ /// The associated
+ ///
+ public Visual Visual { get; }
+
+ private bool _drawListChanged;
+ private CompositionDrawList? _drawList;
+
+ ///
+ /// The list of drawing commands
+ ///
+ public CompositionDrawList? DrawList
+ {
+ get => _drawList;
+ set
+ {
+ _drawList?.Dispose();
+ _drawList = value;
+ _drawListChanged = true;
+ RegisterForSerialization();
+ }
+ }
+
+ 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)
+ {
+ Visual = visual;
+ }
+
+ 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)
+ if (op.Item.HitTest(pt))
+ return true;
+ return false;
+ }
+}
\ 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..f529ee9cff
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs
@@ -0,0 +1,141 @@
+using System;
+using Avalonia.Rendering.Composition.Animations;
+using Avalonia.Rendering.Composition.Expressions;
+using Avalonia.Rendering.Composition.Server;
+using Avalonia.Rendering.Composition.Transport;
+using Avalonia.Utilities;
+
+namespace Avalonia.Rendering.Composition
+{
+ ///
+ /// Base class of the composition API representing a node in the visual tree structure.
+ /// Composition objects are the visual tree structure on which all other features of the composition API use and build on.
+ /// The API allows developers to define and create one or many objects each representing a single node in a Visual tree.
+ ///
+ public abstract class CompositionObject : IDisposable
+ {
+ ///
+ /// The collection of implicit animations attached to this object.
+ ///
+ public ImplicitAnimationCollection? ImplicitAnimations { get; set; }
+
+ private protected InlineDictionary PendingAnimations;
+ internal CompositionObject(Compositor compositor, ServerObject server)
+ {
+ Compositor = compositor;
+ Server = server;
+ }
+
+ ///
+ /// The associated Compositor
+ ///
+ public Compositor Compositor { get; }
+ internal ServerObject Server { get; }
+ public bool IsDisposed { get; private set; }
+ private bool _registeredForSerialization;
+
+ private static void ThrowInvalidOperation() =>
+ throw new InvalidOperationException("There is no server-side counterpart for this object");
+
+ public void Dispose()
+ {
+ RegisterForSerialization();
+ IsDisposed = true;
+ }
+
+ ///
+ /// Connects an animation with the specified property of the object and starts the animation.
+ ///
+ public void StartAnimation(string propertyName, CompositionAnimation animation)
+ => StartAnimation(propertyName, animation, null);
+
+ internal virtual void StartAnimation(string propertyName, CompositionAnimation animation, ExpressionVariant? finalValue)
+ {
+ throw new ArgumentException("Unknown property " + propertyName);
+ }
+
+ ///
+ /// Starts an animation group.
+ /// The StartAnimationGroup method on CompositionObject lets you start CompositionAnimationGroup.
+ /// All the animations in the group will be started at the same time on the object.
+ ///
+ 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();
+ }
+
+ 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)
+ {
+ if (Server is IDisposable)
+ writer.Write((byte)(IsDisposed ? 1 : 0));
+ }
+ }
+}
\ 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..ee4552d154
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs
@@ -0,0 +1,147 @@
+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
+{
+ ///
+ /// s are s that allow storage of key values pairs
+ /// that can be shared across the application and are not tied to the lifetime of another composition object.
+ /// s are most commonly used with animations, where they maintain key-value pairs
+ /// that are referenced to drive portions of composition animations. s
+ /// provide the ability to insert key-value pairs or retrieve a value for a given key.
+ /// does not support a delete function – ensure you use
+ /// to store values that will be shared across the application.
+ ///
+ public class CompositionPropertySet : CompositionObject
+ {
+ private readonly Dictionary _variants = new Dictionary();
+ private readonly Dictionary _objects = new Dictionary();
+
+ internal CompositionPropertySet(Compositor compositor) : base(compositor, null!)
+ {
+ }
+
+ internal void Set(string key, ExpressionVariant value)
+ {
+ _objects.Remove(key);
+ _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);
+
+ 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() =>
+ SnapshotCore(1);
+
+ private PropertySetSnapshot SnapshotCore(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.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(o.Value.Server);
+ }
+
+ 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..25bbd4dc88
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
@@ -0,0 +1,138 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using Avalonia.Collections.Pooled;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.Composition
+{
+ ///
+ /// Represents the composition output (e. g. a window, embedded control, entire screen)
+ ///
+ public partial class CompositionTarget
+ {
+ partial void OnRootChanged()
+ {
+ if (Root != null)
+ Root.Root = this;
+ }
+
+ partial void OnRootChanging()
+ {
+ if (Root != null)
+ Root.Root = null;
+ }
+
+ ///
+ /// Attempts to perform a hit-tst
+ ///
+ ///
+ ///
+ ///
+ public PooledList? TryHitTest(Point point, Func? filter)
+ {
+ Server.Readback.NextRead();
+ if (Root == null)
+ return null;
+ var res = new PooledList();
+ HitTestCore(Root, point, res, filter);
+ return res;
+ }
+
+ ///
+ /// Attempts to transform a point to a particular CompositionVisual coordinate space
+ ///
+ ///
+ public Point? TryTransformToVisual(CompositionVisual visual, Point point)
+ {
+ if (visual.Root != this)
+ return null;
+ var v = visual;
+ var m = Matrix.Identity;
+ while (v != null)
+ {
+ if (!TryGetInvertedTransform(v, out var cm))
+ return null;
+ m = m * cm;
+ v = v.Parent;
+ }
+
+ return point * m;
+ }
+
+ bool TryGetInvertedTransform(CompositionVisual visual, out Matrix matrix)
+ {
+ var m = visual.TryGetServerTransform();
+ if (m == null)
+ {
+ matrix = default;
+ return false;
+ }
+
+ var m33 = MatrixUtils.ToMatrix(m.Value);
+ return m33.TryInvert(out matrix);
+ }
+
+ bool TryTransformTo(CompositionVisual visual, ref Point v)
+ {
+ if (TryGetInvertedTransform(visual, out var m))
+ {
+ v = v * m;
+ return true;
+ }
+
+ return false;
+ }
+
+ bool HitTestCore(CompositionVisual visual, Point point, PooledList result,
+ Func? filter)
+ {
+ //TODO: Check readback too
+ if (visual.Visible == false)
+ return false;
+ if (!TryTransformTo(visual, ref point))
+ return false;
+
+ if (visual.ClipToBounds
+ && (point.X < 0 || point.Y < 0 || point.X > visual.Size.X || point.Y > visual.Size.Y))
+ return false;
+ if (visual.Clip?.FillContains(point) == false)
+ return false;
+
+ bool success = false;
+ // Hit-test the current node
+ if (visual.HitTest(point, filter))
+ {
+ 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, filter);
+ if (hit)
+ return true;
+ }
+
+ return success;
+
+ }
+
+ ///
+ /// Registers the composition target for explicit redraw
+ ///
+ public void RequestRedraw() => RegisterForSerialization();
+
+ ///
+ /// Performs composition directly on the UI thread
+ ///
+ internal void ImmediateUIThreadRender()
+ {
+ Compositor.RequestCommitAsync();
+ Compositor.Server.Render();
+ }
+ }
+}
\ 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..1bdae44cb9
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Numerics;
+using System.Threading.Tasks;
+using Avalonia.Animation.Easings;
+using Avalonia.Media;
+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
+{
+ ///
+ /// The Compositor class manages communication between UI-thread and render-thread parts of the composition engine.
+ /// It also serves as a factory to create UI-thread parts of various composition objects
+ ///
+ public partial class Compositor
+ {
+ internal IRenderLoop Loop { get; }
+ private ServerCompositor _server;
+ private bool _implicitBatchCommitQueued;
+ private Action _implicitBatchCommit;
+ private BatchStreamObjectPool