Browse Source

Merge branch 'master' into feature/7120-control-themes

pull/8263/head
Steven Kirk 4 years ago
parent
commit
edef92f42f
  1. 0
      .ncrunch/ControlCatalog.net6.0.v3.ncrunchproject
  2. 5
      .ncrunch/ControlCatalog.netstandard2.0.v3.ncrunchproject
  3. 14
      Avalonia.sln
  4. 23
      azure-pipelines-integrationtests.yml
  5. 2
      build/SourceGenerators.props
  6. 6
      native/Avalonia.Native/src/OSX/AvnView.mm
  7. 6
      native/Avalonia.Native/src/OSX/rendertarget.mm
  8. 4
      samples/ControlCatalog.Android/ControlCatalog.Android.csproj
  9. 3
      samples/ControlCatalog.NetCore/NativeControls/Gtk/EmbedSample.Gtk.cs
  10. 9
      samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs
  11. 19
      samples/ControlCatalog.NetCore/Program.cs
  12. 3
      samples/ControlCatalog/MainView.xaml
  13. 44
      samples/ControlCatalog/Pages/ColorPickerPage.xaml
  14. 45
      samples/ControlCatalog/Pages/CompositionPage.axaml
  15. 153
      samples/ControlCatalog/Pages/CompositionPage.axaml.cs
  16. 27
      samples/ControlCatalog/Pages/ProgressBarPage.xaml
  17. 4
      samples/RenderDemo/App.xaml.cs
  18. 3
      src/Android/Avalonia.Android/ChoreographerTimer.cs
  19. 306
      src/Avalonia.Base/Animation/Easings/CubicBezier.cs
  20. 27
      src/Avalonia.Base/Animation/Easings/CubicBezierEasing.cs
  21. 12
      src/Avalonia.Base/Avalonia.Base.csproj
  22. 1
      src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs
  23. 2
      src/Avalonia.Base/Collections/Pooled/PooledList.cs
  24. 56
      src/Avalonia.Base/Controls/Classes.cs
  25. 14
      src/Avalonia.Base/Controls/IClassesChangedListener.cs
  26. 7
      src/Avalonia.Base/Controls/IPseudoClasses.cs
  27. 2
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  28. 8
      src/Avalonia.Base/Layout/LayoutManager.cs
  29. 16
      src/Avalonia.Base/Platform/IPlatformGpu.cs
  30. 82
      src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs
  31. 75
      src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs
  32. 24
      src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs
  33. 53
      src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs
  34. 49
      src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs
  35. 16
      src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs
  36. 15
      src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs
  37. 82
      src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs
  38. 76
      src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs
  39. 134
      src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs
  40. 178
      src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs
  41. 89
      src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs
  42. 49
      src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs
  43. 278
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  44. 68
      src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
  45. 141
      src/Avalonia.Base/Rendering/Composition/CompositionObject.cs
  46. 147
      src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs
  47. 138
      src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
  48. 141
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  49. 24
      src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs
  50. 102
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs
  51. 391
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
  52. 14
      src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs
  53. 120
      src/Avalonia.Base/Rendering/Composition/Enums.cs
  54. 237
      src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs
  55. 184
      src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs
  56. 377
      src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs
  57. 32
      src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs
  58. 14
      src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs
  59. 298
      src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs
  60. 57
      src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionTrackedValues.cs
  61. 730
      src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs
  62. 259
      src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs
  63. 66
      src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs
  64. 15
      src/Avalonia.Base/Rendering/Composition/Server/CompositionProperty.cs
  65. 179
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  66. 76
      src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs
  67. 46
      src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs
  68. 44
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs
  69. 75
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs
  70. 9
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs
  71. 220
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  72. 76
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs
  73. 246
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  74. 140
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
  75. 44
      src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs
  76. 180
      src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs
  77. 39
      src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs
  78. 184
      src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs
  79. 152
      src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs
  80. 9
      src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs
  81. 98
      src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs
  82. 56
      src/Avalonia.Base/Rendering/Composition/Visual.cs
  83. 73
      src/Avalonia.Base/Rendering/Composition/VisualCollection.cs
  84. 2
      src/Avalonia.Base/Rendering/DefaultRenderTimer.cs
  85. 6
      src/Avalonia.Base/Rendering/DeferredRenderer.cs
  86. 2
      src/Avalonia.Base/Rendering/IRenderLoop.cs
  87. 5
      src/Avalonia.Base/Rendering/IRenderTimer.cs
  88. 6
      src/Avalonia.Base/Rendering/IRenderer.cs
  89. 6
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  90. 2
      src/Avalonia.Base/Rendering/RenderLoop.cs
  91. 16
      src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs
  92. 5
      src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  93. 7
      src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs
  94. 11
      src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs
  95. 11
      src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs
  96. 8
      src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs
  97. 12
      src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs
  98. 11
      src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs
  99. 44
      src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs
  100. 4
      src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs

0
.ncrunch/ControlCatalog.v3.ncrunchproject → .ncrunch/ControlCatalog.net6.0.v3.ncrunchproject

5
.ncrunch/ControlCatalog.netstandard2.0.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

14
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}

23
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

2
build/SourceGenerators.props

@ -1,7 +1,7 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ProjectReference
Include="$(MSBuildThisFileDirectory)/../src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj"
Include="$(MSBuildThisFileDirectory)/../src/tools/DevGenerators/DevGenerators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"
PrivateAssets="all" />

6
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);
}
}
}

6
native/Avalonia.Native/src/OSX/rendertarget.mm

@ -13,6 +13,7 @@
{
@public IOSurfaceRef surface;
@public AvnPixelSize size;
@public bool hasContent;
@public float scale;
ComPtr<IAvnGlContext> _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;
}

4
samples/ControlCatalog.Android/ControlCatalog.Android.csproj

@ -21,12 +21,12 @@
<RunAOTCompilation>True</RunAOTCompilation>
</PropertyGroup>
<PropertyGroup Condition="'$(RunAOTCompilation)'=='True'">
<!-- PropertyGroup Condition="'$(RunAOTCompilation)'=='True'">
<EnableLLVM>True</EnableLLVM>
<AndroidAotAdditionalArguments>no-write-symbols,nodebug</AndroidAotAdditionalArguments>
<AndroidAotMode>Hybrid</AndroidAotMode>
<AndroidGenerateJniMarshalMethods>True</AndroidGenerateJniMarshalMethods>
</PropertyGroup>
</PropertyGroup -->
<PropertyGroup Condition="'$(AndroidEnableProfiler)'=='True'">
<IsEmulator Condition="'$(IsEmulator)' == ''">True</IsEmulator>

3
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}\"")
{

9
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<bool> 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"))
{

19
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<TabControl>().First();
foreach (var page in tc.Items.Cast<TabItem>().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<object>().First();
@ -77,7 +80,7 @@ namespace ControlCatalog.NetCore
for (var c = 0; c < 3; c++)
{
GC.Collect(2, GCCollectionMode.Forced);
await Task.Delay(500);
await Task.Delay(50);
}
void FormatMem(string metric, long bytes)
@ -87,7 +90,6 @@ namespace ControlCatalog.NetCore
FormatMem("GC allocated bytes", GC.GetTotalMemory(true));
FormatMem("WorkingSet64", Process.GetCurrentProcess().WorkingSet64);
}, TimeSpan.FromSeconds(1));
})
.StartWithClassicDesktopLifetime(args);
@ -111,10 +113,11 @@ namespace ControlCatalog.NetCore
{
EnableMultiTouch = true,
UseDBusMenu = true,
EnableIme = true,
EnableIme = true
})
.With(new Win32PlatformOptions
{
EnableMultitouch = true
})
.UseSkia()
.AfterSetup(builder =>

3
samples/ControlCatalog/MainView.xaml

@ -13,6 +13,9 @@
</Style>
</Grid.Styles>
<controls:HamburgerMenu Name="Sidebar">
<TabItem Header="Composition">
<pages:CompositionPage/>
</TabItem>
<TabItem Header="Acrylic">
<pages:AcrylicPage />
</TabItem>

44
samples/ControlCatalog/Pages/ColorPickerPage.xaml

@ -13,8 +13,14 @@
<pc:ThirdComponentConverter x:Key="ThirdComponent" />
</UserControl.Resources>
<Grid ColumnDefinitions="Auto,10,Auto">
<Grid Grid.Column="0"
<Grid ColumnDefinitions="Auto,10,Auto,10,Auto"
RowDefinitions="Auto,Auto">
<ColorPicker Grid.Column="0"
Grid.Row="1" />
<ColorView Grid.Column="0"
Grid.Row="0"
ColorSpectrumShape="Ring" />
<Grid Grid.Column="2"
Grid.Row="0"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto">
<ColorSpectrum x:Name="ColorSpectrum1"
@ -41,39 +47,11 @@
ColorModel="Hsva"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
<ColorPreviewer Grid.Row="5"
ShowAccentColors="True"
IsAccentColorsVisible="True"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
</Grid>
<Grid Grid.Column="2"
Grid.Row="0"
ColumnDefinitions="Auto,Auto,Auto"
RowDefinitions="Auto,Auto">
<ColorSlider Grid.Column="0"
Grid.Row="0"
IsAlphaMaxForced="True"
IsSaturationValueMaxForced="False"
ColorComponent="{Binding Components, ElementName=ColorSpectrum2, Converter={StaticResource ThirdComponent}}"
ColorModel="Hsva"
Orientation="Vertical"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" />
<ColorSpectrum x:Name="ColorSpectrum2"
Grid.Column="1"
Grid.Row="0"
Color="Green"
Shape="Ring"
Height="256"
Width="256" />
<ColorSlider Grid.Column="2"
Grid.Row="0"
ColorComponent="Alpha"
ColorModel="Hsva"
Orientation="Vertical"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" />
<ColorPreviewer Grid.Column="0"
Grid.ColumnSpan="3"
Grid.Row="1"
ShowAccentColors="True"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" />
<Grid Grid.Column="4"
Grid.Row="0">
</Grid>
</Grid>
</UserControl>

45
samples/ControlCatalog/Pages/CompositionPage.axaml

@ -0,0 +1,45 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:pages="clr-namespace:ControlCatalog.Pages"
x:Class="ControlCatalog.Pages.CompositionPage">
<StackPanel>
<TextBlock Classes="h1">Implicit animations</TextBlock>
<Grid ColumnDefinitions="*,10,40" Margin="0 0 40 0">
<ItemsControl x:Name="Items">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.DataTemplates>
<DataTemplate DataType="pages:CompositionPageColorItem">
<Border
pages:CompositionPage.EnableAnimations="True"
Padding="10" BorderBrush="Gray" BorderThickness="2"
Background="{Binding ColorBrush}" Width="100" Height="100" Margin="10">
<TextBlock Text="{Binding ColorHexValue}"/>
</Border>
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
<GridSplitter Margin="2" BorderThickness="1" BorderBrush="Gray"
Background="#e0e0e0" Grid.Column="1"
ResizeDirection="Columns" ResizeBehavior="PreviousAndNext"
/>
<Border Grid.Column="2">
<LayoutTransformControl
HorizontalAlignment="Center"
MinWidth="30">
<LayoutTransformControl.LayoutTransform>
<RotateTransform Angle="90"/>
</LayoutTransformControl.LayoutTransform>
<TextBlock>Resize me</TextBlock>
</LayoutTransformControl>
</Border>
</Grid>
</StackPanel>
</UserControl>

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

@ -0,0 +1,153 @@
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<ItemsControl>("Items").Items = CreateColorItems();
}
private List<CompositionPageColorItem> CreateColorItems()
{
var list = new List<CompositionPageColorItem>();
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 185, 0)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 231, 72, 86)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 120, 215)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 153, 188)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 122, 117, 116)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 118, 118, 118)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 141, 0)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 232, 17, 35)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 99, 177)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 45, 125, 154)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 93, 90, 88)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 76, 74, 72)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 247, 99, 12)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 234, 0, 94)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 142, 140, 216)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 183, 195)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 104, 118, 138)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 105, 121, 126)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 202, 80, 16)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 195, 0, 82)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 107, 105, 214)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 3, 131, 135)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 81, 92, 107)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 74, 84, 89)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 218, 59, 1)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 227, 0, 140)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 135, 100, 184)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 178, 148)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 86, 124, 115)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 100, 124, 100)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 239, 105, 80)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 191, 0, 119)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 116, 77, 169)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 1, 133, 116)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 72, 104, 96)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 82, 94, 84)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 209, 52, 56)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 194, 57, 179)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 177, 70, 194)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 204, 106)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 73, 130, 5)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 132, 117, 69)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 67, 67)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 154, 0, 137)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 136, 23, 152)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 16, 137, 62)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 16, 124, 16)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 126, 115, 95)));
return list;
}
private void EnsureImplicitAnimations()
{
if (_implicitAnimations == null)
{
var compositor = ElementComposition.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<CompositionPage>();
if (page == null)
{
border.AttachedToVisualTree += delegate { SetEnableAnimations(border, true); };
return;
}
if (ElementComposition.GetElementVisual(page) == null)
return;
page.EnsureImplicitAnimations();
ElementComposition.GetElementVisual((Visual)border.GetVisualParent()).ImplicitAnimations =
page._implicitAnimations;
}
}
public class CompositionPageColorItem
{
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 CompositionPageColorItem(Color color)
{
Color = color;
}
}

27
samples/ControlCatalog/Pages/ProgressBarPage.xaml

@ -1,22 +1,37 @@
<UserControl xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.Pages.ProgressBarPage">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h2">A progress bar control</TextBlock>
<StackPanel>
<StackPanel Spacing="5">
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock VerticalAlignment="Center">Maximum</TextBlock>
<NumericUpDown x:Name="maximum" Value="100" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock VerticalAlignment="Center">Minimum</TextBlock>
<NumericUpDown x:Name="minimum" Value="0" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock VerticalAlignment="Center">Progress Text Format</TextBlock>
<TextBox x:Name="stringFormat" Text="{}{0:0}%" VerticalAlignment="Center"/>
</StackPanel>
<CheckBox x:Name="showProgress" Margin="10,16,0,0" Content="Show Progress Text" />
<CheckBox x:Name="isIndeterminate" Margin="10,16,0,0" Content="Toggle Indeterminate" />
<StackPanel Orientation="Horizontal" Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16">
<StackPanel Spacing="16">
<ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #hprogress.Value}" />
<ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #hprogress.Value}"
Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.Value}" ProgressTextFormat="{Binding #stringFormat.Text}"/>
</StackPanel>
<ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #vprogress.Value}" Orientation="Vertical" />
<ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #vprogress.Value}" Orientation="Vertical"
Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.Value}" ProgressTextFormat="{Binding #stringFormat.Text}"/>
</StackPanel>
<StackPanel Margin="16">
<Slider Name="hprogress" Maximum="100" Value="40" />
<Slider Name="vprogress" Maximum="100" Value="60" />
<Slider Name="hprogress" Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.Value}" Value="40" />
<Slider Name="vprogress" Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.Value}" Value="60" />
</StackPanel>
<StackPanel Spacing="10">
<ProgressBar VerticalAlignment="Center" IsIndeterminate="True" />
<ProgressBar VerticalAlignment="Center" IsIndeterminate="True"
Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.value}"/>
<ProgressBar VerticalAlignment="Center" Value="5" Maximum="10" />
<ProgressBar VerticalAlignment="Center" Value="50" />
<ProgressBar VerticalAlignment="Center" Value="50" Minimum="25" Maximum="75" />

4
samples/RenderDemo/App.xaml.cs

@ -29,6 +29,10 @@ namespace RenderDemo
.With(new Win32PlatformOptions
{
OverlayPopups = true,
})
.With(new X11PlatformOptions
{
UseCompositor = true
})
.UsePlatformDetect()
.LogToTrace();

3
src/Android/Avalonia.Android/ChoreographerTimer.cs

@ -29,6 +29,9 @@ namespace Avalonia.Android
_thread = new Thread(Loop);
_thread.Start();
}
public bool RunsInBackground => true;
public event Action<TimeSpan> Tick
{

306
src/Avalonia.Base/Animation/Easings/CubicBezier.cs

@ -0,0 +1,306 @@
// ReSharper disable InconsistentNaming
// Ported from Chromium project https://github.com/chromium/chromium/blob/374d31b7704475fa59f7b2cb836b3b68afdc3d79/ui/gfx/geometry/cubic_bezier.cc
using System;
using Avalonia.Utilities;
// ReSharper disable CompareOfFloatsByEqualityOperator
// ReSharper disable CommentTypo
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable TooWideLocalVariableScope
// ReSharper disable UnusedMember.Global
#pragma warning disable 649
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Represents a cubic bezier curve and can compute Y coordinate for a given X
/// </summary>
internal unsafe struct CubicBezier
{
const int CUBIC_BEZIER_SPLINE_SAMPLES = 11;
double ax_;
double bx_;
double cx_;
double ay_;
double by_;
double cy_;
double start_gradient_;
double end_gradient_;
double range_min_;
double range_max_;
private bool monotonically_increasing_;
fixed double spline_samples_[CUBIC_BEZIER_SPLINE_SAMPLES];
public CubicBezier(double p1x, double p1y, double p2x, double p2y) : this()
{
InitCoefficients(p1x, p1y, p2x, p2y);
InitGradients(p1x, p1y, p2x, p2y);
InitRange(p1y, p2y);
InitSpline();
}
public readonly double SampleCurveX(double t)
{
// `ax t^3 + bx t^2 + cx t' expanded using Horner's rule.
return ((ax_ * t + bx_) * t + cx_) * t;
}
readonly double SampleCurveY(double t)
{
return ((ay_ * t + by_) * t + cy_) * t;
}
readonly double SampleCurveDerivativeX(double t)
{
return (3.0 * ax_ * t + 2.0 * bx_) * t + cx_;
}
readonly double SampleCurveDerivativeY(double t)
{
return (3.0 * ay_ * t + 2.0 * by_) * t + cy_;
}
public readonly double SolveWithEpsilon(double x, double epsilon)
{
if (x < 0.0)
return 0.0 + start_gradient_ * x;
if (x > 1.0)
return 1.0 + end_gradient_ * (x - 1.0);
return SampleCurveY(SolveCurveX(x, epsilon));
}
void InitCoefficients(double p1x,
double p1y,
double p2x,
double p2y)
{
// Calculate the polynomial coefficients, implicit first and last control
// points are (0,0) and (1,1).
cx_ = 3.0 * p1x;
bx_ = 3.0 * (p2x - p1x) - cx_;
ax_ = 1.0 - cx_ - bx_;
cy_ = 3.0 * p1y;
by_ = 3.0 * (p2y - p1y) - cy_;
ay_ = 1.0 - cy_ - by_;
#if DEBUG
// Bezier curves with x-coordinates outside the range [0,1] for internal
// control points may have multiple values for t for a given value of x.
// In this case, calls to SolveCurveX may produce ambiguous results.
monotonically_increasing_ = p1x >= 0 && p1x <= 1 && p2x >= 0 && p2x <= 1;
#endif
}
void InitGradients(double p1x,
double p1y,
double p2x,
double p2y)
{
// End-point gradients are used to calculate timing function results
// outside the range [0, 1].
//
// There are four possibilities for the gradient at each end:
// (1) the closest control point is not horizontally coincident with regard to
// (0, 0) or (1, 1). In this case the line between the end point and
// the control point is tangent to the bezier at the end point.
// (2) the closest control point is coincident with the end point. In
// this case the line between the end point and the far control
// point is tangent to the bezier at the end point.
// (3) both internal control points are coincident with an endpoint. There
// are two special case that fall into this category:
// CubicBezier(0, 0, 0, 0) and CubicBezier(1, 1, 1, 1). Both are
// equivalent to linear.
// (4) the closest control point is horizontally coincident with the end
// point, but vertically distinct. In this case the gradient at the
// end point is Infinite. However, this causes issues when
// interpolating. As a result, we break down to a simple case of
// 0 gradient under these conditions.
if (p1x > 0)
start_gradient_ = p1y / p1x;
else if (p1y == 0 && p2x > 0)
start_gradient_ = p2y / p2x;
else if (p1y == 0 && p2y == 0)
start_gradient_ = 1;
else
start_gradient_ = 0;
if (p2x < 1)
end_gradient_ = (p2y - 1) / (p2x - 1);
else if (p2y == 1 && p1x < 1)
end_gradient_ = (p1y - 1) / (p1x - 1);
else if (p2y == 1 && p1y == 1)
end_gradient_ = 1;
else
end_gradient_ = 0;
}
const double kBezierEpsilon = 1e-7;
void InitRange(double p1y, double p2y)
{
range_min_ = 0;
range_max_ = 1;
if (0 <= p1y && p1y < 1 && 0 <= p2y && p2y <= 1)
return;
double epsilon = kBezierEpsilon;
// Represent the function's derivative in the form at^2 + bt + c
// as in sampleCurveDerivativeY.
// (Technically this is (dy/dt)*(1/3), which is suitable for finding zeros
// but does not actually give the slope of the curve.)
double a = 3.0 * ay_;
double b = 2.0 * by_;
double c = cy_;
// Check if the derivative is constant.
if (Math.Abs(a) < epsilon && Math.Abs(b) < epsilon)
return;
// Zeros of the function's derivative.
double t1;
double t2 = 0;
if (Math.Abs(a) < epsilon)
{
// The function's derivative is linear.
t1 = -c / b;
}
else
{
// The function's derivative is a quadratic. We find the zeros of this
// quadratic using the quadratic formula.
double discriminant = b * b - 4 * a * c;
if (discriminant < 0)
return;
double discriminant_sqrt = Math.Sqrt(discriminant);
t1 = (-b + discriminant_sqrt) / (2 * a);
t2 = (-b - discriminant_sqrt) / (2 * a);
}
double sol1 = 0;
double sol2 = 0;
// If the solution is in the range [0,1] then we include it, otherwise we
// ignore it.
// An interesting fact about these beziers is that they are only
// actually evaluated in [0,1]. After that we take the tangent at that point
// and linearly project it out.
if (0 < t1 && t1 < 1)
sol1 = SampleCurveY(t1);
if (0 < t2 && t2 < 1)
sol2 = SampleCurveY(t2);
range_min_ = Math.Min(Math.Min(range_min_, sol1), sol2);
range_max_ = Math.Max(Math.Max(range_max_, sol1), sol2);
}
void InitSpline()
{
double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1);
for (int i = 0; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++)
{
spline_samples_[i] = SampleCurveX(i * delta_t);
}
}
const int kMaxNewtonIterations = 4;
public readonly double SolveCurveX(double x, double epsilon)
{
if (x < 0 || x > 1)
throw new ArgumentException();
double t0 = 0;
double t1 = 0;
double t2 = x;
double x2 = 0;
double d2;
int i;
#if DEBUG
if (!monotonically_increasing_)
throw new InvalidOperationException();
#endif
// Linear interpolation of spline curve for initial guess.
double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1);
for (i = 1; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++)
{
if (x <= spline_samples_[i])
{
t1 = delta_t * i;
t0 = t1 - delta_t;
t2 = t0 + (t1 - t0) * (x - spline_samples_[i - 1]) /
(spline_samples_[i] - spline_samples_[i - 1]);
break;
}
}
// Perform a few iterations of Newton's method -- normally very fast.
// See https://en.wikipedia.org/wiki/Newton%27s_method.
double newton_epsilon = Math.Min(kBezierEpsilon, epsilon);
for (i = 0; i < kMaxNewtonIterations; i++)
{
x2 = SampleCurveX(t2) - x;
if (Math.Abs(x2) < newton_epsilon)
return t2;
d2 = SampleCurveDerivativeX(t2);
if (Math.Abs(d2) < kBezierEpsilon)
break;
t2 = t2 - x2 / d2;
}
if (Math.Abs(x2) < epsilon)
return t2;
// Fall back to the bisection method for reliability.
while (t0 < t1)
{
x2 = SampleCurveX(t2);
if (Math.Abs(x2 - x) < epsilon)
return t2;
if (x > x2)
t0 = t2;
else
t1 = t2;
t2 = (t1 + t0) * .5;
}
// Failure.
return t2;
}
public readonly double Solve(double x)
{
return SolveWithEpsilon(x, kBezierEpsilon);
}
public readonly double SlopeWithEpsilon(double x, double epsilon)
{
x = MathUtilities.Clamp(x, 0.0, 1.0);
double t = SolveCurveX(x, epsilon);
double dx = SampleCurveDerivativeX(t);
double dy = SampleCurveDerivativeY(t);
return dy / dx;
}
public readonly double Slope(double x)
{
return SlopeWithEpsilon(x, kBezierEpsilon);
}
public readonly double RangeMin => range_min_;
public readonly double RangeMax => range_max_;
}
}

27
src/Avalonia.Base/Animation/Easings/CubicBezierEasing.cs

@ -0,0 +1,27 @@
using System;
namespace Avalonia.Animation.Easings;
public class CubicBezierEasing : IEasing
{
private CubicBezier _bezier;
//cubic-bezier(0.25, 0.1, 0.25, 1.0)
internal CubicBezierEasing(Point controlPoint1, Point controlPoint2)
{
ControlPoint1 = controlPoint1;
ControlPoint2 = controlPoint2;
if (controlPoint1.X < 0 || controlPoint1.X > 1 || controlPoint2.X < 0 || controlPoint2.X > 1)
throw new ArgumentException();
_bezier = new CubicBezier(controlPoint1.X, controlPoint1.Y, controlPoint2.X, controlPoint2.Y);
}
public Point ControlPoint2 { get; set; }
public Point ControlPoint1 { get; set; }
internal static IEasing Ease { get; } = new CubicBezierEasing(new Point(0.25, 0.1), new Point(0.25, 1));
double IEasing.Ease(double progress)
{
return _bezier.Solve(progress);
}
}

12
src/Avalonia.Base/Avalonia.Base.csproj

@ -3,10 +3,13 @@
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<AssemblyName>Avalonia.Base</AssemblyName>
<RootNamespace>Avalonia</RootNamespace>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Assets\*.trie" />
<AdditionalFiles Include="composition-schema.xml" />
</ItemGroup>
<Import Project="..\..\build\Base.props" />
<Import Project="..\..\build\Binding.props" />
@ -32,6 +35,11 @@
<InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Web.Blazor, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7"/>
<InternalsVisibleTo Include="Avalonia.Dialogs, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7" />
</ItemGroup>
<ItemGroup>
<Folder Include="Rendering\Composition\Utils" />
</ItemGroup>
</Project>

1
src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;

2
src/Avalonia.Base/Collections/Pooled/PooledList.cs

@ -1434,7 +1434,7 @@ namespace Avalonia.Collections.Pooled
/// <summary>
/// Returns the internal buffers to the ArrayPool.
/// </summary>
public void Dispose()
public virtual void Dispose()
{
ReturnArray();
_size = 0;

56
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
/// </remarks>
public class Classes : AvaloniaList<string>, IPseudoClasses
{
private SafeEnumerableList<IClassesChangedListener>? _listeners;
/// <summary>
/// Initializes a new instance of the <see cref="Classes"/> class.
/// </summary>
@ -39,6 +40,11 @@ namespace Avalonia.Controls
{
}
/// <summary>
/// Gets the number of listeners subscribed to this collection for unit testing purposes.
/// </summary>
internal int ListenerCount => _listeners?.Count ?? 0;
/// <summary>
/// Parses a classes string.
/// </summary>
@ -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();
}
/// <summary>
@ -103,6 +111,8 @@ namespace Avalonia.Controls
RemoveAt(i);
}
}
NotifyChanged();
}
/// <summary>
@ -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;
}
/// <summary>
@ -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();
}
/// <summary>
@ -224,6 +245,7 @@ namespace Avalonia.Controls
public override void RemoveRange(int index, int count)
{
base.RemoveRange(index, count);
NotifyChanged();
}
/// <summary>
@ -255,6 +277,7 @@ namespace Avalonia.Controls
}
base.AddRange(source);
NotifyChanged();
}
/// <inheritdoc/>
@ -263,13 +286,38 @@ namespace Avalonia.Controls
if (!Contains(name))
{
base.Add(name);
NotifyChanged();
}
}
/// <inheritdoc/>
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)

14
src/Avalonia.Base/Controls/IClassesChangedListener.cs

@ -0,0 +1,14 @@
namespace Avalonia.Controls
{
/// <summary>
/// Internal interface for listening to changes in <see cref="Classes"/> in a more
/// performant manner than subscribing to CollectionChanged.
/// </summary>
internal interface IClassesChangedListener
{
/// <summary>
/// Notifies the listener that the <see cref="Classes"/> collection has changed.
/// </summary>
void Changed();
}
}

7
src/Avalonia.Base/Controls/IPseudoClasses.cs

@ -19,5 +19,12 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="name">The pseudoclass name.</param>
bool Remove(string name);
/// <summary>
/// Returns whether a pseudoclass is present in the collection.
/// </summary>
/// <param name="name">The pseudoclass name.</param>
/// <returns>Whether the pseudoclass is present.</returns>
bool Contains(string name);
}
}

2
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();

8
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; }
}
}
}

16
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();
}

82
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;
/// <summary>
/// The base class for both key-frame and expression animation instances
/// Is responsible for activation tracking and for subscribing to properties used in dependencies
/// </summary>
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);
}
}

75
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
{
/// <summary>
/// This is the base class for ExpressionAnimation and KeyFrameAnimation.
/// </summary>
/// <remarks>
/// Use the <see cref="CompositionObject.StartAnimation"/> method to start the animation.
/// Value parameters (as opposed to reference parameters which are set using <see cref="SetReferenceParameter"/>)
/// are copied and "embedded" into an expression at the time CompositionObject.StartAnimation is called.
/// Changing the value of the variable after <see cref="CompositionObject.StartAnimation"/> is called will not affect
/// the value of the ExpressionAnimation.
/// See the remarks section of ExpressionAnimation for additional information.
/// </remarks>
public abstract class CompositionAnimation : CompositionObject, ICompositionAnimationBase
{
private readonly CompositionPropertySet _propertySet;
internal CompositionAnimation(Compositor compositor) : base(compositor, null!)
{
_propertySet = new CompositionPropertySet(compositor);
}
/// <summary>
/// Clears all of the parameters of the animation.
/// </summary>
public void ClearAllParameters() => _propertySet.ClearAll();
/// <summary>
/// Clears a parameter from the animation.
/// </summary>
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()
{
}
}
}

24
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<CompositionAnimation> Animations { get; } = new List<CompositionAnimation>();
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!)
{
}
}
}

53
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
{
/// <summary>
/// A Composition Animation that uses a mathematical equation to calculate the value for an animating property every frame.
/// </summary>
/// <remarks>
/// 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 <see cref="KeyFrameAnimation"/>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 <see cref="CompositionObject.StartAnimation"/> method to start the animation.
/// </remarks>
public class ExpressionAnimation : CompositionAnimation
{
private string? _expression;
private Expression? _parsedExpression;
internal ExpressionAnimation(Compositor compositor) : base(compositor)
{
}
/// <summary>
/// The mathematical equation specifying how the animated value is calculated each frame.
/// The Expression is the core of an <see cref="ExpressionAnimation"/> 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.
/// </summary>
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());
}
}

49
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
{
/// <summary>
/// Server-side counterpart of <see cref="ExpressionAnimation"/> with values baked-in.
/// </summary>
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;
}
}
}

16
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();
}
}

15
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
{
/// <summary>
/// Base class for composition animations.
/// </summary>
public interface ICompositionAnimationBase
{
internal void InternalOnly();
}
}

82
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
{
/// <summary>
/// A collection of animations triggered when a condition is met.
/// </summary>
/// <remarks>
/// 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
/// </remarks>
public class ImplicitAnimationCollection : CompositionObject, IDictionary<string, ICompositionAnimationBase>
{
private Dictionary<string, ICompositionAnimationBase> _inner = new Dictionary<string, ICompositionAnimationBase>();
private IDictionary<string, ICompositionAnimationBase> _innerface;
internal ImplicitAnimationCollection(Compositor compositor) : base(compositor, null!)
{
_innerface = _inner;
}
public IEnumerator<KeyValuePair<string, ICompositionAnimationBase>> GetEnumerator() => _inner.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable) _inner).GetEnumerator();
void ICollection<KeyValuePair<string, ICompositionAnimationBase>>.Add(KeyValuePair<string, ICompositionAnimationBase> item) => _innerface.Add(item);
public void Clear() => _inner.Clear();
bool ICollection<KeyValuePair<string, ICompositionAnimationBase>>.Contains(KeyValuePair<string, ICompositionAnimationBase> item) => _innerface.Contains(item);
void ICollection<KeyValuePair<string, ICompositionAnimationBase>>.CopyTo(KeyValuePair<string, ICompositionAnimationBase>[] array, int arrayIndex) => _innerface.CopyTo(array, arrayIndex);
bool ICollection<KeyValuePair<string, ICompositionAnimationBase>>.Remove(KeyValuePair<string, ICompositionAnimationBase> item) => _innerface.Remove(item);
public int Count => _inner.Count;
bool ICollection<KeyValuePair<string, ICompositionAnimationBase>>.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<string> IDictionary<string, ICompositionAnimationBase>.Keys => _innerface.Keys;
ICollection<ICompositionAnimationBase> IDictionary<string, ICompositionAnimationBase>.Values =>
_innerface.Values;
// UWP compat
public uint Size => (uint) Count;
public IReadOnlyDictionary<string, ICompositionAnimationBase> GetView() =>
new Dictionary<string, ICompositionAnimationBase>(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;
}
}
}

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

@ -0,0 +1,76 @@
using System;
using System.Numerics;
namespace Avalonia.Rendering.Composition.Animations
{
/// <summary>
/// An interface to define interpolation logic for a particular type
/// </summary>
internal interface IInterpolator<T>
{
T Interpolate(T from, T to, float progress);
}
class ScalarInterpolator : IInterpolator<float>
{
public float Interpolate(float @from, float to, float progress) => @from + (to - @from) * progress;
public static ScalarInterpolator Instance { get; } = new ScalarInterpolator();
}
class Vector2Interpolator : IInterpolator<Vector2>
{
public Vector2 Interpolate(Vector2 @from, Vector2 to, float progress)
=> Vector2.Lerp(@from, to, progress);
public static Vector2Interpolator Instance { get; } = new Vector2Interpolator();
}
class Vector3Interpolator : IInterpolator<Vector3>
{
public Vector3 Interpolate(Vector3 @from, Vector3 to, float progress)
=> Vector3.Lerp(@from, to, progress);
public static Vector3Interpolator Instance { get; } = new Vector3Interpolator();
}
class Vector4Interpolator : IInterpolator<Vector4>
{
public Vector4 Interpolate(Vector4 @from, Vector4 to, float progress)
=> Vector4.Lerp(@from, to, progress);
public static Vector4Interpolator Instance { get; } = new Vector4Interpolator();
}
class QuaternionInterpolator : IInterpolator<Quaternion>
{
public Quaternion Interpolate(Quaternion @from, Quaternion to, float progress)
=> Quaternion.Lerp(@from, to, progress);
public static QuaternionInterpolator Instance { get; } = new QuaternionInterpolator();
}
class ColorInterpolator : IInterpolator<Avalonia.Media.Color>
{
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<bool>
{
public bool Interpolate(bool @from, bool to, float progress) => progress >= 1 ? to : @from;
public static BooleanInterpolator Instance { get; } = new BooleanInterpolator();
}
}

134
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
{
/// <summary>
/// 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.
/// </summary>
public abstract class KeyFrameAnimation : CompositionAnimation
{
private TimeSpan _duration = TimeSpan.FromMilliseconds(1);
internal KeyFrameAnimation(Compositor compositor) : base(compositor)
{
}
/// <summary>
/// The delay behavior of the key frame animation.
/// </summary>
public AnimationDelayBehavior DelayBehavior { get; set; }
/// <summary>
/// Delay before the animation starts after <see cref="CompositionObject.StartAnimation"/> is called.
/// </summary>
public System.TimeSpan DelayTime { get; set; }
/// <summary>
/// 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 <see cref="IterationCount"/> greater than one.
/// This gives an easy way for customizing animation definitions.
/// </summary>
public PlaybackDirection Direction { get; set; }
/// <summary>
/// The duration of the animation.
/// Minimum allowed value is 1ms and maximum allowed value is 24 days.
/// </summary>
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;
}
}
/// <summary>
/// The iteration behavior for the key frame animation.
/// </summary>
public AnimationIterationBehavior IterationBehavior { get; set; }
/// <summary>
/// The number of times to repeat the key frame animation.
/// </summary>
public int IterationCount { get; set; } = 1;
/// <summary>
/// Specifies how to set the property value when animation is stopped
/// </summary>
public AnimationStopBehavior StopBehavior { get; set; }
private protected abstract IKeyFrames KeyFrames { get; }
/// <summary>
/// Inserts an expression keyframe.
/// </summary>
/// <param name="normalizedProgressKey">
/// 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.
/// </param>
/// <param name="value">The expression used to calculate the value of the key frame.</param>
/// <param name="easingFunction">The easing function to use when interpolating between frames.</param>
public void InsertExpressionKeyFrame(float normalizedProgressKey, string value,
Easing? easingFunction = null) =>
KeyFrames.InsertExpressionKeyFrame(normalizedProgressKey, value, easingFunction ?? Compositor.DefaultEasing);
}
/// <summary>
/// Specifies the animation delay behavior.
/// </summary>
public enum AnimationDelayBehavior
{
/// <summary>
/// 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.
/// </summary>
SetInitialValueAfterDelay,
/// <summary>
/// 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.
/// </summary>
SetInitialValueBeforeDelay
}
/// <summary>
/// Specifies if the animation should loop.
/// </summary>
public enum AnimationIterationBehavior
{
/// <summary>
/// The animation should loop the specified number of times.
/// </summary>
Count,
/// <summary>
/// The animation should loop forever.
/// </summary>
Forever
}
/// <summary>
/// Specifies the behavior of an animation when it stops.
/// </summary>
public enum AnimationStopBehavior
{
/// <summary>
/// Leave the animation at its current value.
/// </summary>
LeaveCurrentValue,
/// <summary>
/// Reset the animation to its initial value.
/// </summary>
SetToInitialValue,
/// <summary>
/// Set the animation to its final value.
/// </summary>
SetToFinalValue
}
}

178
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
{
/// <summary>
/// Server-side counterpart of KeyFrameAnimation with values baked-in
/// </summary>
class KeyFrameAnimationInstance<T> : AnimationInstanceBase, IAnimationInstance where T : struct
{
private readonly IInterpolator<T> _interpolator;
private readonly ServerKeyFrame<T>[] _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<T> interpolator, ServerKeyFrame<T>[] 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<T>
{
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<T> f)
{
if (f.Expression != null)
return f.Expression.Evaluate(ref ctx).CastOrDefault<T>();
else
return f.Value;
}
public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property)
{
_startedAt = startedAt;
_startingValue = startingValue.CastOrDefault<T>();
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();
}
}
}

89
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
{
/// <summary>
/// Collection of composition animation key frames
/// </summary>
/// <typeparam name="T"></typeparam>
class KeyFrames<T> : List<KeyFrame<T>>, 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<T>
{
NormalizedProgressKey = normalizedProgressKey,
Expression = Expression.Parse(value),
EasingFunction = easingFunction
});
}
public void Insert(float normalizedProgressKey, T value, IEasing easingFunction)
{
Validate(normalizedProgressKey);
Add(new KeyFrame<T>
{
NormalizedProgressKey = normalizedProgressKey,
Value = value,
EasingFunction = easingFunction
});
}
public ServerKeyFrame<T>[] Snapshot()
{
var frames = new ServerKeyFrame<T>[Count];
for (var c = 0; c < Count; c++)
{
var f = this[c];
frames[c] = new ServerKeyFrame<T>
{
Expression = f.Expression,
Value = f.Value,
EasingFunction = f.EasingFunction,
Key = f.NormalizedProgressKey
};
}
return frames;
}
}
/// <summary>
/// Composition animation key frame
/// </summary>
struct KeyFrame<T>
{
public float NormalizedProgressKey;
public T Value;
public Expression Expression;
public IEasing EasingFunction;
}
/// <summary>
/// Server-side composition animation key frame
/// </summary>
struct ServerKeyFrame<T>
{
public T Value;
public Expression? Expression;
public IEasing EasingFunction;
public float Key;
}
interface IKeyFrames
{
public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, IEasing easingFunction);
}
}

49
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
{
/// <summary>
/// A snapshot of properties used by an animation
/// </summary>
internal class PropertySetSnapshot : IExpressionParameterCollection, IExpressionObject
{
private readonly Dictionary<string, Value> _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<string, Value> 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);
}
}

278
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;
/// <summary>
/// A renderer that utilizes <see cref="Avalonia.Rendering.Composition.Compositor"/> to render the visual tree
/// </summary>
public class CompositingRenderer : IRendererWithCompositor
{
private readonly IRenderRoot _root;
private readonly Compositor _compositor;
CompositionDrawingContext _recorder = new();
DrawingContext _recordingContext;
private HashSet<Visual> _dirty = new();
private HashSet<Visual> _recalculateChildren = new();
private readonly CompositionTarget _target;
private bool _queuedUpdate;
private Action _update;
private Action _invalidateScene;
/// <summary>
/// Asks the renderer to only draw frames on the render thread. Makes Paint to wait until frame is rendered.
/// </summary>
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;
}
/// <inheritdoc/>
public bool DrawFps
{
get => _target.DrawFps;
set => _target.DrawFps = value;
}
/// <inheritdoc/>
public bool DrawDirtyRects
{
get => _target.DrawDirtyRects;
set => _target.DrawDirtyRects = value;
}
/// <inheritdoc/>
public event EventHandler<SceneInvalidatedEventArgs>? SceneInvalidated;
void QueueUpdate()
{
if(_queuedUpdate)
return;
_queuedUpdate = true;
Dispatcher.UIThread.Post(_update, DispatcherPriority.Composition);
}
/// <inheritdoc/>
public void AddDirty(IVisual visual)
{
_dirty.Add((Visual)visual);
QueueUpdate();
}
/// <inheritdoc/>
public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool>? 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;
}
}
}
/// <inheritdoc/>
public IVisual? HitTestFirst(Point p, IVisual root, Func<IVisual, bool>? filter)
{
// TODO: Optimize
return HitTest(p, root, filter).FirstOrDefault();
}
/// <inheritdoc/>
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<IVisual>)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();
}
/// <summary>
/// The associated <see cref="Avalonia.Rendering.Composition.Compositor"/> object
/// </summary>
public Compositor Compositor => _compositor;
}

68
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;
/// <summary>
/// A composition visual that holds a list of drawing commands issued by <see cref="Avalonia.Visual"/>
/// </summary>
internal class CompositionDrawListVisual : CompositionContainerVisual
{
/// <summary>
/// The associated <see cref="Avalonia.Visual"/>
/// </summary>
public Visual Visual { get; }
private bool _drawListChanged;
private CompositionDrawList? _drawList;
/// <summary>
/// The list of drawing commands
/// </summary>
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<IVisual, bool>? 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;
}
}

141
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
{
/// <summary>
/// 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 <see cref="CompositionVisual" /> objects each representing a single node in a Visual tree.
/// </summary>
public abstract class CompositionObject : IDisposable
{
/// <summary>
/// The collection of implicit animations attached to this object.
/// </summary>
public ImplicitAnimationCollection? ImplicitAnimations { get; set; }
private protected InlineDictionary<CompositionProperty, IAnimationInstance> PendingAnimations;
internal CompositionObject(Compositor compositor, ServerObject server)
{
Compositor = compositor;
Server = server;
}
/// <summary>
/// The associated Compositor
/// </summary>
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;
}
/// <summary>
/// Connects an animation with the specified property of the object and starts the animation.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
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));
}
}
}

147
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
{
/// <summary>
/// <see cref="CompositionPropertySet"/>s are <see cref="CompositionObject"/>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.
/// <see cref="CompositionPropertySet"/>s are most commonly used with animations, where they maintain key-value pairs
/// that are referenced to drive portions of composition animations. <see cref="CompositionPropertySet"/>s
/// provide the ability to insert key-value pairs or retrieve a value for a given key.
/// <see cref="CompositionPropertySet"/> does not support a delete function – ensure you use <see cref="CompositionPropertySet"/>
/// to store values that will be shared across the application.
/// </summary>
public class CompositionPropertySet : CompositionObject
{
private readonly Dictionary<string, ExpressionVariant> _variants = new Dictionary<string, ExpressionVariant>();
private readonly Dictionary<string, CompositionObject> _objects = new Dictionary<string, CompositionObject>();
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<T>(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<string, PropertySetSnapshot.Value>(_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
}
}

138
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
{
/// <summary>
/// Represents the composition output (e. g. a window, embedded control, entire screen)
/// </summary>
public partial class CompositionTarget
{
partial void OnRootChanged()
{
if (Root != null)
Root.Root = this;
}
partial void OnRootChanging()
{
if (Root != null)
Root.Root = null;
}
/// <summary>
/// Attempts to perform a hit-tst
/// </summary>
/// <param name="point"></param>
/// <param name="filter"></param>
/// <returns></returns>
public PooledList<CompositionVisual>? TryHitTest(Point point, Func<IVisual, bool>? filter)
{
Server.Readback.NextRead();
if (Root == null)
return null;
var res = new PooledList<CompositionVisual>();
HitTestCore(Root, point, res, filter);
return res;
}
/// <summary>
/// Attempts to transform a point to a particular CompositionVisual coordinate space
/// </summary>
/// <returns></returns>
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<CompositionVisual> result,
Func<IVisual, bool>? 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;
}
/// <summary>
/// Registers the composition target for explicit redraw
/// </summary>
public void RequestRedraw() => RegisterForSerialization();
/// <summary>
/// Performs composition directly on the UI thread
/// </summary>
internal void ImmediateUIThreadRender()
{
Compositor.RequestCommitAsync();
Compositor.Server.Render();
}
}
}

141
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
{
/// <summary>
/// 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
/// </summary>
public partial class Compositor
{
internal IRenderLoop Loop { get; }
private ServerCompositor _server;
private bool _implicitBatchCommitQueued;
private Action _implicitBatchCommit;
private BatchStreamObjectPool<object?> _batchObjectPool = new();
private BatchStreamMemoryPool _batchMemoryPool = new();
private List<CompositionObject> _objectsForSerialization = new();
internal ServerCompositor Server => _server;
internal IEasing DefaultEasing { get; }
private List<Action>? _invokeOnNextCommit;
private readonly Stack<List<Action>> _invokeListPool = new();
/// <summary>
/// Creates a new compositor on a specified render loop that would use a particular GPU
/// </summary>
/// <param name="loop"></param>
/// <param name="gpu"></param>
public Compositor(IRenderLoop loop, IPlatformGpu? gpu)
{
Loop = loop;
_server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool);
_implicitBatchCommit = ImplicitBatchCommit;
DefaultEasing = new CubicBezierEasing(new Point(0.25f, 0.1f), new Point(0.25f, 1f));
}
/// <summary>
/// Creates a new CompositionTarget
/// </summary>
/// <param name="renderTargetFactory">A factory method to create IRenderTarget to be called from the render thread</param>
/// <returns></returns>
public CompositionTarget CreateCompositionTarget(Func<IRenderTarget> renderTargetFactory)
{
return new CompositionTarget(this, new ServerCompositionTarget(_server, renderTargetFactory));
}
/// <summary>
/// Requests pending changes in the composition objects to be serialized and sent to the render thread
/// </summary>
/// <returns>A task that completes when sent changes are applied and rendered on the render thread</returns>
public Task RequestCommitAsync()
{
Dispatcher.UIThread.VerifyAccess();
var batch = new Batch();
using (var writer = new BatchStreamWriter(batch.Changes, _batchMemoryPool, _batchObjectPool))
{
foreach (var obj in _objectsForSerialization)
{
writer.WriteObject(obj.Server);
obj.SerializeChanges(writer);
#if DEBUG_COMPOSITOR_SERIALIZATION
writer.Write(BatchStreamDebugMarkers.ObjectEndMagic);
writer.WriteObject(BatchStreamDebugMarkers.ObjectEndMarker);
#endif
}
_objectsForSerialization.Clear();
}
batch.CommitedAt = Server.Clock.Elapsed;
_server.EnqueueBatch(batch);
if (_invokeOnNextCommit != null)
ScheduleCommitCallbacks(batch.Completed);
return batch.Completed;
}
async void ScheduleCommitCallbacks(Task task)
{
var list = _invokeOnNextCommit;
_invokeOnNextCommit = null;
await task;
foreach (var i in list!)
i();
list.Clear();
_invokeListPool.Push(list);
}
public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server));
public ExpressionAnimation CreateExpressionAnimation() => new ExpressionAnimation(this);
public ExpressionAnimation CreateExpressionAnimation(string expression) => new ExpressionAnimation(this)
{
Expression = expression
};
public ImplicitAnimationCollection CreateImplicitAnimationCollection() => new ImplicitAnimationCollection(this);
public CompositionAnimationGroup CreateAnimationGroup() => new CompositionAnimationGroup(this);
private void QueueImplicitBatchCommit()
{
if(_implicitBatchCommitQueued)
return;
_implicitBatchCommitQueued = true;
Dispatcher.UIThread.Post(_implicitBatchCommit, DispatcherPriority.CompositionBatch);
}
private void ImplicitBatchCommit()
{
_implicitBatchCommitQueued = false;
RequestCommitAsync();
}
internal void RegisterForSerialization(CompositionObject compositionObject)
{
Dispatcher.UIThread.VerifyAccess();
_objectsForSerialization.Add(compositionObject);
QueueImplicitBatchCommit();
}
internal void InvokeOnNextCommit(Action action)
{
_invokeOnNextCommit ??= _invokeListPool.Count > 0 ? _invokeListPool.Pop() : new();
_invokeOnNextCommit.Add(action);
}
}
}

24
src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs

@ -0,0 +1,24 @@
using Avalonia.Rendering.Composition.Server;
namespace Avalonia.Rendering.Composition
{
/// <summary>
/// A node in the visual tree that can have children.
/// </summary>
public partial class CompositionContainerVisual : CompositionVisual
{
public CompositionVisualCollection Children { get; private set; } = null!;
partial void InitializeDefaultsExtra()
{
Children = new CompositionVisualCollection(this, Server.Children);
}
private protected override void OnRootChangedCore()
{
foreach (var ch in Children)
ch.Root = Root;
base.OnRootChangedCore();
}
}
}

102
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs

@ -0,0 +1,102 @@
using System;
using Avalonia.Collections.Pooled;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition.Drawing;
/// <summary>
/// A list of serialized drawing commands
/// </summary>
internal class CompositionDrawList : PooledList<IRef<IDrawOperation>>
{
public Size? Size { get; set; }
public CompositionDrawList()
{
}
public CompositionDrawList(int capacity) : base(capacity)
{
}
public override void Dispose()
{
foreach(var item in this)
item.Dispose();
base.Dispose();
}
public CompositionDrawList Clone()
{
var clone = new CompositionDrawList(Count) { Size = Size };
foreach (var r in this)
clone.Add(r.Clone());
return clone;
}
public void Render(CompositorDrawingContextProxy canvas)
{
foreach (var cmd in this)
{
canvas.VisualBrushDrawList = (cmd.Item as BrushDrawOperation)?.Aux as CompositionDrawList;
cmd.Item.Render(canvas);
}
canvas.VisualBrushDrawList = null;
}
}
/// <summary>
/// An helper class for building <see cref="CompositionDrawList"/>
/// </summary>
internal class CompositionDrawListBuilder
{
private CompositionDrawList? _operations;
private bool _owns;
public void Reset(CompositionDrawList? previousOperations)
{
_operations = previousOperations;
_owns = false;
}
public int Count => _operations?.Count ?? 0;
public CompositionDrawList? DrawOperations => _operations;
void MakeWritable(int atIndex)
{
if(_owns)
return;
_owns = true;
var newOps = new CompositionDrawList(_operations?.Count ?? Math.Max(1, atIndex));
if (_operations != null)
{
for (var c = 0; c < atIndex; c++)
newOps.Add(_operations[c].Clone());
}
_operations = newOps;
}
public void ReplaceDrawOperation(int index, IDrawOperation node)
{
MakeWritable(index);
DrawOperations!.Add(RefCountable.Create(node));
}
public void AddDrawOperation(IDrawOperation node)
{
MakeWritable(Count);
DrawOperations!.Add(RefCountable.Create(node));
}
public void TrimTo(int count)
{
if (count < Count)
_operations!.RemoveRange(count, _operations.Count - count);
}
}

391
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs

@ -0,0 +1,391 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Drawing;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Utilities;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.Composition;
/// <summary>
/// An IDrawingContextImpl implementation that builds <see cref="CompositionDrawList"/>
/// </summary>
internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
{
private CompositionDrawListBuilder _builder = new();
private int _drawOperationIndex;
/// <inheritdoc/>
public Matrix Transform { get; set; } = Matrix.Identity;
/// <inheritdoc/>
public void Clear(Color color)
{
// Cannot clear a deferred scene.
}
/// <inheritdoc/>
public void Dispose()
{
// Nothing to do here since we allocate no unmanaged resources.
}
public void BeginUpdate(CompositionDrawList? list)
{
_builder.Reset(list);
_drawOperationIndex = 0;
}
public CompositionDrawList EndUpdate()
{
_builder.TrimTo(_drawOperationIndex);
return _builder.DrawOperations!;
}
/// <inheritdoc/>
public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry)
{
var next = NextDrawAs<GeometryNode>();
if (next == null || !next.Item.Equals(Transform, brush, pen, geometry))
{
Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush)));
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect,
BitmapInterpolationMode bitmapInterpolationMode)
{
var next = NextDrawAs<ImageNode>();
if (next == null ||
!next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode))
{
Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode));
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect)
{
// This method is currently only used to composite layers so shouldn't be called here.
throw new NotSupportedException();
}
/// <inheritdoc/>
public void DrawLine(IPen pen, Point p1, Point p2)
{
var next = NextDrawAs<LineNode>();
if (next == null || !next.Item.Equals(Transform, pen, p1, p2))
{
Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush)));
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect,
BoxShadows boxShadows = default)
{
var next = NextDrawAs<RectangleNode>();
if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows))
{
Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush)));
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect)
{
var next = NextDrawAs<ExperimentalAcrylicNode>();
if (next == null || !next.Item.Equals(Transform, material, rect))
{
Add(new ExperimentalAcrylicNode(Transform, material, rect));
}
else
{
++_drawOperationIndex;
}
}
public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
{
var next = NextDrawAs<EllipseNode>();
if (next == null || !next.Item.Equals(Transform, brush, pen, rect))
{
Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush)));
}
else
{
++_drawOperationIndex;
}
}
public void Custom(ICustomDrawOperation custom)
{
var next = NextDrawAs<CustomDrawOperation>();
if (next == null || !next.Item.Equals(Transform, custom))
Add(new CustomDrawOperation(custom, Transform));
else
++_drawOperationIndex;
}
/// <inheritdoc/>
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
{
var next = NextDrawAs<GlyphRunNode>();
if (next == null || !next.Item.Equals(Transform, foreground, glyphRun))
{
Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground)));
}
else
{
++_drawOperationIndex;
}
}
public IDrawingContextLayerImpl CreateLayer(Size size)
{
throw new NotSupportedException("Creating layers on a deferred drawing context not supported");
}
/// <inheritdoc/>
public void PopClip()
{
var next = NextDrawAs<ClipNode>();
if (next == null || !next.Item.Equals(null))
{
Add(new ClipNode());
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
public void PopGeometryClip()
{
var next = NextDrawAs<GeometryClipNode>();
if (next == null || !next.Item.Equals(null))
{
Add(new GeometryClipNode());
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
public void PopBitmapBlendMode()
{
var next = NextDrawAs<BitmapBlendModeNode>();
if (next == null || !next.Item.Equals(null))
{
Add(new BitmapBlendModeNode());
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
public void PopOpacity()
{
var next = NextDrawAs<OpacityNode>();
if (next == null || !next.Item.Equals(null))
{
Add(new OpacityNode());
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
public void PopOpacityMask()
{
var next = NextDrawAs<OpacityMaskNode>();
if (next == null || !next.Item.Equals(null, null))
{
Add(new OpacityMaskNode());
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
public void PushClip(Rect clip)
{
var next = NextDrawAs<ClipNode>();
if (next == null || !next.Item.Equals(Transform, clip))
{
Add(new ClipNode(Transform, clip));
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc />
public void PushClip(RoundedRect clip)
{
var next = NextDrawAs<ClipNode>();
if (next == null || !next.Item.Equals(Transform, clip))
{
Add(new ClipNode(Transform, clip));
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
public void PushGeometryClip(IGeometryImpl? clip)
{
if (clip is null)
return;
var next = NextDrawAs<GeometryClipNode>();
if (next == null || !next.Item.Equals(Transform, clip))
{
Add(new GeometryClipNode(Transform, clip));
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
public void PushOpacity(double opacity)
{
var next = NextDrawAs<OpacityNode>();
if (next == null || !next.Item.Equals(opacity))
{
Add(new OpacityNode(opacity));
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
public void PushOpacityMask(IBrush mask, Rect bounds)
{
var next = NextDrawAs<OpacityMaskNode>();
if (next == null || !next.Item.Equals(mask, bounds))
{
Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask)));
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
{
var next = NextDrawAs<BitmapBlendModeNode>();
if (next == null || !next.Item.Equals(blendingMode))
{
Add(new BitmapBlendModeNode(blendingMode));
}
else
{
++_drawOperationIndex;
}
}
private void Add<T>(T node) where T : class, IDrawOperation
{
if (_drawOperationIndex < _builder.Count)
{
_builder.ReplaceDrawOperation(_drawOperationIndex, node);
}
else
{
_builder.AddDrawOperation(node);
}
++_drawOperationIndex;
}
private IRef<T>? NextDrawAs<T>() where T : class, IDrawOperation
{
return _drawOperationIndex < _builder.Count
? _builder.DrawOperations![_drawOperationIndex] as IRef<T>
: null;
}
private IDisposable? CreateChildScene(IBrush? brush)
{
if (brush is VisualBrush visualBrush)
{
var visual = visualBrush.Visual;
if (visual != null)
{
// TODO: This is a temporary solution to make visual brush to work like it does with DeferredRenderer
// We should directly reference the corresponding CompositionVisual (which should
// be attached to the same composition target) like UWP does.
// Render-able visuals shouldn't be dangling unattached
(visual as IVisualBrushInitialize)?.EnsureInitialized();
var recorder = new CompositionDrawingContext();
recorder.BeginUpdate(null);
ImmediateRenderer.Render(visual, new DrawingContext(recorder));
var drawList = recorder.EndUpdate();
drawList.Size = visual.Bounds.Size;
return drawList;
}
}
return null;
}
}

14
src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs

@ -0,0 +1,14 @@
namespace Avalonia.Rendering.Composition;
/// <summary>
/// Enables access to composition visual objects that back XAML elements in the XAML composition tree.
/// </summary>
public static class ElementComposition
{
/// <summary>
/// Gets CompositionVisual that backs a Visual
/// </summary>
/// <param name="visual"></param>
/// <returns></returns>
public static CompositionVisual? GetElementVisual(Visual visual) => visual.CompositionVisual;
}

120
src/Avalonia.Base/Rendering/Composition/Enums.cs

@ -0,0 +1,120 @@
using System;
namespace Avalonia.Rendering.Composition
{
public enum CompositionBlendMode
{
/// <summary>No regions are enabled. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_clr.svg)</summary>
Clear,
/// <summary>Only the source will be present. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src.svg)</summary>
Src,
/// <summary>Only the destination will be present. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst.svg)</summary>
Dst,
/// <summary>Source is placed over the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-over.svg)</summary>
SrcOver,
/// <summary>Destination is placed over the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-over.svg)</summary>
DstOver,
/// <summary>The source that overlaps the destination, replaces the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-in.svg)</summary>
SrcIn,
/// <summary>Destination which overlaps the source, replaces the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-in.svg)</summary>
DstIn,
/// <summary>Source is placed, where it falls outside of the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-out.svg)</summary>
SrcOut,
/// <summary>Destination is placed, where it falls outside of the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-out.svg)</summary>
DstOut,
/// <summary>Source which overlaps the destination, replaces the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-atop.svg)</summary>
SrcATop,
/// <summary>Destination which overlaps the source replaces the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-atop.svg)</summary>
DstATop,
/// <summary>The non-overlapping regions of source and destination are combined. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_xor.svg)</summary>
Xor,
/// <summary>Display the sum of the source image and destination image. [Porter Duff Compositing Operators]</summary>
Plus,
/// <summary>Multiplies all components (= alpha and color). [Separable Blend Modes]</summary>
Modulate,
/// <summary>Multiplies the complements of the backdrop and source CompositionColorvalues, then complements the result. [Separable Blend Modes]</summary>
Screen,
/// <summary>Multiplies or screens the colors, depending on the backdrop CompositionColorvalue. [Separable Blend Modes]</summary>
Overlay,
/// <summary>Selects the darker of the backdrop and source colors. [Separable Blend Modes]</summary>
Darken,
/// <summary>Selects the lighter of the backdrop and source colors. [Separable Blend Modes]</summary>
Lighten,
/// <summary>Brightens the backdrop CompositionColorto reflect the source color. [Separable Blend Modes]</summary>
ColorDodge,
/// <summary>Darkens the backdrop CompositionColorto reflect the source color. [Separable Blend Modes]</summary>
ColorBurn,
/// <summary>Multiplies or screens the colors, depending on the source CompositionColorvalue. [Separable Blend Modes]</summary>
HardLight,
/// <summary>Darkens or lightens the colors, depending on the source CompositionColorvalue. [Separable Blend Modes]</summary>
SoftLight,
/// <summary>Subtracts the darker of the two constituent colors from the lighter color. [Separable Blend Modes]</summary>
Difference,
/// <summary>Produces an effect similar to that of the Difference mode but lower in contrast. [Separable Blend Modes]</summary>
Exclusion,
/// <summary>The source CompositionColoris multiplied by the destination CompositionColorand replaces the destination [Separable Blend Modes]</summary>
Multiply,
/// <summary>Creates a CompositionColorwith the hue of the source CompositionColorand the saturation and luminosity of the backdrop color. [Non-Separable Blend Modes]</summary>
Hue,
/// <summary>Creates a CompositionColorwith the saturation of the source CompositionColorand the hue and luminosity of the backdrop color. [Non-Separable Blend Modes]</summary>
Saturation,
/// <summary>Creates a CompositionColorwith the hue and saturation of the source CompositionColorand the luminosity of the backdrop color. [Non-Separable Blend Modes]</summary>
Color,
/// <summary>Creates a CompositionColorwith the luminosity of the source CompositionColorand the hue and saturation of the backdrop color. [Non-Separable Blend Modes]</summary>
Luminosity,
}
public enum CompositionGradientExtendMode
{
Clamp,
Wrap,
Mirror
}
[Flags]
public enum CompositionTileMode
{
None = 0,
TileX = 1,
TileY = 2,
FlipX = 4,
FlipY = 8,
Tile = TileX | TileY,
Flip = FlipX | FlipY
}
public enum CompositionStretch
{
None = 0,
Fill = 1,
//TODO: Uniform, UniformToFill
}
}

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

@ -0,0 +1,237 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Avalonia.Rendering.Composition.Animations;
using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition.Expressions
{
/// <summary>
/// Built-in functions for Foreign Function Interface available from composition animation expressions
/// </summary>
internal class BuiltInExpressionFfi : IExpressionForeignFunctionInterface
{
private readonly DelegateExpressionFfi _registry;
static float Lerp(float a, float b, float p) => p * (b - a) + a;
static Matrix3x2 Inverse(Matrix3x2 m)
{
Matrix3x2.Invert(m, out var r);
return r;
}
static Matrix4x4 Inverse(Matrix4x4 m)
{
Matrix4x4.Invert(m, out var r);
return r;
}
static float SmoothStep(float edge0, float edge1, float x)
{
var t = MathUtilities.Clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f);
return t * t * (3.0f - 2.0f * t);
}
static Vector2 SmoothStep(Vector2 edge0, Vector2 edge1, Vector2 x)
{
return new Vector2(
SmoothStep(edge0.X, edge1.X, x.X),
SmoothStep(edge0.Y, edge1.Y, x.Y)
);
}
static Vector3 SmoothStep(Vector3 edge0, Vector3 edge1, Vector3 x)
{
return new Vector3(
SmoothStep(edge0.X, edge1.X, x.X),
SmoothStep(edge0.Y, edge1.Y, x.Y),
SmoothStep(edge0.Z, edge1.Z, x.Z)
);
}
static Vector4 SmoothStep(Vector4 edge0, Vector4 edge1, Vector4 x)
{
return new Vector4(
SmoothStep(edge0.X, edge1.X, x.X),
SmoothStep(edge0.Y, edge1.Y, x.Y),
SmoothStep(edge0.Z, edge1.Z, x.Z),
SmoothStep(edge0.W, edge1.W, x.W)
);
}
private BuiltInExpressionFfi()
{
_registry = new DelegateExpressionFfi
{
{"Abs", (float f) => Math.Abs(f)},
{"Abs", (Vector2 v) => Vector2.Abs(v)},
{"Abs", (Vector3 v) => Vector3.Abs(v)},
{"Abs", (Vector4 v) => Vector4.Abs(v)},
{"ACos", (float f) => (float) Math.Acos(f)},
{"ASin", (float f) => (float) Math.Asin(f)},
{"ATan", (float f) => (float) Math.Atan(f)},
{"Ceil", (float f) => (float) Math.Ceiling(f)},
{"Clamp", (float a1, float a2, float a3) => MathUtilities.Clamp(a1, a2, a3)},
{"Clamp", (Vector2 a1, Vector2 a2, Vector2 a3) => Vector2.Clamp(a1, a2, a3)},
{"Clamp", (Vector3 a1, Vector3 a2, Vector3 a3) => Vector3.Clamp(a1, a2, a3)},
{"Clamp", (Vector4 a1, Vector4 a2, Vector4 a3) => Vector4.Clamp(a1, a2, a3)},
{"Concatenate", (Quaternion a1, Quaternion a2) => Quaternion.Concatenate(a1, a2)},
{"Cos", (float a) => (float) Math.Cos(a)},
/*
TODO:
ColorHsl(Float h, Float s, Float l)
ColorLerpHSL(Color colorTo, CompositionColorcolorFrom, Float progress)
*/
{
"ColorLerp", (Avalonia.Media.Color to, Avalonia.Media.Color from, float progress) =>
ColorInterpolator.LerpRGB(to, from, progress)
},
{
"ColorLerpRGB", (Avalonia.Media.Color to, Avalonia.Media.Color from, float progress) =>
ColorInterpolator.LerpRGB(to, from, progress)
},
{
"ColorRGB", (float a, float r, float g, float b) => Avalonia.Media.Color.FromArgb(
(byte) MathUtilities.Clamp(a, 0, 255),
(byte) MathUtilities.Clamp(r, 0, 255),
(byte) MathUtilities.Clamp(g, 0, 255),
(byte) MathUtilities.Clamp(b, 0, 255)
)
},
{"Distance", (Vector2 a1, Vector2 a2) => Vector2.Distance(a1, a2)},
{"Distance", (Vector3 a1, Vector3 a2) => Vector3.Distance(a1, a2)},
{"Distance", (Vector4 a1, Vector4 a2) => Vector4.Distance(a1, a2)},
{"DistanceSquared", (Vector2 a1, Vector2 a2) => Vector2.DistanceSquared(a1, a2)},
{"DistanceSquared", (Vector3 a1, Vector3 a2) => Vector3.DistanceSquared(a1, a2)},
{"DistanceSquared", (Vector4 a1, Vector4 a2) => Vector4.DistanceSquared(a1, a2)},
{"Floor", (float v) => (float) Math.Floor(v)},
{"Inverse", (Matrix3x2 v) => Inverse(v)},
{"Inverse", (Matrix4x4 v) => Inverse(v)},
{"Length", (Vector2 a1) => a1.Length()},
{"Length", (Vector3 a1) => a1.Length()},
{"Length", (Vector4 a1) => a1.Length()},
{"Length", (Quaternion a1) => a1.Length()},
{"LengthSquared", (Vector2 a1) => a1.LengthSquared()},
{"LengthSquared", (Vector3 a1) => a1.LengthSquared()},
{"LengthSquared", (Vector4 a1) => a1.LengthSquared()},
{"LengthSquared", (Quaternion a1) => a1.LengthSquared()},
{"Lerp", (float a1, float a2, float a3) => Lerp(a1, a2, a3)},
{"Lerp", (Vector2 a1, Vector2 a2, float a3) => Vector2.Lerp(a1, a2, a3)},
{"Lerp", (Vector3 a1, Vector3 a2, float a3) => Vector3.Lerp(a1, a2, a3)},
{"Lerp", (Vector4 a1, Vector4 a2, float a3) => Vector4.Lerp(a1, a2, a3)},
{"Ln", (float f) => (float) Math.Log(f)},
{"Log10", (float f) => (float) Math.Log10(f)},
{"Matrix3x2.CreateFromScale", (Vector2 v) => Matrix3x2.CreateScale(v)},
{"Matrix3x2.CreateFromTranslation", (Vector2 v) => Matrix3x2.CreateTranslation(v)},
{"Matrix3x2.CreateRotation", (float v) => Matrix3x2.CreateRotation(v)},
{"Matrix3x2.CreateScale", (Vector2 v) => Matrix3x2.CreateScale(v)},
{"Matrix3x2.CreateSkew", (float a1, float a2, Vector2 a3) => Matrix3x2.CreateSkew(a1, a2, a3)},
{"Matrix3x2.CreateTranslation", (Vector2 v) => Matrix3x2.CreateScale(v)},
{
"Matrix3x2", (float m11, float m12, float m21, float m22, float m31, float m32) =>
new Matrix3x2(m11, m12, m21, m22, m31, m32)
},
{"Matrix4x4.CreateFromAxisAngle", (Vector3 v, float angle) => Matrix4x4.CreateFromAxisAngle(v, angle)},
{"Matrix4x4.CreateFromScale", (Vector3 v) => Matrix4x4.CreateScale(v)},
{"Matrix4x4.CreateFromTranslation", (Vector3 v) => Matrix4x4.CreateTranslation(v)},
{"Matrix4x4.CreateScale", (Vector3 v) => Matrix4x4.CreateScale(v)},
{"Matrix4x4.CreateTranslation", (Vector3 v) => Matrix4x4.CreateScale(v)},
{"Matrix4x4", (Matrix3x2 m) => new Matrix4x4(m)},
{
"Matrix4x4",
(float m11, float m12, float m13, float m14,
float m21, float m22, float m23, float m24,
float m31, float m32, float m33, float m34,
float m41, float m42, float m43, float m44) =>
new Matrix4x4(
m11, m12, m13, m14,
m21, m22, m23, m24,
m31, m32, m33, m34,
m41, m42, m43, m44)
},
{"Max", (float a1, float a2) => Math.Max(a1, a2)},
{"Max", (Vector2 a1, Vector2 a2) => Vector2.Max(a1, a2)},
{"Max", (Vector3 a1, Vector3 a2) => Vector3.Max(a1, a2)},
{"Max", (Vector4 a1, Vector4 a2) => Vector4.Max(a1, a2)},
{"Min", (float a1, float a2) => Math.Min(a1, a2)},
{"Min", (Vector2 a1, Vector2 a2) => Vector2.Min(a1, a2)},
{"Min", (Vector3 a1, Vector3 a2) => Vector3.Min(a1, a2)},
{"Min", (Vector4 a1, Vector4 a2) => Vector4.Min(a1, a2)},
{"Mod", (float a, float b) => a % b},
{"Normalize", (Quaternion a) => Quaternion.Normalize(a)},
{"Normalize", (Vector2 a) => Vector2.Normalize(a)},
{"Normalize", (Vector3 a) => Vector3.Normalize(a)},
{"Normalize", (Vector4 a) => Vector4.Normalize(a)},
{"Pow", (float a, float b) => (float) Math.Pow(a, b)},
{"Quaternion.CreateFromAxisAngle", (Vector3 a, float b) => Quaternion.CreateFromAxisAngle(a, b)},
{"Quaternion", (float a, float b, float c, float d) => new Quaternion(a, b, c, d)},
{"Round", (float a) => (float) Math.Round(a)},
{"Scale", (Matrix3x2 a, float b) => a * b},
{"Scale", (Matrix4x4 a, float b) => a * b},
{"Scale", (Vector2 a, float b) => a * b},
{"Scale", (Vector3 a, float b) => a * b},
{"Scale", (Vector4 a, float b) => a * b},
{"Sin", (float a) => (float) Math.Sin(a)},
{"SmoothStep", (float a1, float a2, float a3) => SmoothStep(a1, a2, a3)},
{"SmoothStep", (Vector2 a1, Vector2 a2, Vector2 a3) => SmoothStep(a1, a2, a3)},
{"SmoothStep", (Vector3 a1, Vector3 a2, Vector3 a3) => SmoothStep(a1, a2, a3)},
{"SmoothStep", (Vector4 a1, Vector4 a2, Vector4 a3) => SmoothStep(a1, a2, a3)},
// I have no idea how to do a spherical interpolation for a scalar value, so we are doing a linear one
{"Slerp", (float a1, float a2, float a3) => Lerp(a1, a2, a3)},
{"Slerp", (Quaternion a1, Quaternion a2, float a3) => Quaternion.Slerp(a1, a2, a3)},
{"Sqrt", (float a) => (float) Math.Sqrt(a)},
{"Square", (float a) => a * a},
{"Tan", (float a) => (float) Math.Tan(a)},
{"ToRadians", (float a) => (float) (a * Math.PI / 180)},
{"ToDegrees", (float a) => (float) (a * 180d / Math.PI)},
{"Transform", (Vector2 a, Matrix3x2 b) => Vector2.Transform(a, b)},
{"Transform", (Vector3 a, Matrix4x4 b) => Vector3.Transform(a, b)},
{"Vector2", (float a, float b) => new Vector2(a, b)},
{"Vector3", (float a, float b, float c) => new Vector3(a, b, c)},
{"Vector3", (Vector2 v2, float z) => new Vector3(v2, z)},
{"Vector4", (float a, float b, float c, float d) => new Vector4(a, b, c, d)},
{"Vector4", (Vector2 v2, float z, float w) => new Vector4(v2, z, w)},
{"Vector4", (Vector3 v3, float w) => new Vector4(v3, w)},
};
}
public bool Call(string name, IReadOnlyList<ExpressionVariant> arguments, out ExpressionVariant result) =>
_registry.Call(name, arguments, out result);
public static BuiltInExpressionFfi Instance { get; } = new BuiltInExpressionFfi();
}
}

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

@ -0,0 +1,184 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Avalonia.Media;
namespace Avalonia.Rendering.Composition.Expressions
{
/// <summary>
/// Foreign function interface for composition animations based on calling delegates
/// </summary>
internal class DelegateExpressionFfi : IExpressionForeignFunctionInterface, IEnumerable
{
struct FfiRecord
{
public VariantType[] Types;
public Func<IReadOnlyList<ExpressionVariant>, ExpressionVariant> Delegate;
}
private readonly Dictionary<string, Dictionary<int, List<FfiRecord>>>
_registry = new Dictionary<string, Dictionary<int, List<FfiRecord>>>();
public bool Call(string name, IReadOnlyList<ExpressionVariant> arguments, out ExpressionVariant result)
{
result = default;
if (!_registry.TryGetValue(name, out var nameGroup))
return false;
if (!nameGroup.TryGetValue(arguments.Count, out var countGroup))
return false;
foreach (var record in countGroup)
{
var match = true;
for (var c = 0; c < arguments.Count; c++)
{
if (record.Types[c] != arguments[c].Type)
{
match = false;
break;
}
}
if (match)
{
result = record.Delegate(arguments);
return true;
}
}
return false;
}
// Stub for collection initializer
IEnumerator IEnumerable.GetEnumerator() => Array.Empty<object>().GetEnumerator();
void Add(string name, Func<IReadOnlyList<ExpressionVariant>, ExpressionVariant> cb,
params Type[] types)
{
if (!_registry.TryGetValue(name, out var nameGroup))
_registry[name] = nameGroup =
new Dictionary<int, List<FfiRecord>>();
if (!nameGroup.TryGetValue(types.Length, out var countGroup))
nameGroup[types.Length] = countGroup = new List<FfiRecord>();
countGroup.Add(new FfiRecord
{
Types = types.Select(t => TypeMap[t]).ToArray(),
Delegate = cb
});
}
static readonly Dictionary<Type, VariantType> TypeMap = new Dictionary<Type, VariantType>
{
[typeof(bool)] = VariantType.Boolean,
[typeof(float)] = VariantType.Scalar,
[typeof(Vector2)] = VariantType.Vector2,
[typeof(Vector3)] = VariantType.Vector3,
[typeof(Vector4)] = VariantType.Vector4,
[typeof(Matrix3x2)] = VariantType.Matrix3x2,
[typeof(Matrix4x4)] = VariantType.Matrix4x4,
[typeof(Quaternion)] = VariantType.Quaternion,
[typeof(Color)] = VariantType.Color
};
public void Add<T1>(string name, Func<T1, ExpressionVariant> cb) where T1 : struct
{
Add(name, args => cb(args[0].CastOrDefault<T1>()), typeof(T1));
}
public void Add<T1, T2>(string name, Func<T1, T2, ExpressionVariant> cb) where T1 : struct where T2 : struct
{
Add(name, args => cb(args[0].CastOrDefault<T1>(), args[1].CastOrDefault<T2>()), typeof(T1), typeof(T2));
}
public void Add<T1, T2, T3>(string name, Func<T1, T2, T3, ExpressionVariant> cb)
where T1 : struct where T2 : struct where T3 : struct
{
Add(name, args => cb(args[0].CastOrDefault<T1>(), args[1].CastOrDefault<T2>(), args[2].CastOrDefault<T3>()), typeof(T1), typeof(T2),
typeof(T3));
}
public void Add<T1, T2, T3, T4>(string name, Func<T1, T2, T3, T4, ExpressionVariant> cb)
where T1 : struct where T2 : struct where T3 : struct where T4 : struct
{
Add(name, args => cb(
args[0].CastOrDefault<T1>(),
args[1].CastOrDefault<T2>(),
args[2].CastOrDefault<T3>(),
args[3].CastOrDefault<T4>()),
typeof(T1), typeof(T2), typeof(T3), typeof(T4));
}
public void Add<T1, T2, T3, T4, T5>(string name, Func<T1, T2, T3, T4, T5, ExpressionVariant> cb)
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct
{
Add(name, args => cb(
args[0].CastOrDefault<T1>(),
args[1].CastOrDefault<T2>(),
args[2].CastOrDefault<T3>(),
args[3].CastOrDefault<T4>(),
args[4].CastOrDefault<T5>()),
typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5));
}
public void Add<T1, T2, T3, T4, T5, T6>(string name, Func<T1, T2, T3, T4, T5, T6, ExpressionVariant> cb)
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct where T6 : struct
{
Add(name, args => cb(
args[0].CastOrDefault<T1>(),
args[1].CastOrDefault<T2>(),
args[2].CastOrDefault<T3>(),
args[3].CastOrDefault<T4>(),
args[4].CastOrDefault<T5>(),
args[4].CastOrDefault<T6>()),
typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6));
}
public void Add<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>(string name,
Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, ExpressionVariant> cb)
where T1 : struct
where T2 : struct
where T3 : struct
where T4 : struct
where T5 : struct
where T6 : struct
where T7 : struct
where T8 : struct
where T9 : struct
where T10 : struct
where T11 : struct
where T12 : struct
where T13 : struct
where T14 : struct
where T15 : struct
where T16 : struct
{
Add(name, args => cb(
args[0].CastOrDefault<T1>(),
args[1].CastOrDefault<T2>(),
args[2].CastOrDefault<T3>(),
args[3].CastOrDefault<T4>(),
args[4].CastOrDefault<T5>(),
args[4].CastOrDefault<T6>(),
args[4].CastOrDefault<T7>(),
args[4].CastOrDefault<T8>(),
args[4].CastOrDefault<T9>(),
args[4].CastOrDefault<T10>(),
args[4].CastOrDefault<T11>(),
args[4].CastOrDefault<T12>(),
args[4].CastOrDefault<T13>(),
args[4].CastOrDefault<T14>(),
args[4].CastOrDefault<T15>(),
args[4].CastOrDefault<T16>()
),
typeof(T1), typeof(T2), typeof(T3), typeof(T4),
typeof(T5), typeof(T6), typeof(T7), typeof(T8),
typeof(T9), typeof(T10), typeof(T11), typeof(T12),
typeof(T13), typeof(T14), typeof(T15), typeof(T16)
);
}
}
}

377
src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs

@ -0,0 +1,377 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using Avalonia.Rendering.Composition.Server;
namespace Avalonia.Rendering.Composition.Expressions
{
/// <summary>
/// A parsed composition expression
/// </summary>
internal abstract class Expression
{
public abstract ExpressionType Type { get; }
public static Expression Parse(string expression)
{
return ExpressionParser.Parse(expression.AsSpan());
}
public abstract ExpressionVariant Evaluate(ref ExpressionEvaluationContext context);
public virtual void CollectReferences(HashSet<(string parameter, string property)> references)
{
}
protected abstract string Print();
public override string ToString() => Print();
internal static string OperatorName(ExpressionType t)
{
var attr = typeof(ExpressionType).GetMember(t.ToString())[0]
.GetCustomAttribute<PrettyPrintStringAttribute>();
if (attr != null)
return attr.Name;
return t.ToString();
}
}
internal class PrettyPrintStringAttribute : Attribute
{
public string Name { get; }
public PrettyPrintStringAttribute(string name)
{
Name = name;
}
}
internal enum ExpressionType
{
// Binary operators
[PrettyPrintString("+")]
Add,
[PrettyPrintString("-")]
Subtract,
[PrettyPrintString("/")]
Divide,
[PrettyPrintString("*")]
Multiply,
[PrettyPrintString(">")]
MoreThan,
[PrettyPrintString("<")]
LessThan,
[PrettyPrintString(">=")]
MoreThanOrEqual,
[PrettyPrintString("<=")]
LessThanOrEqual,
[PrettyPrintString("&&")]
LogicalAnd,
[PrettyPrintString("||")]
LogicalOr,
[PrettyPrintString("%")]
Remainder,
[PrettyPrintString("==")]
Equals,
[PrettyPrintString("!=")]
NotEquals,
// Unary operators
[PrettyPrintString("!")]
Not,
[PrettyPrintString("-")]
UnaryMinus,
// The rest
MemberAccess,
Parameter,
FunctionCall,
Keyword,
Constant,
ConditionalExpression
}
internal enum ExpressionKeyword
{
StartingValue,
CurrentValue,
FinalValue,
Target,
Pi,
True,
False
}
internal class ConditionalExpression : Expression
{
public Expression Condition { get; }
public Expression TruePart { get; }
public Expression FalsePart { get; }
public override ExpressionType Type => ExpressionType.ConditionalExpression;
public ConditionalExpression(Expression condition, Expression truePart, Expression falsePart)
{
Condition = condition;
TruePart = truePart;
FalsePart = falsePart;
}
public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context)
{
var cond = Condition.Evaluate(ref context);
if (cond.Type == VariantType.Boolean && cond.Boolean)
return TruePart.Evaluate(ref context);
return FalsePart.Evaluate(ref context);
}
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})";
}
internal class ConstantExpression : Expression
{
public float Constant { get; }
public override ExpressionType Type => ExpressionType.Constant;
public ConstantExpression(float constant)
{
Constant = constant;
}
public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) => Constant;
protected override string Print() => Constant.ToString(CultureInfo.InvariantCulture);
}
internal class FunctionCallExpression : Expression
{
public string Name { get; }
public List<Expression> Parameters { get; }
public override ExpressionType Type => ExpressionType.FunctionCall;
public FunctionCallExpression(string name, List<Expression> parameters)
{
Name = name;
Parameters = parameters;
}
public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context)
{
if (context.ForeignFunctionInterface == null)
return default;
var args = new List<ExpressionVariant>();
foreach (var expr in Parameters)
args.Add(expr.Evaluate(ref context));
if (!context.ForeignFunctionInterface.Call(Name, args, out var res))
return default;
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) + ") )";
}
}
internal class MemberAccessExpression : Expression
{
public override ExpressionType Type => ExpressionType.MemberAccess;
public Expression Target { get; }
public string Member { get; }
public MemberAccessExpression(Expression target, string member)
{
Target = target;
Member = string.Intern(member);
}
public override 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);
}
protected override string Print()
{
return "(" + Target.ToString() + ")." + Member;
}
}
internal class ParameterExpression : Expression
{
public string Name { get; }
public override ExpressionType Type => ExpressionType.Parameter;
public ParameterExpression(string name)
{
Name = name;
}
public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context)
{
return context.Parameters?.GetParameter(Name) ?? default;
}
protected override string Print()
{
return "{" + Name + "}";
}
}
internal class KeywordExpression : Expression
{
public override ExpressionType Type => ExpressionType.Keyword;
public ExpressionKeyword Keyword { get; }
public KeywordExpression(ExpressionKeyword keyword)
{
Keyword = keyword;
}
public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context)
{
if (Keyword == ExpressionKeyword.StartingValue)
return context.StartingValue;
if (Keyword == ExpressionKeyword.CurrentValue)
return context.CurrentValue;
if (Keyword == ExpressionKeyword.FinalValue)
return context.FinalValue;
if (Keyword == ExpressionKeyword.Target)
// should be handled by MemberAccess
return default;
if (Keyword == ExpressionKeyword.True)
return true;
if (Keyword == ExpressionKeyword.False)
return false;
if (Keyword == ExpressionKeyword.Pi)
return (float) Math.PI;
return default;
}
protected override string Print()
{
return "[" + Keyword + "]";
}
}
internal class UnaryExpression : Expression
{
public Expression Parameter { get; }
public override ExpressionType Type { get; }
public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context)
{
if (Type == ExpressionType.Not)
return !Parameter.Evaluate(ref context);
if (Type == ExpressionType.UnaryMinus)
return -Parameter.Evaluate(ref context);
return default;
}
public override void CollectReferences(HashSet<(string parameter, string property)> references)
{
Parameter.CollectReferences(references);
}
protected override string Print()
{
return OperatorName(Type) + Parameter;
}
public UnaryExpression(Expression parameter, ExpressionType type)
{
Parameter = parameter;
Type = type;
}
}
internal class BinaryExpression : Expression
{
public Expression Left { get; }
public Expression Right { get; }
public override ExpressionType Type { get; }
public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context)
{
var left = Left.Evaluate(ref context);
var right = Right.Evaluate(ref context);
if (Type == ExpressionType.Add)
return left + right;
if (Type == ExpressionType.Subtract)
return left - right;
if (Type == ExpressionType.Multiply)
return left * right;
if (Type == ExpressionType.Divide)
return left / right;
if (Type == ExpressionType.Remainder)
return left % right;
if (Type == ExpressionType.MoreThan)
return left > right;
if (Type == ExpressionType.LessThan)
return left < right;
if (Type == ExpressionType.MoreThanOrEqual)
return left > right;
if (Type == ExpressionType.LessThanOrEqual)
return left < right;
if (Type == ExpressionType.LogicalAnd)
return left.And(right);
if (Type == ExpressionType.LogicalOr)
return left.Or(right);
if (Type == ExpressionType.Equals)
return left.EqualsTo(right);
if (Type == ExpressionType.NotEquals)
return left.NotEqualsTo(right);
return default;
}
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 + ")";
}
public BinaryExpression(Expression left, Expression right, ExpressionType type)
{
Left = left;
Right = right;
Type = type;
}
}
}

32
src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs

@ -0,0 +1,32 @@
using System.Collections.Generic;
using Avalonia.Rendering.Composition.Server;
namespace Avalonia.Rendering.Composition.Expressions
{
internal struct ExpressionEvaluationContext
{
public ExpressionVariant StartingValue { get; set; }
public ExpressionVariant CurrentValue { get; set; }
public ExpressionVariant FinalValue { get; set; }
public IExpressionObject Target { get; set; }
public IExpressionParameterCollection Parameters { get; set; }
public IExpressionForeignFunctionInterface ForeignFunctionInterface { get; set; }
}
internal interface IExpressionObject
{
ExpressionVariant GetProperty(string name);
}
internal interface IExpressionParameterCollection
{
public ExpressionVariant GetParameter(string name);
public IExpressionObject GetObjectParameter(string name);
}
internal interface IExpressionForeignFunctionInterface
{
bool Call(string name, IReadOnlyList<ExpressionVariant> arguments, out ExpressionVariant result);
}
}

14
src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs

@ -0,0 +1,14 @@
using System;
namespace Avalonia.Rendering.Composition.Expressions
{
internal class ExpressionParseException : Exception
{
public int Position { get; }
public ExpressionParseException(string message, int position) : base(message)
{
Position = position;
}
}
}

298
src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs

@ -0,0 +1,298 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
// ReSharper disable StringLiteralTypo
namespace Avalonia.Rendering.Composition.Expressions
{
internal class ExpressionParser
{
public static Expression Parse(ReadOnlySpan<char> s)
{
var p = new TokenParser(s);
var parsed = ParseTillTerminator(ref p, "", false, false, out _);
p.SkipWhitespace();
if (p.Length != 0)
throw new ExpressionParseException("Unexpected data ", p.Position);
return parsed;
}
private static ReadOnlySpan<char> Dot => ".".AsSpan();
static bool TryParseAtomic(ref TokenParser parser,
[MaybeNullWhen(returnValue: false)] out Expression expr)
{
// We can parse keywords, parameter names and constants
expr = null;
if (parser.TryParseKeywordLowerCase("this.startingvalue"))
expr = new KeywordExpression(ExpressionKeyword.StartingValue);
else if(parser.TryParseKeywordLowerCase("this.currentvalue"))
expr = new KeywordExpression(ExpressionKeyword.CurrentValue);
else if(parser.TryParseKeywordLowerCase("this.finalvalue"))
expr = new KeywordExpression(ExpressionKeyword.FinalValue);
else if(parser.TryParseKeywordLowerCase("pi"))
expr = new KeywordExpression(ExpressionKeyword.Pi);
else if(parser.TryParseKeywordLowerCase("true"))
expr = new KeywordExpression(ExpressionKeyword.True);
else if(parser.TryParseKeywordLowerCase("false"))
expr = new KeywordExpression(ExpressionKeyword.False);
else if (parser.TryParseKeywordLowerCase("this.target"))
expr = new KeywordExpression(ExpressionKeyword.Target);
if (expr != null)
return true;
if (parser.TryParseIdentifier(out var identifier))
{
expr = new ParameterExpression(identifier.ToString());
return true;
}
if(parser.TryParseFloat(out var scalar))
{
expr = new ConstantExpression(scalar);
return true;
}
return false;
}
static bool TryParseOperator(ref TokenParser parser, out ExpressionType op)
{
op = (ExpressionType) (-1);
if (parser.TryConsume("||"))
op = ExpressionType.LogicalOr;
else if (parser.TryConsume("&&"))
op = ExpressionType.LogicalAnd;
else if (parser.TryConsume(">="))
op = ExpressionType.MoreThanOrEqual;
else if (parser.TryConsume("<="))
op = ExpressionType.LessThanOrEqual;
else if (parser.TryConsume("=="))
op = ExpressionType.Equals;
else if (parser.TryConsume("!="))
op = ExpressionType.NotEquals;
else if (parser.TryConsumeAny("+-/*><%".AsSpan(), out var sop))
{
#pragma warning disable CS8509
op = sop switch
#pragma warning restore CS8509
{
'+' => ExpressionType.Add,
'-' => ExpressionType.Subtract,
'/' => ExpressionType.Divide,
'*' => ExpressionType.Multiply,
'<' => ExpressionType.LessThan,
'>' => ExpressionType.MoreThan,
'%' => ExpressionType.Remainder
};
}
else
return false;
return true;
}
struct ExpressionOperatorGroup
{
private List<Expression> _expressions;
private List<ExpressionType> _operators;
private Expression? _first;
public bool NotEmpty => !Empty;
public bool Empty => _expressions == null && _first == null;
public void AppendFirst(Expression expr)
{
if (NotEmpty)
throw new InvalidOperationException();
_first = expr;
}
public void AppendWithOperator(Expression expr, ExpressionType op)
{
if (_expressions == null)
{
if (_first == null)
throw new InvalidOperationException();
_expressions = new List<Expression>();
_expressions.Add(_first);
_first = null;
_operators = new List<ExpressionType>();
}
_expressions.Add(expr);
_operators.Add(op);
}
// https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/
private static readonly ExpressionType[][] OperatorPrecedenceGroups = new[]
{
// multiplicative
new[] {ExpressionType.Multiply, ExpressionType.Divide, ExpressionType.Remainder},
// additive
new[] {ExpressionType.Add, ExpressionType.Subtract},
// relational
new[] {ExpressionType.MoreThan, ExpressionType.MoreThanOrEqual, ExpressionType.LessThan, ExpressionType.LessThanOrEqual},
// equality
new[] {ExpressionType.Equals, ExpressionType.NotEquals},
// conditional AND
new[] {ExpressionType.LogicalAnd},
// conditional OR
new[]{ ExpressionType.LogicalOr},
};
private static readonly ExpressionType[][] OperatorPrecedenceGroupsReversed =
OperatorPrecedenceGroups.Reverse().ToArray();
// a*b+c [a,b,c] [*,+], call with (0, 2)
// ToExpression(a*b) + ToExpression(c)
// a+b*c -> ToExpression(a) + ToExpression(b*c)
Expression ToExpression(int from, int to)
{
if (to - from == 0)
return _expressions[from];
if (to - from == 1)
return new BinaryExpression(_expressions[from], _expressions[to], _operators[from]);
foreach (var grp in OperatorPrecedenceGroupsReversed)
{
for (var c = from; c < to; c++)
{
var currentOperator = _operators[c];
foreach(var operatorFromGroup in grp)
if (currentOperator == operatorFromGroup)
{
// We are dividing the expression right here
var left = ToExpression(from, c);
var right = ToExpression(c + 1, to);
return new BinaryExpression(left, right, currentOperator);
}
}
}
// We shouldn't ever get here, if we are, there is something wrong in the code
throw new ExpressionParseException("Expression parsing algorithm bug in ToExpression", 0);
}
public Expression ToExpression()
{
if (_expressions == null)
return _first ?? throw new InvalidOperationException();
return ToExpression(0, _expressions.Count - 1);
}
}
static Expression ParseTillTerminator(ref TokenParser parser, string terminatorChars,
bool throwOnTerminator,
bool throwOnEnd,
out char? token)
{
ExpressionOperatorGroup left = default;
token = null;
while (true)
{
if (parser.TryConsumeAny(terminatorChars.AsSpan(), out var consumedToken))
{
if (throwOnTerminator || left.Empty)
throw new ExpressionParseException($"Unexpected '{token}'", parser.Position - 1);
token = consumedToken;
return left.ToExpression();
}
parser.SkipWhitespace();
if (parser.Length == 0)
{
if (throwOnEnd || left.Empty)
throw new ExpressionParseException("Unexpected end of expression", parser.Position);
return left.ToExpression();
}
ExpressionType? op = null;
if (left.NotEmpty)
{
if (parser.TryConsume('?'))
{
var truePart = ParseTillTerminator(ref parser, ":",
false, true, out _);
// pass through the current parsing rules to consume the rest
var falsePart = ParseTillTerminator(ref parser, terminatorChars, throwOnTerminator, throwOnEnd,
out token);
return new ConditionalExpression(left.ToExpression(), truePart, falsePart);
}
// We expect a binary operator here
if (!TryParseOperator(ref parser, out var sop))
throw new ExpressionParseException("Unexpected token", parser.Position);
op = sop;
}
// We expect an expression to be parsed (either due to expecting a binary operator or parsing the first part
var applyNegation = false;
while (parser.TryConsume('!'))
applyNegation = !applyNegation;
var applyUnaryMinus = false;
while (parser.TryConsume('-'))
applyUnaryMinus = !applyUnaryMinus;
Expression? parsed;
if (parser.TryConsume('('))
parsed = ParseTillTerminator(ref parser, ")", false, true, out _);
else if (parser.TryParseCall(out var functionName))
{
var parameterList = new List<Expression>();
while (true)
{
parameterList.Add(ParseTillTerminator(ref parser, ",)", false, true, out var closingToken));
if (closingToken == ')')
break;
if (closingToken != ',')
throw new ExpressionParseException("Unexpected end of the expression", parser.Position);
}
parsed = new FunctionCallExpression(functionName.ToString(), parameterList);
}
else if (TryParseAtomic(ref parser, out parsed))
{
// do nothing
}
else
throw new ExpressionParseException("Unexpected token", parser.Position);
// Parse any following member accesses
while (parser.TryConsume('.'))
{
if(!parser.TryParseIdentifier(out var memberName))
throw new ExpressionParseException("Unexpected token", parser.Position);
parsed = new MemberAccessExpression(parsed, memberName.ToString());
}
// Apply ! operator
if (applyNegation)
parsed = new UnaryExpression(parsed, ExpressionType.Not);
if (applyUnaryMinus)
{
if(parsed is ConstantExpression constexpr)
parsed = new ConstantExpression(-constexpr.Constant);
else parsed = new UnaryExpression(parsed, ExpressionType.UnaryMinus);
}
if (left.Empty)
left.AppendFirst(parsed);
else
left.AppendWithOperator(parsed, op!.Value);
}
}
}
}

57
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<IExpressionObject>
{
private List<IExpressionObject> _list = new();
private HashSet<IExpressionObject> _hashSet = new();
public void Add(IExpressionObject obj, string member)
{
if (_hashSet.Add(obj))
_list.Add(obj);
}
public void Clear()
{
_list.Clear();
_hashSet.Clear();
}
IEnumerator<IExpressionObject> IEnumerable<IExpressionObject>.GetEnumerator()
{
return _list.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)_list).GetEnumerator();
}
public List<IExpressionObject>.Enumerator GetEnumerator() => _list.GetEnumerator();
public struct Pool
{
private Stack<ExpressionTrackedObjects> _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);
}
}
}

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

@ -0,0 +1,730 @@
using System;
using System.Globalization;
using System.Numerics;
using System.Runtime.InteropServices;
using Avalonia.Media;
namespace Avalonia.Rendering.Composition.Expressions
{
internal enum VariantType
{
Invalid,
Boolean,
Scalar,
Double,
Vector2,
Vector3,
Vector4,
AvaloniaMatrix,
Matrix3x2,
Matrix4x4,
Quaternion,
Color
}
/// <summary>
/// A VARIANT type used in expression animations. Can represent multiple value types
/// </summary>
[StructLayout(LayoutKind.Explicit)]
internal struct ExpressionVariant
{
[FieldOffset(0)] public VariantType Type;
[FieldOffset(4)] public bool Boolean;
[FieldOffset(4)] public float Scalar;
[FieldOffset(4)] public double Double;
[FieldOffset(4)] public Vector2 Vector2;
[FieldOffset(4)] public Vector3 Vector3;
[FieldOffset(4)] public Vector4 Vector4;
[FieldOffset(4)] public Matrix AvaloniaMatrix;
[FieldOffset(4)] public Matrix3x2 Matrix3x2;
[FieldOffset(4)] public Matrix4x4 Matrix4x4;
[FieldOffset(4)] public Quaternion Quaternion;
[FieldOffset(4)] public Color Color;
public ExpressionVariant GetProperty(string property)
{
if (Type == VariantType.Vector2)
{
if (ReferenceEquals(property, "X"))
return Vector2.X;
if (ReferenceEquals(property, "Y"))
return Vector2.Y;
return default;
}
if (Type == VariantType.Vector3)
{
if (ReferenceEquals(property, "X"))
return Vector3.X;
if (ReferenceEquals(property, "Y"))
return Vector3.Y;
if (ReferenceEquals(property, "Z"))
return Vector3.Z;
if(ReferenceEquals(property, "XY"))
return new Vector2(Vector3.X, Vector3.Y);
if(ReferenceEquals(property, "YX"))
return new Vector2(Vector3.Y, Vector3.X);
if(ReferenceEquals(property, "XZ"))
return new Vector2(Vector3.X, Vector3.Z);
if(ReferenceEquals(property, "ZX"))
return new Vector2(Vector3.Z, Vector3.X);
if(ReferenceEquals(property, "YZ"))
return new Vector2(Vector3.Y, Vector3.Z);
if(ReferenceEquals(property, "ZY"))
return new Vector2(Vector3.Z, Vector3.Y);
return default;
}
if (Type == VariantType.Vector4)
{
if (ReferenceEquals(property, "X"))
return Vector4.X;
if (ReferenceEquals(property, "Y"))
return Vector4.Y;
if (ReferenceEquals(property, "Z"))
return Vector4.Z;
if (ReferenceEquals(property, "W"))
return Vector4.W;
return default;
}
if (Type == VariantType.Matrix3x2)
{
if (ReferenceEquals(property, "M11"))
return Matrix3x2.M11;
if (ReferenceEquals(property, "M12"))
return Matrix3x2.M12;
if (ReferenceEquals(property, "M21"))
return Matrix3x2.M21;
if (ReferenceEquals(property, "M22"))
return Matrix3x2.M22;
if (ReferenceEquals(property, "M31"))
return Matrix3x2.M31;
if (ReferenceEquals(property, "M32"))
return Matrix3x2.M32;
return default;
}
if (Type == VariantType.AvaloniaMatrix)
{
if (ReferenceEquals(property, "M11"))
return AvaloniaMatrix.M11;
if (ReferenceEquals(property, "M12"))
return AvaloniaMatrix.M12;
if (ReferenceEquals(property, "M21"))
return AvaloniaMatrix.M21;
if (ReferenceEquals(property, "M22"))
return AvaloniaMatrix.M22;
if (ReferenceEquals(property, "M31"))
return AvaloniaMatrix.M31;
if (ReferenceEquals(property, "M32"))
return AvaloniaMatrix.M32;
return default;
}
if (Type == VariantType.Matrix4x4)
{
if (ReferenceEquals(property, "M11"))
return Matrix4x4.M11;
if (ReferenceEquals(property, "M12"))
return Matrix4x4.M12;
if (ReferenceEquals(property, "M13"))
return Matrix4x4.M13;
if (ReferenceEquals(property, "M14"))
return Matrix4x4.M14;
if (ReferenceEquals(property, "M21"))
return Matrix4x4.M21;
if (ReferenceEquals(property, "M22"))
return Matrix4x4.M22;
if (ReferenceEquals(property, "M23"))
return Matrix4x4.M23;
if (ReferenceEquals(property, "M24"))
return Matrix4x4.M24;
if (ReferenceEquals(property, "M31"))
return Matrix4x4.M31;
if (ReferenceEquals(property, "M32"))
return Matrix4x4.M32;
if (ReferenceEquals(property, "M33"))
return Matrix4x4.M33;
if (ReferenceEquals(property, "M34"))
return Matrix4x4.M34;
if (ReferenceEquals(property, "M41"))
return Matrix4x4.M41;
if (ReferenceEquals(property, "M42"))
return Matrix4x4.M42;
if (ReferenceEquals(property, "M43"))
return Matrix4x4.M43;
if (ReferenceEquals(property, "M44"))
return Matrix4x4.M44;
return default;
}
if (Type == VariantType.Quaternion)
{
if (ReferenceEquals(property, "X"))
return Quaternion.X;
if (ReferenceEquals(property, "Y"))
return Quaternion.Y;
if (ReferenceEquals(property, "Z"))
return Quaternion.Z;
if (ReferenceEquals(property, "W"))
return Quaternion.W;
return default;
}
if (Type == VariantType.Color)
{
if (ReferenceEquals(property, "A"))
return Color.A;
if (ReferenceEquals(property, "R"))
return Color.R;
if (ReferenceEquals(property, "G"))
return Color.G;
if (ReferenceEquals(property, "B"))
return Color.B;
return default;
}
return default;
}
public static implicit operator ExpressionVariant(bool value) =>
new ExpressionVariant
{
Type = VariantType.Boolean,
Boolean = value
};
public static implicit operator ExpressionVariant(float scalar) =>
new ExpressionVariant
{
Type = VariantType.Scalar,
Scalar = scalar
};
public static implicit operator ExpressionVariant(double d) =>
new ExpressionVariant
{
Type = VariantType.Double,
Double = d
};
public static implicit operator ExpressionVariant(Vector2 value) =>
new ExpressionVariant
{
Type = VariantType.Vector2,
Vector2 = value
};
public static implicit operator ExpressionVariant(Vector3 value) =>
new ExpressionVariant
{
Type = VariantType.Vector3,
Vector3 = value
};
public static implicit operator ExpressionVariant(Vector4 value) =>
new ExpressionVariant
{
Type = VariantType.Vector4,
Vector4 = value
};
public static implicit operator ExpressionVariant(Matrix3x2 value) =>
new ExpressionVariant
{
Type = VariantType.Matrix3x2,
Matrix3x2 = value
};
public static implicit operator ExpressionVariant(Matrix value) =>
new ExpressionVariant
{
Type = VariantType.Matrix3x2,
AvaloniaMatrix = value
};
public static implicit operator ExpressionVariant(Matrix4x4 value) =>
new ExpressionVariant
{
Type = VariantType.Matrix4x4,
Matrix4x4 = value
};
public static implicit operator ExpressionVariant(Quaternion value) =>
new ExpressionVariant
{
Type = VariantType.Quaternion,
Quaternion = value
};
public static implicit operator ExpressionVariant(Avalonia.Media.Color value) =>
new ExpressionVariant
{
Type = VariantType.Color,
Color = value
};
public static ExpressionVariant operator +(ExpressionVariant left, ExpressionVariant right)
{
if (left.Type != right.Type || left.Type == VariantType.Invalid)
return default;
if (left.Type == VariantType.Scalar)
return left.Scalar + right.Scalar;
if (left.Type == VariantType.Double)
return left.Double + right.Double;
if (left.Type == VariantType.Vector2)
return left.Vector2 + right.Vector2;
if (left.Type == VariantType.Vector3)
return left.Vector3 + right.Vector3;
if (left.Type == VariantType.Vector4)
return left.Vector4 + right.Vector4;
if (left.Type == VariantType.Matrix3x2)
return left.Matrix3x2 + right.Matrix3x2;
if (left.Type == VariantType.Matrix4x4)
return left.Matrix4x4 + right.Matrix4x4;
if (left.Type == VariantType.Quaternion)
return left.Quaternion + right.Quaternion;
return default;
}
public static ExpressionVariant operator -(ExpressionVariant left, ExpressionVariant right)
{
if (left.Type != right.Type || left.Type == VariantType.Invalid)
return default;
if (left.Type == VariantType.Scalar)
return left.Scalar - right.Scalar;
if (left.Type == VariantType.Double)
return left.Double - right.Double;
if (left.Type == VariantType.Vector2)
return left.Vector2 - right.Vector2;
if (left.Type == VariantType.Vector3)
return left.Vector3 - right.Vector3;
if (left.Type == VariantType.Vector4)
return left.Vector4 - right.Vector4;
if (left.Type == VariantType.Matrix3x2)
return left.Matrix3x2 - right.Matrix3x2;
if (left.Type == VariantType.Matrix4x4)
return left.Matrix4x4 - right.Matrix4x4;
if (left.Type == VariantType.Quaternion)
return left.Quaternion - right.Quaternion;
return default;
}
public static ExpressionVariant operator -(ExpressionVariant left)
{
if (left.Type == VariantType.Scalar)
return -left.Scalar;
if (left.Type == VariantType.Double)
return -left.Double;
if (left.Type == VariantType.Vector2)
return -left.Vector2;
if (left.Type == VariantType.Vector3)
return -left.Vector3;
if (left.Type == VariantType.Vector4)
return -left.Vector4;
if (left.Type == VariantType.Matrix3x2)
return -left.Matrix3x2;
if (left.Type == VariantType.AvaloniaMatrix)
return -left.AvaloniaMatrix;
if (left.Type == VariantType.Matrix4x4)
return -left.Matrix4x4;
if (left.Type == VariantType.Quaternion)
return -left.Quaternion;
return default;
}
public static ExpressionVariant operator *(ExpressionVariant left, ExpressionVariant right)
{
if (left.Type == VariantType.Invalid || right.Type == VariantType.Invalid)
return default;
if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar)
return left.Scalar * right.Scalar;
if (left.Type == VariantType.Double && right.Type == VariantType.Double)
return left.Double * right.Double;
if (left.Type == VariantType.Vector2 && right.Type == VariantType.Vector2)
return left.Vector2 * right.Vector2;
if (left.Type == VariantType.Vector2 && right.Type == VariantType.Scalar)
return left.Vector2 * right.Scalar;
if (left.Type == VariantType.Vector3 && right.Type == VariantType.Vector3)
return left.Vector3 * right.Vector3;
if (left.Type == VariantType.Vector3 && right.Type == VariantType.Scalar)
return left.Vector3 * right.Scalar;
if (left.Type == VariantType.Vector4 && right.Type == VariantType.Vector4)
return left.Vector4 * right.Vector4;
if (left.Type == VariantType.Vector4 && right.Type == VariantType.Scalar)
return left.Vector4 * right.Scalar;
if (left.Type == VariantType.Matrix3x2 && right.Type == VariantType.Matrix3x2)
return left.Matrix3x2 * right.Matrix3x2;
if (left.Type == VariantType.Matrix3x2 && right.Type == VariantType.Scalar)
return left.Matrix3x2 * right.Scalar;
if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.AvaloniaMatrix)
return left.AvaloniaMatrix * right.AvaloniaMatrix;
if (left.Type == VariantType.Matrix4x4 && right.Type == VariantType.Matrix4x4)
return left.Matrix4x4 * right.Matrix4x4;
if (left.Type == VariantType.Matrix4x4 && right.Type == VariantType.Scalar)
return left.Matrix4x4 * right.Scalar;
if (left.Type == VariantType.Quaternion && right.Type == VariantType.Quaternion)
return left.Quaternion * right.Quaternion;
if (left.Type == VariantType.Quaternion && right.Type == VariantType.Scalar)
return left.Quaternion * right.Scalar;
return default;
}
public static ExpressionVariant operator /(ExpressionVariant left, ExpressionVariant right)
{
if (left.Type == VariantType.Invalid || right.Type == VariantType.Invalid)
return default;
if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar)
return left.Scalar / right.Scalar;
if (left.Type == VariantType.Double && right.Type == VariantType.Double)
return left.Double / right.Double;
if (left.Type == VariantType.Vector2 && right.Type == VariantType.Vector2)
return left.Vector2 / right.Vector2;
if (left.Type == VariantType.Vector2 && right.Type == VariantType.Scalar)
return left.Vector2 / right.Scalar;
if (left.Type == VariantType.Vector3 && right.Type == VariantType.Vector3)
return left.Vector3 / right.Vector3;
if (left.Type == VariantType.Vector3 && right.Type == VariantType.Scalar)
return left.Vector3 / right.Scalar;
if (left.Type == VariantType.Vector4 && right.Type == VariantType.Vector4)
return left.Vector4 / right.Vector4;
if (left.Type == VariantType.Vector4 && right.Type == VariantType.Scalar)
return left.Vector4 / right.Scalar;
if (left.Type == VariantType.Quaternion && right.Type == VariantType.Quaternion)
return left.Quaternion / right.Quaternion;
return default;
}
public ExpressionVariant EqualsTo(ExpressionVariant right)
{
if (Type != right.Type || Type == VariantType.Invalid)
return default;
if (Type == VariantType.Scalar)
return Scalar == right.Scalar;
if (Type == VariantType.Double)
return Double == right.Double;
if (Type == VariantType.Vector2)
return Vector2 == right.Vector2;
if (Type == VariantType.Vector3)
return Vector3 == right.Vector3;
if (Type == VariantType.Vector4)
return Vector4 == right.Vector4;
if (Type == VariantType.Boolean)
return Boolean == right.Boolean;
if (Type == VariantType.Matrix3x2)
return Matrix3x2 == right.Matrix3x2;
if (Type == VariantType.AvaloniaMatrix)
return AvaloniaMatrix == right.AvaloniaMatrix;
if (Type == VariantType.Matrix4x4)
return Matrix4x4 == right.Matrix4x4;
if (Type == VariantType.Quaternion)
return Quaternion == right.Quaternion;
return default;
}
public ExpressionVariant NotEqualsTo(ExpressionVariant right)
{
var r = EqualsTo(right);
if (r.Type == VariantType.Boolean)
return !r.Boolean;
return default;
}
public static ExpressionVariant operator !(ExpressionVariant v)
{
if (v.Type == VariantType.Boolean)
return !v.Boolean;
return default;
}
public static ExpressionVariant operator %(ExpressionVariant left, ExpressionVariant right)
{
if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar)
return left.Scalar % right.Scalar;
if (left.Type == VariantType.Double && right.Type == VariantType.Double)
return left.Double % right.Double;
return default;
}
public static ExpressionVariant operator <(ExpressionVariant left, ExpressionVariant right)
{
if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar)
return left.Scalar < right.Scalar;
if (left.Type == VariantType.Double && right.Type == VariantType.Double)
return left.Double < right.Double;
return default;
}
public static ExpressionVariant operator >(ExpressionVariant left, ExpressionVariant right)
{
if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar)
return left.Scalar > right.Scalar;
if (left.Type == VariantType.Double && right.Type == VariantType.Double)
return left.Double > right.Double;
return default;
}
public ExpressionVariant And(ExpressionVariant right)
{
if (Type == VariantType.Boolean && right.Type == VariantType.Boolean)
return Boolean && right.Boolean;
return default;
}
public ExpressionVariant Or(ExpressionVariant right)
{
if (Type == VariantType.Boolean && right.Type == VariantType.Boolean)
return Boolean && right.Boolean;
return default;
}
public bool TryCast<T>(out T res) where T : struct
{
if (typeof(T) == typeof(bool))
{
if (Type == VariantType.Boolean)
{
res = (T) (object) Boolean;
return true;
}
}
if (typeof(T) == typeof(float))
{
if (Type == VariantType.Scalar)
{
res = (T) (object) Scalar;
return true;
}
}
if (typeof(T) == typeof(double))
{
if (Type == VariantType.Double)
{
res = (T) (object) Double;
return true;
}
}
if (typeof(T) == typeof(Vector2))
{
if (Type == VariantType.Vector2)
{
res = (T) (object) Vector2;
return true;
}
}
if (typeof(T) == typeof(Vector3))
{
if (Type == VariantType.Vector3)
{
res = (T) (object) Vector3;
return true;
}
}
if (typeof(T) == typeof(Vector4))
{
if (Type == VariantType.Vector4)
{
res = (T) (object) Vector4;
return true;
}
}
if (typeof(T) == typeof(Matrix3x2))
{
if (Type == VariantType.Matrix3x2)
{
res = (T) (object) Matrix3x2;
return true;
}
}
if (typeof(T) == typeof(Matrix))
{
if (Type == VariantType.AvaloniaMatrix)
{
res = (T) (object) Matrix3x2;
return true;
}
}
if (typeof(T) == typeof(Matrix4x4))
{
if (Type == VariantType.Matrix4x4)
{
res = (T) (object) Matrix4x4;
return true;
}
}
if (typeof(T) == typeof(Quaternion))
{
if (Type == VariantType.Quaternion)
{
res = (T) (object) Quaternion;
return true;
}
}
if (typeof(T) == typeof(Avalonia.Media.Color))
{
if (Type == VariantType.Color)
{
res = (T) (object) Color;
return true;
}
}
res = default(T);
return false;
}
public static ExpressionVariant Create<T>(T v) where T : struct
{
if (typeof(T) == typeof(bool))
return (bool) (object) v;
if (typeof(T) == typeof(float))
return (float) (object) v;
if (typeof(T) == typeof(Vector2))
return (Vector2) (object) v;
if (typeof(T) == typeof(Vector3))
return (Vector3) (object) v;
if (typeof(T) == typeof(Vector4))
return (Vector4) (object) v;
if (typeof(T) == typeof(Matrix3x2))
return (Matrix3x2) (object) v;
if (typeof(T) == typeof(Matrix))
return (Matrix) (object) v;
if (typeof(T) == typeof(Matrix4x4))
return (Matrix4x4) (object) v;
if (typeof(T) == typeof(Quaternion))
return (Quaternion) (object) v;
if (typeof(T) == typeof(Avalonia.Media.Color))
return (Avalonia.Media.Color) (object) v;
throw new ArgumentException("Invalid variant type: " + typeof(T));
}
public T CastOrDefault<T>() where T : struct
{
TryCast<T>(out var r);
return r;
}
public override string ToString()
{
if (Type == VariantType.Boolean)
return Boolean.ToString();
if (Type == VariantType.Scalar)
return Scalar.ToString(CultureInfo.InvariantCulture);
if (Type == VariantType.Double)
return Double.ToString(CultureInfo.InvariantCulture);
if (Type == VariantType.Vector2)
return Vector2.ToString();
if (Type == VariantType.Vector3)
return Vector3.ToString();
if (Type == VariantType.Vector4)
return Vector4.ToString();
if (Type == VariantType.Quaternion)
return Quaternion.ToString();
if (Type == VariantType.Matrix3x2)
return Matrix3x2.ToString();
if (Type == VariantType.AvaloniaMatrix)
return AvaloniaMatrix.ToString();
if (Type == VariantType.Matrix4x4)
return Matrix4x4.ToString();
if (Type == VariantType.Color)
return Color.ToString();
if (Type == VariantType.Invalid)
return "Invalid";
return "Unknown";
}
}
}

259
src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs

@ -0,0 +1,259 @@
using System;
using System.Globalization;
namespace Avalonia.Rendering.Composition.Expressions
{
/// <summary>
/// Helper class for composition expression parser
/// </summary>
internal ref struct TokenParser
{
private ReadOnlySpan<char> _s;
public int Position { get; private set; }
public TokenParser(ReadOnlySpan<char> s)
{
_s = s;
Position = 0;
}
public void SkipWhitespace()
{
while (true)
{
if (_s.Length > 0 && char.IsWhiteSpace(_s[0]))
Advance(1);
else
return;
}
}
static bool IsAlphaNumeric(char ch) => (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z');
public bool TryConsume(char c)
{
SkipWhitespace();
if (_s.Length == 0 || _s[0] != c)
return false;
Advance(1);
return true;
}
public bool TryConsume(string s)
{
SkipWhitespace();
if (_s.Length < s.Length)
return false;
for (var c = 0; c < s.Length; c++)
{
if (_s[c] != s[c])
return false;
}
Advance(s.Length);
return true;
}
public bool TryConsumeAny(ReadOnlySpan<char> chars, out char token)
{
SkipWhitespace();
token = default;
if (_s.Length == 0)
return false;
foreach (var c in chars)
{
if (c == _s[0])
{
token = c;
Advance(1);
return true;
}
}
return false;
}
public bool TryParseKeyword(string keyword)
{
SkipWhitespace();
if (keyword.Length > _s.Length)
return false;
for(var c=0; c<keyword.Length;c++)
if (keyword[c] != _s[c])
return false;
if (_s.Length > keyword.Length && IsAlphaNumeric(_s[keyword.Length]))
return false;
Advance(keyword.Length);
return true;
}
public bool TryParseKeywordLowerCase(string keywordInLowerCase)
{
SkipWhitespace();
if (keywordInLowerCase.Length > _s.Length)
return false;
for(var c=0; c<keywordInLowerCase.Length;c++)
if (keywordInLowerCase[c] != char.ToLowerInvariant(_s[c]))
return false;
if (_s.Length > keywordInLowerCase.Length && IsAlphaNumeric(_s[keywordInLowerCase.Length]))
return false;
Advance(keywordInLowerCase.Length);
return true;
}
public void Advance(int c)
{
_s = _s.Slice(c);
Position += c;
}
public int Length => _s.Length;
public bool TryParseIdentifier(ReadOnlySpan<char> extraValidChars, out ReadOnlySpan<char> res)
{
res = ReadOnlySpan<char>.Empty;
SkipWhitespace();
if (_s.Length == 0)
return false;
var first = _s[0];
if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z')))
return false;
int len = 1;
for (var c = 1; c < _s.Length; c++)
{
var ch = _s[c];
if (IsAlphaNumeric(ch))
len++;
else
{
var found = false;
foreach(var vc in extraValidChars)
if (vc == ch)
{
found = true;
break;
}
if (found)
len++;
else
break;
}
}
res = _s.Slice(0, len);
Advance(len);
return true;
}
public bool TryParseIdentifier(out ReadOnlySpan<char> res)
{
res = ReadOnlySpan<char>.Empty;
SkipWhitespace();
if (_s.Length == 0)
return false;
var first = _s[0];
if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z')))
return false;
int len = 1;
for (var c = 1; c < _s.Length; c++)
{
var ch = _s[c];
if (IsAlphaNumeric(ch))
len++;
else
break;
}
res = _s.Slice(0, len);
Advance(len);
return true;
}
public bool TryParseCall(out ReadOnlySpan<char> res)
{
res = ReadOnlySpan<char>.Empty;
SkipWhitespace();
if (_s.Length == 0)
return false;
var first = _s[0];
if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z')))
return false;
int len = 1;
for (var c = 1; c < _s.Length; c++)
{
var ch = _s[c];
if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch<= 'Z') || ch == '.')
len++;
else
break;
}
res = _s.Slice(0, len);
// Find '('
for (var c = len; c < _s.Length; c++)
{
if(char.IsWhiteSpace(_s[c]))
continue;
if(_s[c]=='(')
{
Advance(c + 1);
return true;
}
return false;
}
return false;
}
public bool TryParseFloat(out float res)
{
res = 0;
SkipWhitespace();
if (_s.Length == 0)
return false;
var len = 0;
var dotCount = 0;
for (var c = 0; c < _s.Length; c++)
{
var ch = _s[c];
if (ch >= '0' && ch <= '9')
len = c + 1;
else if (ch == '.' && dotCount == 0)
{
len = c + 1;
dotCount++;
}
else
break;
}
var span = _s.Slice(0, len);
#if NETSTANDARD2_0
if (!float.TryParse(span.ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, out res))
return false;
#else
if (!float.TryParse(span, NumberStyles.Number, CultureInfo.InvariantCulture, out res))
return false;
#endif
Advance(len);
return true;
}
public override string ToString() => _s.ToString();
}
}

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

@ -0,0 +1,66 @@
using System.Numerics;
namespace Avalonia.Rendering.Composition
{
static class MatrixUtils
{
public static Matrix4x4 ComputeTransform(Vector2 size, Vector2 anchorPoint, Vector3 centerPoint,
Matrix4x4 transformMatrix, Vector3 scale, float rotationAngle, Quaternion orientation, Vector3 offset)
{
// The math here follows the *observed* UWP behavior since there are no docs on how it's supposed to work
var anchor = size * anchorPoint;
var mat = Matrix4x4.CreateTranslation(-anchor.X, -anchor.Y, 0);
var center = new Vector3(centerPoint.X, centerPoint.Y, centerPoint.Z);
if (!transformMatrix.IsIdentity)
mat = transformMatrix * mat;
if (scale != new Vector3(1, 1, 1))
mat *= Matrix4x4.CreateScale(scale, center);
//TODO: RotationAxis support
if (rotationAngle != 0)
mat *= Matrix4x4.CreateRotationZ(rotationAngle, center);
if (orientation != Quaternion.Identity)
{
if (centerPoint != default)
{
mat *= Matrix4x4.CreateTranslation(-center)
* Matrix4x4.CreateFromQuaternion(orientation)
* Matrix4x4.CreateTranslation(center);
}
else
mat *= Matrix4x4.CreateFromQuaternion(orientation);
}
if (offset != default)
mat *= Matrix4x4.CreateTranslation(offset);
return mat;
}
public static Matrix4x4 ToMatrix4x4(Matrix matrix) =>
new Matrix4x4(
(float)matrix.M11, (float)matrix.M12, 0, (float)matrix.M13,
(float)matrix.M21, (float)matrix.M22, 0, (float)matrix.M23,
0, 0, 1, 0,
(float)matrix.M31, (float)matrix.M32, 0, (float)matrix.M33
);
public static Matrix ToMatrix(Matrix4x4 matrix44) =>
new Matrix(
matrix44.M11,
matrix44.M12,
matrix44.M14,
matrix44.M21,
matrix44.M22,
matrix44.M24,
matrix44.M41,
matrix44.M42,
matrix44.M44);
}
}

15
src/Avalonia.Base/Rendering/Composition/Server/CompositionProperty.cs

@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.Threading;
namespace Avalonia.Rendering.Composition.Server;
internal class CompositionProperty
{
private static volatile int s_NextId = 1;
public int Id { get; private set; }
public static CompositionProperty Register() => new()
{
Id = Interlocked.Increment(ref s_NextId)
};
}

179
src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs

@ -0,0 +1,179 @@
using System.Numerics;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Drawing;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition.Server;
/// <summary>
/// A bunch of hacks to make the existing rendering operations and IDrawingContext
/// to work with composition rendering infrastructure.
/// 1) Keeps and applies the transform of the current visual since drawing operations think that
/// they have information about the full render transform (they are not)
/// 2) Keeps the draw list for the VisualBrush contents of the current drawing operation.
/// </summary>
internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
{
private IDrawingContextImpl _impl;
private readonly VisualBrushRenderer _visualBrushRenderer;
public CompositorDrawingContextProxy(IDrawingContextImpl impl, VisualBrushRenderer visualBrushRenderer)
{
_impl = impl;
_visualBrushRenderer = visualBrushRenderer;
}
// This is a hack to make it work with the current way of handling visual brushes
public CompositionDrawList? VisualBrushDrawList
{
get => _visualBrushRenderer.VisualBrushDrawList;
set => _visualBrushRenderer.VisualBrushDrawList = value;
}
public Matrix PostTransform { get; set; } = Matrix.Identity;
public void Dispose()
{
_impl.Dispose();
}
Matrix _transform;
public Matrix Transform
{
get => _transform;
set => _impl.Transform = (_transform = value) * PostTransform;
}
public void Clear(Color color)
{
_impl.Clear(color);
}
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect,
BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
{
_impl.DrawBitmap(source, opacity, sourceRect, destRect, bitmapInterpolationMode);
}
public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
{
_impl.DrawBitmap(source, opacityMask, opacityMaskRect, destRect);
}
public void DrawLine(IPen pen, Point p1, Point p2)
{
_impl.DrawLine(pen, p1, p2);
}
public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry)
{
_impl.DrawGeometry(brush, pen, geometry);
}
public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default)
{
_impl.DrawRectangle(brush, pen, rect, boxShadows);
}
public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
{
_impl.DrawEllipse(brush, pen, rect);
}
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
{
_impl.DrawGlyphRun(foreground, glyphRun);
}
public IDrawingContextLayerImpl CreateLayer(Size size)
{
return _impl.CreateLayer(size);
}
public void PushClip(Rect clip)
{
_impl.PushClip(clip);
}
public void PushClip(RoundedRect clip)
{
_impl.PushClip(clip);
}
public void PopClip()
{
_impl.PopClip();
}
public void PushOpacity(double opacity)
{
_impl.PushOpacity(opacity);
}
public void PopOpacity()
{
_impl.PopOpacity();
}
public void PushOpacityMask(IBrush mask, Rect bounds)
{
_impl.PushOpacityMask(mask, bounds);
}
public void PopOpacityMask()
{
_impl.PopOpacityMask();
}
public void PushGeometryClip(IGeometryImpl clip)
{
_impl.PushGeometryClip(clip);
}
public void PopGeometryClip()
{
_impl.PopGeometryClip();
}
public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
{
_impl.PushBitmapBlendMode(blendingMode);
}
public void PopBitmapBlendMode()
{
_impl.PopBitmapBlendMode();
}
public void Custom(ICustomDrawOperation custom)
{
_impl.Custom(custom);
}
public class VisualBrushRenderer : IVisualBrushRenderer
{
public CompositionDrawList? VisualBrushDrawList { get; set; }
public Size GetRenderTargetSize(IVisualBrush brush)
{
return VisualBrushDrawList?.Size ?? Size.Empty;
}
public void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush)
{
if (VisualBrushDrawList != null)
{
foreach (var cmd in VisualBrushDrawList)
cmd.Item.Render(context);
}
}
}
public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect)
{
if (_impl is IDrawingContextWithAcrylicLikeSupport acrylic)
acrylic.DrawRectangle(material, rect);
}
}

76
src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs

@ -0,0 +1,76 @@
using System;
using System.Diagnostics;
using System.Globalization;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition.Server;
/// <summary>
/// An FPS counter helper that can draw itself on the render thread
/// </summary>
internal class FpsCounter
{
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private int _framesThisSecond;
private int _totalFrames;
private int _fps;
private TimeSpan _lastFpsUpdate;
const int FirstChar = 32;
const int LastChar = 126;
// ASCII chars
private GlyphRun[] _runs = new GlyphRun[LastChar - FirstChar + 1];
public FpsCounter(GlyphTypeface typeface)
{
for (var c = FirstChar; c <= LastChar; c++)
{
var s = new string((char)c, 1);
var glyph = typeface.GetGlyph((uint)(s[0]));
_runs[c - FirstChar] = new GlyphRun(typeface, 18, new ReadOnlySlice<char>(s.AsMemory()), new ushort[] { glyph });
}
}
public void FpsTick() => _framesThisSecond++;
public void RenderFps(IDrawingContextImpl context, string aux)
{
var now = _stopwatch.Elapsed;
var elapsed = now - _lastFpsUpdate;
++_framesThisSecond;
++_totalFrames;
if (elapsed.TotalSeconds > 1)
{
_fps = (int)(_framesThisSecond / elapsed.TotalSeconds);
_framesThisSecond = 0;
_lastFpsUpdate = now;
}
var fpsLine = $"Frame #{_totalFrames:00000000} FPS: {_fps:000} " + aux;
double width = 0;
double height = 0;
foreach (var ch in fpsLine)
{
var run = _runs[ch - FirstChar];
width += run.Size.Width;
height = Math.Max(height, run.Size.Height);
}
var rect = new Rect(0, 0, width + 3, height + 3);
context.DrawRectangle(Brushes.Black, null, rect);
double offset = 0;
foreach (var ch in fpsLine)
{
var run = _runs[ch - FirstChar];
context.Transform = Matrix.CreateTranslation(offset, 0);
context.DrawGlyphRun(Brushes.White, run);
offset += run.Size.Width;
}
}
}

46
src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs

@ -0,0 +1,46 @@
namespace Avalonia.Rendering.Composition.Server
{
/// <summary>
/// A helper class used to manage the current slots for writing data from the render thread
/// and reading it from the UI thread.
/// Used mostly by hit-testing which needs to know the last transform of the visual
/// </summary>
internal class ReadbackIndices
{
private readonly object _lock = new object();
public int ReadIndex { get; private set; } = 0;
public int WriteIndex { get; private set; } = 1;
public int WrittenIndex { get; private set; } = 0;
public ulong ReadRevision { get; private set; }
public ulong LastWrittenRevision { get; private set; }
public void NextRead()
{
lock (_lock)
{
if (ReadRevision < LastWrittenRevision)
{
ReadIndex = WrittenIndex;
ReadRevision = LastWrittenRevision;
}
}
}
public void CompleteWrite(ulong writtenRevision)
{
lock (_lock)
{
for (var c = 0; c < 3; c++)
{
if (c != WriteIndex && c != ReadIndex)
{
WrittenIndex = WriteIndex;
LastWrittenRevision = writtenRevision;
WriteIndex = c;
return;
}
}
}
}
}
}

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

@ -0,0 +1,44 @@
using System.Numerics;
using Avalonia.Platform;
namespace Avalonia.Rendering.Composition.Server
{
/// <summary>
/// Server-side counterpart of <see cref="CompositionContainerVisual"/>.
/// Mostly propagates update and render calls, but is also responsible
/// for updating adorners in deferred manner
/// </summary>
internal partial class ServerCompositionContainerVisual : ServerCompositionVisual
{
public ServerCompositionVisualCollection Children { get; private set; } = null!;
protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
{
base.RenderCore(canvas, currentTransformedClip);
foreach (var ch in Children)
{
ch.Render(canvas, currentTransformedClip);
}
}
public override void Update(ServerCompositionTarget root)
{
base.Update(root);
foreach (var child in Children)
{
if (child.AdornedVisual != null)
root.EnqueueAdornerUpdate(child);
else
child.Update(root);
}
IsDirtyComposition = false;
}
partial void Initialize()
{
Children = new ServerCompositionVisualCollection(Compositor);
}
}
}

75
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs

@ -0,0 +1,75 @@
using System;
using System.Numerics;
using Avalonia.Collections.Pooled;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Drawing;
using Avalonia.Rendering.Composition.Transport;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition.Server;
/// <summary>
/// Server-side counterpart of <see cref="CompositionDrawListVisual"/>
/// </summary>
internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisual
{
#if DEBUG
// This is needed for debugging purposes so we could see inspect the associated visual from debugger
public readonly Visual UiVisual;
#endif
private CompositionDrawList? _renderCommands;
public ServerCompositionDrawListVisual(ServerCompositor compositor, Visual v) : base(compositor)
{
#if DEBUG
UiVisual = v;
#endif
}
Rect? _contentBounds;
public override Rect OwnContentBounds
{
get
{
if (_contentBounds == null)
{
var rect = Rect.Empty;
if(_renderCommands!=null)
foreach (var cmd in _renderCommands)
rect = rect.Union(cmd.Item.Bounds);
_contentBounds = rect;
}
return _contentBounds.Value;
}
}
protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt)
{
if (reader.Read<byte>() == 1)
{
_renderCommands?.Dispose();
_renderCommands = reader.ReadObject<CompositionDrawList?>();
_contentBounds = null;
}
base.DeserializeChangesCore(reader, commitedAt);
}
protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
{
if (_renderCommands != null)
{
_renderCommands.Render(canvas);
}
base.RenderCore(canvas, currentTransformedClip);
}
#if DEBUG
public override string ToString()
{
return UiVisual.GetType().ToString();
}
#endif
}

9
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs

@ -0,0 +1,9 @@
namespace Avalonia.Rendering.Composition.Server
{
internal abstract class ServerCompositionSurface : ServerObject
{
protected ServerCompositionSurface(ServerCompositor compositor) : base(compositor)
{
}
}
}

220
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@ -0,0 +1,220 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Threading;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Transport;
using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition.Server
{
/// <summary>
/// Server-side counterpart of the <see cref="CompositionTarget"/>
/// That's the place where we update visual transforms, track dirty rects and actually do rendering
/// </summary>
internal partial class ServerCompositionTarget : IDisposable
{
private readonly ServerCompositor _compositor;
private readonly Func<IRenderTarget> _renderTargetFactory;
private static long s_nextId = 1;
public long Id { get; }
public ulong Revision { get; private set; }
private IRenderTarget? _renderTarget;
private FpsCounter _fpsCounter = new FpsCounter(Typeface.Default.GlyphTypeface);
private Rect _dirtyRect;
private Random _random = new();
private Size _layerSize;
private IDrawingContextLayerImpl? _layer;
private bool _redrawRequested;
private bool _disposed;
private HashSet<ServerCompositionVisual> _attachedVisuals = new();
private Queue<ServerCompositionVisual> _adornerUpdateQueue = new();
public ReadbackIndices Readback { get; } = new();
public int RenderedVisuals { get; set; }
public ServerCompositionTarget(ServerCompositor compositor, Func<IRenderTarget> renderTargetFactory) :
base(compositor)
{
_compositor = compositor;
_renderTargetFactory = renderTargetFactory;
Id = Interlocked.Increment(ref s_nextId);
}
partial void OnIsEnabledChanged()
{
if (IsEnabled)
{
_compositor.AddCompositionTarget(this);
foreach (var v in _attachedVisuals)
v.Activate();
}
else
{
_compositor.RemoveCompositionTarget(this);
foreach (var v in _attachedVisuals)
v.Deactivate();
}
}
partial void DeserializeChangesExtra(BatchStreamReader c)
{
_redrawRequested = true;
}
public void Render()
{
if (_disposed)
{
Compositor.RemoveCompositionTarget(this);
return;
}
if (Root == null)
return;
_renderTarget ??= _renderTargetFactory();
Compositor.UpdateServerTime();
if(_dirtyRect.IsEmpty && !_redrawRequested)
return;
Revision++;
// Update happens in a separate phase to extend dirty rect if needed
Root.Update(this);
while (_adornerUpdateQueue.Count > 0)
{
var adorner = _adornerUpdateQueue.Dequeue();
adorner.Update(this);
}
Readback.CompleteWrite(Revision);
_redrawRequested = false;
using (var targetContext = _renderTarget.CreateDrawingContext(null))
{
var layerSize = Size * Scaling;
if (layerSize != _layerSize || _layer == null)
{
_layer?.Dispose();
_layer = null;
_layer = targetContext.CreateLayer(Size);
_layerSize = layerSize;
}
if (!_dirtyRect.IsEmpty)
{
var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer();
using (var context = _layer.CreateDrawingContext(visualBrushHelper))
{
context.PushClip(_dirtyRect);
context.Clear(Colors.Transparent);
Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), _dirtyRect);
context.PopClip();
}
}
targetContext.Clear(Colors.Transparent);
targetContext.Transform = Matrix.Identity;
if (_layer.CanBlit)
_layer.Blit(targetContext);
else
targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1,
new Rect(_layerSize),
new Rect(Size), BitmapInterpolationMode.LowQuality);
if (DrawDirtyRects)
{
targetContext.DrawRectangle(new ImmutableSolidColorBrush(
new Color(30, (byte)_random.Next(255), (byte)_random.Next(255),
(byte)_random.Next(255)))
, null, _dirtyRect);
}
if (DrawFps)
{
var nativeMem = ByteSizeHelper.ToString((ulong)(
(Compositor.BatchMemoryPool.CurrentUsage + Compositor.BatchMemoryPool.CurrentPool) *
Compositor.BatchMemoryPool.BufferSize), false);
var managedMem = ByteSizeHelper.ToString((ulong)(
(Compositor.BatchObjectPool.CurrentUsage + Compositor.BatchObjectPool.CurrentPool) *
Compositor.BatchObjectPool.ArraySize *
IntPtr.Size), false);
_fpsCounter.RenderFps(targetContext, $"M:{managedMem} / N:{nativeMem} R:{RenderedVisuals:0000}");
}
RenderedVisuals = 0;
_dirtyRect = Rect.Empty;
}
}
public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(rect, Scaling);
private static Rect SnapToDevicePixels(Rect rect, double scale)
{
return new Rect(
new Point(
Math.Floor(rect.X * scale) / scale,
Math.Floor(rect.Y * scale) / scale),
new Point(
Math.Ceiling(rect.Right * scale) / scale,
Math.Ceiling(rect.Bottom * scale) / scale));
}
public void AddDirtyRect(Rect rect)
{
if(rect.IsEmpty)
return;
var snapped = SnapToDevicePixels(rect, Scaling);
_dirtyRect = _dirtyRect.Union(snapped);
_redrawRequested = true;
}
public void Invalidate()
{
_redrawRequested = true;
}
public void Dispose()
{
if(_disposed)
return;
_disposed = true;
using (_compositor.GpuContext?.EnsureCurrent())
{
if (_layer != null)
{
_layer.Dispose();
_layer = null;
}
_renderTarget?.Dispose();
_renderTarget = null;
}
_compositor.RemoveCompositionTarget(this);
}
public void AddVisual(ServerCompositionVisual visual)
{
if (_attachedVisuals.Add(visual) && IsEnabled)
visual.Activate();
}
public void RemoveVisual(ServerCompositionVisual visual)
{
if (_attachedVisuals.Remove(visual) && IsEnabled)
visual.Deactivate();
if(visual.IsVisibleInFrame)
AddDirtyRect(visual.TransformedOwnContentBounds);
}
public void EnqueueAdornerUpdate(ServerCompositionVisual visual) => _adornerUpdateQueue.Enqueue(visual);
}
}

76
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs

@ -0,0 +1,76 @@
namespace Avalonia.Rendering.Composition.Server;
partial class ServerCompositionVisual
{
protected bool IsDirtyComposition;
private bool _combinedTransformDirty;
private bool _clipSizeDirty;
private const CompositionVisualChangedFields CompositionFieldsMask
= CompositionVisualChangedFields.Opacity
| CompositionVisualChangedFields.OpacityAnimated
| CompositionVisualChangedFields.OpacityMaskBrush
| CompositionVisualChangedFields.Clip
| CompositionVisualChangedFields.ClipToBounds
| CompositionVisualChangedFields.ClipToBoundsAnimated
| CompositionVisualChangedFields.Size
| CompositionVisualChangedFields.SizeAnimated;
private const CompositionVisualChangedFields CombinedTransformFieldsMask =
CompositionVisualChangedFields.Size
| CompositionVisualChangedFields.SizeAnimated
| CompositionVisualChangedFields.AnchorPoint
| CompositionVisualChangedFields.AnchorPointAnimated
| CompositionVisualChangedFields.CenterPoint
| CompositionVisualChangedFields.CenterPointAnimated
| CompositionVisualChangedFields.AdornedVisual
| CompositionVisualChangedFields.TransformMatrix
| CompositionVisualChangedFields.Scale
| CompositionVisualChangedFields.ScaleAnimated
| CompositionVisualChangedFields.RotationAngle
| CompositionVisualChangedFields.RotationAngleAnimated
| CompositionVisualChangedFields.Orientation
| CompositionVisualChangedFields.OrientationAnimated
| CompositionVisualChangedFields.Offset
| CompositionVisualChangedFields.OffsetAnimated;
private const CompositionVisualChangedFields ClipSizeDirtyMask =
CompositionVisualChangedFields.Size
| CompositionVisualChangedFields.SizeAnimated
| CompositionVisualChangedFields.ClipToBounds
| CompositionVisualChangedFields.ClipToBoundsAnimated;
partial void OnFieldsDeserialized(CompositionVisualChangedFields changed)
{
if ((changed & CompositionFieldsMask) != 0)
IsDirtyComposition = true;
if ((changed & CombinedTransformFieldsMask) != 0)
_combinedTransformDirty = true;
if ((changed & ClipSizeDirtyMask) != 0)
_clipSizeDirty = true;
}
public override void NotifyAnimatedValueChanged(CompositionProperty offset)
{
base.NotifyAnimatedValueChanged(offset);
if (offset == s_IdOfClipToBoundsProperty
|| offset == s_IdOfOpacityProperty
|| offset == s_IdOfSizeProperty)
IsDirtyComposition = true;
if (offset == s_IdOfSizeProperty
|| offset == s_IdOfAnchorPointProperty
|| offset == s_IdOfCenterPointProperty
|| offset == s_IdOfAdornedVisualProperty
|| offset == s_IdOfTransformMatrixProperty
|| offset == s_IdOfScaleProperty
|| offset == s_IdOfRotationAngleProperty
|| offset == s_IdOfOrientationProperty
|| offset == s_IdOfOffsetProperty)
_combinedTransformDirty = true;
if (offset == s_IdOfClipToBoundsProperty
|| offset == s_IdOfSizeProperty)
_clipSizeDirty = true;
}
}

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

@ -0,0 +1,246 @@
using System;
using System.Numerics;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Animations;
using Avalonia.Rendering.Composition.Transport;
using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition.Server
{
/// <summary>
/// Server-side <see cref="CompositionVisual"/> counterpart.
/// Is responsible for computing the transformation matrix, for applying various visual
/// properties before calling visual-specific drawing code and for notifying the
/// <see cref="ServerCompositionTarget"/> for new dirty rects
/// </summary>
partial class ServerCompositionVisual : ServerObject
{
private bool _isDirtyForUpdate;
private Rect _oldOwnContentBounds;
private bool _isBackface;
private Rect? _transformedClipBounds;
private Rect _combinedTransformedClipBounds;
protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
{
}
public void Render(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
{
if(Visible == false || IsVisibleInFrame == false)
return;
if(Opacity == 0)
return;
currentTransformedClip = currentTransformedClip.Intersect(_combinedTransformedClipBounds);
if(currentTransformedClip.IsEmpty)
return;
Root!.RenderedVisuals++;
var transform = GlobalTransformMatrix;
canvas.PostTransform = MatrixUtils.ToMatrix(transform);
canvas.Transform = Matrix.Identity;
if (Opacity != 1)
canvas.PushOpacity(Opacity);
var boundsRect = new Rect(new Size(Size.X, Size.Y));
if(ClipToBounds)
canvas.PushClip(Root!.SnapToDevicePixels(boundsRect));
if (Clip != null)
canvas.PushGeometryClip(Clip);
if(OpacityMaskBrush != null)
canvas.PushOpacityMask(OpacityMaskBrush, boundsRect);
RenderCore(canvas, currentTransformedClip);
// Hack to force invalidation of SKMatrix
canvas.PostTransform = MatrixUtils.ToMatrix(transform);
canvas.Transform = Matrix.Identity;
if (OpacityMaskBrush != null)
canvas.PopOpacityMask();
if (Clip != null)
canvas.PopGeometryClip();
if (ClipToBounds)
canvas.PopClip();
if(Opacity != 1)
canvas.PopOpacity();
}
private ReadbackData _readback0, _readback1, _readback2;
/// <summary>
/// Obtains "readback" data - the data that is sent from the render thread to the UI thread
/// in non-blocking manner. Used mostly by hit-testing
/// </summary>
public ref ReadbackData GetReadback(int idx)
{
if (idx == 0)
return ref _readback0;
if (idx == 1)
return ref _readback1;
return ref _readback2;
}
public Matrix4x4 CombinedTransformMatrix { get; private set; } = Matrix4x4.Identity;
public Matrix4x4 GlobalTransformMatrix { get; private set; }
public virtual void Update(ServerCompositionTarget root)
{
if(Parent == null && Root == null)
return;
var wasVisible = IsVisibleInFrame;
// Calculate new parent-relative transform
if (_combinedTransformDirty)
{
CombinedTransformMatrix = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint,
// HACK: Ignore RenderTransform set by the adorner layer
AdornedVisual != null ? Matrix4x4.Identity : TransformMatrix,
Scale, RotationAngle, Orientation, Offset);
_combinedTransformDirty = false;
}
var parentTransform = (AdornedVisual ?? Parent)?.GlobalTransformMatrix ?? Matrix4x4.Identity;
var newTransform = CombinedTransformMatrix * parentTransform;
// Check if visual was moved and recalculate face orientation
var positionChanged = false;
if (GlobalTransformMatrix != newTransform)
{
_isBackface = Vector3.Transform(
new Vector3(0, 0, float.PositiveInfinity), GlobalTransformMatrix).Z <= 0;
positionChanged = true;
}
var oldTransformedContentBounds = TransformedOwnContentBounds;
var oldCombinedTransformedClipBounds = _combinedTransformedClipBounds;
var dirtyOldBounds = false;
if (_parent?.IsDirtyComposition == true)
{
IsDirtyComposition = true;
_isDirtyForUpdate = true;
dirtyOldBounds = true;
}
GlobalTransformMatrix = newTransform;
var ownBounds = OwnContentBounds;
if (ownBounds != _oldOwnContentBounds || positionChanged)
{
_oldOwnContentBounds = ownBounds;
if (ownBounds.IsEmpty)
TransformedOwnContentBounds = default;
else
TransformedOwnContentBounds =
ownBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix));
}
if (_clipSizeDirty || positionChanged)
{
_transformedClipBounds = ClipToBounds
? new Rect(new Size(Size.X, Size.Y))
.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix))
: null;
_clipSizeDirty = false;
}
_combinedTransformedClipBounds = Parent?._combinedTransformedClipBounds ?? new Rect(Root!.Size);
if (_transformedClipBounds != null)
_combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value);
EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1);
IsVisibleInFrame = Visible && EffectiveOpacity > 0.04 && !_isBackface &&
!_combinedTransformedClipBounds.IsEmpty;
if (wasVisible != IsVisibleInFrame)
_isDirtyForUpdate = true;
// Invalidate previous rect and queue new rect based on visibility
if (positionChanged)
{
if (wasVisible)
dirtyOldBounds = true;
if (IsVisibleInFrame)
_isDirtyForUpdate = true;
}
// Invalidate new bounds
if (IsVisibleInFrame && _isDirtyForUpdate)
{
dirtyOldBounds = true;
AddDirtyRect(TransformedOwnContentBounds.Intersect(_combinedTransformedClipBounds));
}
if (dirtyOldBounds && wasVisible)
AddDirtyRect(oldTransformedContentBounds.Intersect(oldCombinedTransformedClipBounds));
_isDirtyForUpdate = false;
// Update readback indices
var i = Root!.Readback;
ref var readback = ref GetReadback(i.WriteIndex);
readback.Revision = root.Revision;
readback.Matrix = CombinedTransformMatrix;
readback.TargetId = Root.Id;
readback.Visible = IsVisibleInFrame;
}
void AddDirtyRect(Rect rc)
{
if(rc == Rect.Empty)
return;
Root?.AddDirtyRect(rc);
}
/// <summary>
/// Data that can be read from the UI thread
/// </summary>
public struct ReadbackData
{
public Matrix4x4 Matrix;
public ulong Revision;
public long TargetId;
public bool Visible;
}
partial void DeserializeChangesExtra(BatchStreamReader c)
{
ValuesInvalidated();
}
partial void OnRootChanging()
{
if (Root != null)
Root.RemoveVisual(this);
}
partial void OnRootChanged()
{
if (Root != null)
Root.AddVisual(this);
}
protected override void ValuesInvalidated()
{
_isDirtyForUpdate = true;
Root?.Invalidate();
}
public bool IsVisibleInFrame { get; set; }
public double EffectiveOpacity { get; set; }
public Rect TransformedOwnContentBounds { get; set; }
public virtual Rect OwnContentBounds => new Rect(0, 0, Size.X, Size.Y);
}
}

140
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs

@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Animations;
using Avalonia.Rendering.Composition.Expressions;
using Avalonia.Rendering.Composition.Transport;
namespace Avalonia.Rendering.Composition.Server
{
/// <summary>
/// Server-side counterpart of the <see cref="Compositor"/>.
/// 1) manages deserialization of changes received from the UI thread
/// 2) triggers animation ticks
/// 3) asks composition targets to render themselves
/// </summary>
internal class ServerCompositor : IRenderLoopTask
{
private readonly IRenderLoop _renderLoop;
private readonly Queue<Batch> _batches = new Queue<Batch>();
public long LastBatchId { get; private set; }
public Stopwatch Clock { get; } = Stopwatch.StartNew();
public TimeSpan ServerNow { get; private set; }
private List<ServerCompositionTarget> _activeTargets = new();
private HashSet<IAnimationInstance> _activeAnimations = new();
private List<IAnimationInstance> _animationsToUpdate = new();
internal BatchStreamObjectPool<object?> BatchObjectPool;
internal BatchStreamMemoryPool BatchMemoryPool;
private object _lock = new object();
public IPlatformGpuContext? GpuContext { get; }
public ServerCompositor(IRenderLoop renderLoop, IPlatformGpu? platformGpu,
BatchStreamObjectPool<object?> batchObjectPool, BatchStreamMemoryPool batchMemoryPool)
{
GpuContext = platformGpu?.PrimaryContext;
_renderLoop = renderLoop;
BatchObjectPool = batchObjectPool;
BatchMemoryPool = batchMemoryPool;
_renderLoop.Add(this);
}
public void EnqueueBatch(Batch batch)
{
lock (_batches)
_batches.Enqueue(batch);
}
internal void UpdateServerTime() => ServerNow = Clock.Elapsed;
List<Batch> _reusableToCompleteList = new();
void ApplyPendingBatches()
{
while (true)
{
Batch batch;
lock (_batches)
{
if(_batches.Count == 0)
break;
batch = _batches.Dequeue();
}
using (var stream = new BatchStreamReader(batch.Changes, BatchMemoryPool, BatchObjectPool))
{
while (!stream.IsObjectEof)
{
var target = (ServerObject)stream.ReadObject()!;
target.DeserializeChanges(stream, batch);
#if DEBUG_COMPOSITOR_SERIALIZATION
if (stream.ReadObject() != BatchStreamDebugMarkers.ObjectEndMarker)
throw new InvalidOperationException(
$"Object {target.GetType()} failed to deserialize properly on object stream");
if(stream.Read<Guid>() != BatchStreamDebugMarkers.ObjectEndMagic)
throw new InvalidOperationException(
$"Object {target.GetType()} failed to deserialize properly on data stream");
#endif
}
}
_reusableToCompleteList.Add(batch);
LastBatchId = batch.SequenceId;
}
}
void CompletePendingBatches()
{
foreach(var batch in _reusableToCompleteList)
batch.Complete();
_reusableToCompleteList.Clear();
}
bool IRenderLoopTask.NeedsUpdate => false;
void IRenderLoopTask.Update(TimeSpan time)
{
}
public void Render()
{
lock (_lock)
{
RenderCore();
}
}
private void RenderCore()
{
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();
CompletePendingBatches();
}
public void AddCompositionTarget(ServerCompositionTarget target)
{
_activeTargets.Add(target);
}
public void RemoveCompositionTarget(ServerCompositionTarget target)
{
_activeTargets.Remove(target);
}
public void AddToClock(IAnimationInstance animationInstance) =>
_activeAnimations.Add(animationInstance);
public void RemoveFromClock(IAnimationInstance animationInstance) =>
_activeAnimations.Remove(animationInstance);
}
}

44
src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using Avalonia.Rendering.Composition.Transport;
namespace Avalonia.Rendering.Composition.Server
{
/// <summary>
/// A server-side list container capable of receiving changes from the UI thread
/// Right now it's quite dumb since it always receives the full list
/// </summary>
class ServerList<T> : ServerObject where T : ServerObject
{
public List<T> List { get; } = new List<T>();
protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt)
{
if (reader.Read<byte>() == 1)
{
List.Clear();
var count = reader.Read<int>();
for (var c = 0; c < count; c++)
List.Add(reader.ReadObject<T>());
}
base.DeserializeChangesCore(reader, commitedAt);
}
public override long LastChangedBy
{
get
{
var seq = base.LastChangedBy;
foreach (var i in List)
seq = Math.Max(i.LastChangedBy, seq);
return seq;
}
}
public List<T>.Enumerator GetEnumerator() => List.GetEnumerator();
public ServerList(ServerCompositor compositor) : base(compositor)
{
}
}
}

180
src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs

@ -0,0 +1,180 @@
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
{
/// <summary>
/// Server-side <see cref="CompositionObject" /> counterpart.
/// Is responsible for animation activation and invalidation
/// </summary>
internal abstract class ServerObject : IExpressionObject
{
public ServerCompositor Compositor { get; }
public virtual long LastChangedBy => ItselfLastChangedBy;
public long ItselfLastChangedBy { get; private set; }
private uint _activationCount;
public bool IsActive => _activationCount != 0;
private InlineDictionary<CompositionProperty, ServerObjectSubscriptionStore> _subscriptions;
private InlineDictionary<CompositionProperty, IAnimationInstance> _animations;
private class ServerObjectSubscriptionStore
{
public bool IsValid;
public RefTrackingDictionary<IAnimationInstance>? Subscribers;
public void Invalidate()
{
if (IsValid)
return;
IsValid = false;
if (Subscribers != null)
foreach (var sub in Subscribers)
sub.Key.Invalidate();
}
}
public ServerObject(ServerCompositor compositor)
{
Compositor = compositor;
}
public virtual ExpressionVariant GetPropertyForAnimation(string name)
{
return default;
}
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 void Activated()
{
foreach(var kp in _animations)
kp.Value.Activate();
}
protected void Deactivated()
{
foreach(var kp in _animations)
kp.Value.Deactivate();
}
void InvalidateSubscriptions(CompositionProperty property)
{
if(_subscriptions.TryGetValue(property, out var subs))
subs.Invalidate();
}
protected void SetValue<T>(CompositionProperty prop, out T field, T value)
{
field = value;
InvalidateSubscriptions(prop);
}
protected T GetValue<T>(CompositionProperty prop, ref T field)
{
if (_subscriptions.TryGetValue(prop, out var subs))
subs.IsValid = true;
return field;
}
protected void SetAnimatedValue<T>(CompositionProperty prop, ref T field,
TimeSpan commitedAt, IAnimationInstance animation) where T : struct
{
if (IsActive && _animations.TryGetValue(prop, out var oldAnimation))
oldAnimation.Deactivate();
_animations[prop] = animation;
animation.Initialize(commitedAt, ExpressionVariant.Create(field), prop);
if(IsActive)
animation.Activate();
InvalidateSubscriptions(prop);
}
protected void SetAnimatedValue<T>(CompositionProperty property, out T field, T value)
{
if (_animations.TryGetAndRemoveValue(property, out var animation) && IsActive)
animation.Deactivate();
field = value;
InvalidateSubscriptions(property);
}
protected T GetAnimatedValue<T>(CompositionProperty property, ref T field) where T : struct
{
if (_subscriptions.TryGetValue(property, out var subscriptions))
subscriptions.IsValid = true;
if (_animations.TryGetValue(property, out var animation))
field = animation.Evaluate(Compositor.ServerNow, ExpressionVariant.Create(field))
.CastOrDefault<T>();
return field;
}
public virtual void NotifyAnimatedValueChanged(CompositionProperty prop)
{
InvalidateSubscriptions(prop);
ValuesInvalidated();
}
protected virtual void ValuesInvalidated()
{
}
public void SubscribeToInvalidation(CompositionProperty member, IAnimationInstance animation)
{
if (!_subscriptions.TryGetValue(member, out var store))
_subscriptions[member] = store = new ServerObjectSubscriptionStore();
if (store.Subscribers == null)
store.Subscribers = new();
store.Subscribers.AddRef(animation);
}
public void UnsubscribeFromInvalidation(CompositionProperty member, IAnimationInstance animation)
{
if(_subscriptions.TryGetValue(member, out var store))
store.Subscribers?.ReleaseRef(animation);
}
public virtual CompositionProperty? GetCompositionProperty(string fieldName) => null;
protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt)
{
if (this is IDisposable disp
&& reader.Read<byte>() == 1)
disp.Dispose();
}
public void DeserializeChanges(BatchStreamReader reader, Batch batch)
{
DeserializeChangesCore(reader, batch.CommitedAt);
ValuesInvalidated();
ItselfLastChangedBy = batch.SequenceId;
}
}
}

39
src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs

@ -0,0 +1,39 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Avalonia.Rendering.Composition.Transport
{
/// <summary>
/// Represents a group of serialized changes from the UI thread to be atomically applied at the render thread
/// </summary>
internal class Batch
{
private static long _nextSequenceId = 1;
private static ConcurrentBag<BatchStreamData> _pool = new();
public long SequenceId { get; }
public Batch()
{
SequenceId = Interlocked.Increment(ref _nextSequenceId);
if (!_pool.TryTake(out var lst))
lst = new BatchStreamData();
Changes = lst;
}
private TaskCompletionSource<int> _tcs = new TaskCompletionSource<int>();
public BatchStreamData Changes { get; private set; }
public TimeSpan CommitedAt { get; set; }
public void Complete()
{
_pool.Add(Changes);
Changes = null!;
_tcs.TrySetResult(0);
}
public Task Completed => _tcs.Task;
}
}

184
src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs

@ -0,0 +1,184 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using Avalonia.Rendering.Composition.Animations;
using Avalonia.Rendering.Composition.Server;
namespace Avalonia.Rendering.Composition.Transport;
/// <summary>
/// The batch data is separated into 2 "streams":
/// - objects: CLR reference types that are references to either server-side or common objects
/// - structs: blittable types like int, Matrix, Color
/// Each "stream" consists of memory segments that are pooled
/// </summary>
internal class BatchStreamData
{
public Queue<BatchStreamSegment<object?[]>> Objects { get; } = new();
public Queue<BatchStreamSegment<IntPtr>> Structs { get; } = new();
}
public struct BatchStreamSegment<TData>
{
public TData Data { get; set; }
public int ElementCount { get; set; }
}
internal class BatchStreamWriter : IDisposable
{
private readonly BatchStreamData _output;
private readonly BatchStreamMemoryPool _memoryPool;
private readonly BatchStreamObjectPool<object?> _objectPool;
private BatchStreamSegment<object?[]?> _currentObjectSegment;
private BatchStreamSegment<IntPtr> _currentDataSegment;
public BatchStreamWriter(BatchStreamData output, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool<object?> objectPool)
{
_output = output;
_memoryPool = memoryPool;
_objectPool = objectPool;
}
void CommitDataSegment()
{
if (_currentDataSegment.Data != IntPtr.Zero)
_output.Structs.Enqueue(_currentDataSegment);
_currentDataSegment = new ();
}
void NextDataSegment()
{
CommitDataSegment();
_currentDataSegment.Data = _memoryPool.Get();
}
void CommitObjectSegment()
{
if (_currentObjectSegment.Data != null)
_output.Objects.Enqueue(_currentObjectSegment!);
_currentObjectSegment = new();
}
void NextObjectSegment()
{
CommitObjectSegment();
_currentObjectSegment.Data = _objectPool.Get();
}
public unsafe void Write<T>(T item) where T : unmanaged
{
var size = Unsafe.SizeOf<T>();
if (_currentDataSegment.Data == IntPtr.Zero || _currentDataSegment.ElementCount + size > _memoryPool.BufferSize)
NextDataSegment();
Unsafe.WriteUnaligned<T>((byte*)_currentDataSegment.Data + _currentDataSegment.ElementCount, item);
_currentDataSegment.ElementCount += size;
}
public void WriteObject(object? item)
{
if (_currentObjectSegment.Data == null ||
_currentObjectSegment.ElementCount >= _currentObjectSegment.Data.Length)
NextObjectSegment();
_currentObjectSegment.Data![_currentObjectSegment.ElementCount] = item;
_currentObjectSegment.ElementCount++;
}
public void Dispose()
{
CommitDataSegment();
CommitObjectSegment();
}
}
internal class BatchStreamReader : IDisposable
{
private readonly BatchStreamData _input;
private readonly BatchStreamMemoryPool _memoryPool;
private readonly BatchStreamObjectPool<object?> _objectPool;
private BatchStreamSegment<object?[]?> _currentObjectSegment;
private BatchStreamSegment<IntPtr> _currentDataSegment;
private int _memoryOffset, _objectOffset;
public BatchStreamReader(BatchStreamData input, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool<object?> objectPool)
{
_input = input;
_memoryPool = memoryPool;
_objectPool = objectPool;
}
public unsafe T Read<T>() where T : unmanaged
{
var size = Unsafe.SizeOf<T>();
if (_currentDataSegment.Data == IntPtr.Zero)
{
if (_input.Structs.Count == 0)
throw new EndOfStreamException();
_currentDataSegment = _input.Structs.Dequeue();
_memoryOffset = 0;
}
if (_memoryOffset + size > _currentDataSegment.ElementCount)
throw new InvalidOperationException("Attempted to read more memory then left in the current segment");
var rv = Unsafe.ReadUnaligned<T>((byte*)_currentDataSegment.Data + _memoryOffset);
_memoryOffset += size;
if (_memoryOffset == _currentDataSegment.ElementCount)
{
_memoryPool.Return(_currentDataSegment.Data);
_currentDataSegment = new();
}
return rv;
}
public T ReadObject<T>() where T : class? => (T)ReadObject()!;
public object? ReadObject()
{
if (_currentObjectSegment.Data == null)
{
if (_input.Objects.Count == 0)
throw new EndOfStreamException();
_currentObjectSegment = _input.Objects.Dequeue()!;
_objectOffset = 0;
}
var rv = _currentObjectSegment.Data![_objectOffset];
_objectOffset++;
if (_objectOffset == _currentObjectSegment.ElementCount)
{
_objectPool.Return(_currentObjectSegment.Data);
_currentObjectSegment = new();
}
return rv;
}
public bool IsObjectEof => _currentObjectSegment.Data == null && _input.Objects.Count == 0;
public bool IsStructEof => _currentDataSegment.Data == IntPtr.Zero && _input.Structs.Count == 0;
public void Dispose()
{
if (_currentDataSegment.Data != IntPtr.Zero)
{
_memoryPool.Return(_currentDataSegment.Data);
_currentDataSegment = new();
}
while (_input.Structs.Count > 0)
_memoryPool.Return(_input.Structs.Dequeue().Data);
if (_currentObjectSegment.Data != null)
{
_objectPool.Return(_currentObjectSegment.Data);
_currentObjectSegment = new();
}
while (_input.Objects.Count > 0)
_objectPool.Return(_input.Objects.Dequeue().Data);
}
}

152
src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs

@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using Avalonia.Threading;
namespace Avalonia.Rendering.Composition.Transport;
/// <summary>
/// A pool that keeps a number of elements that was used in the last 10 seconds
/// </summary>
internal abstract class BatchStreamPoolBase<T> : IDisposable
{
readonly Stack<T> _pool = new();
bool _disposed;
int _usage;
readonly int[] _usageStatistics = new int[10];
int _usageStatisticsSlot;
public int CurrentUsage => _usage;
public int CurrentPool => _pool.Count;
public BatchStreamPoolBase(bool needsFinalize, Action<Func<bool>>? startTimer = null)
{
if(!needsFinalize)
GC.SuppressFinalize(needsFinalize);
var updateRef = new WeakReference<BatchStreamPoolBase<T>>(this);
StartUpdateTimer(startTimer, updateRef);
}
static void StartUpdateTimer(Action<Func<bool>>? startTimer, WeakReference<BatchStreamPoolBase<T>> updateRef)
{
Func<bool> timerProc = () =>
{
if (updateRef.TryGetTarget(out var target))
{
target.UpdateStatistics();
return true;
}
return false;
};
if (startTimer != null)
startTimer(timerProc);
else
DispatcherTimer.Run(timerProc, TimeSpan.FromSeconds(1));
}
private void UpdateStatistics()
{
lock (_pool)
{
var maximumUsage = _usageStatistics.Max();
var recentlyUsedPooledSlots = maximumUsage - _usage;
var keepSlots = Math.Max(recentlyUsedPooledSlots, 10);
while (keepSlots < _pool.Count)
DestroyItem(_pool.Pop());
_usageStatisticsSlot = (_usageStatisticsSlot + 1) % _usageStatistics.Length;
_usageStatistics[_usageStatisticsSlot] = 0;
}
}
protected abstract T CreateItem();
protected virtual void DestroyItem(T item)
{
}
public T Get()
{
lock (_pool)
{
_usage++;
if (_usageStatistics[_usageStatisticsSlot] < _usage)
_usageStatistics[_usageStatisticsSlot] = _usage;
if (_pool.Count != 0)
return _pool.Pop();
}
return CreateItem();
}
public void Return(T item)
{
lock (_pool)
{
_usage--;
if (!_disposed)
{
_pool.Push(item);
return;
}
}
DestroyItem(item);
}
public void Dispose()
{
lock (_pool)
{
_disposed = true;
foreach (var item in _pool)
DestroyItem(item);
_pool.Clear();
}
}
~BatchStreamPoolBase()
{
Dispose();
}
}
internal sealed class BatchStreamObjectPool<T> : BatchStreamPoolBase<T[]> where T : class?
{
public int ArraySize { get; }
public BatchStreamObjectPool(int arraySize = 128, Action<Func<bool>>? startTimer = null) : base(false, startTimer)
{
ArraySize = arraySize;
}
protected override T[] CreateItem()
{
return new T[ArraySize];
}
protected override void DestroyItem(T[] item)
{
Array.Clear(item, 0, item.Length);
}
}
internal sealed class BatchStreamMemoryPool : BatchStreamPoolBase<IntPtr>
{
public int BufferSize { get; }
public BatchStreamMemoryPool(int bufferSize = 1024, Action<Func<bool>>? startTimer = null) : base(true, startTimer)
{
BufferSize = bufferSize;
}
protected override IntPtr CreateItem() => Marshal.AllocHGlobal(BufferSize);
protected override void DestroyItem(IntPtr item) => Marshal.FreeHGlobal(item);
}

9
src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs

@ -0,0 +1,9 @@
using System;
namespace Avalonia.Rendering.Composition.Transport;
internal class BatchStreamDebugMarkers
{
public static object ObjectEndMarker = new object();
public static Guid ObjectEndMagic = Guid.NewGuid();
}

98
src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs

@ -0,0 +1,98 @@
using System.Collections;
using System.Collections.Generic;
using Avalonia.Rendering.Composition.Server;
namespace Avalonia.Rendering.Composition.Transport
{
/// <summary>
/// A helper class used from generated UI-thread-side collections of composition objects.
/// </summary>
// NOTE: This should probably be a base class since TServer isn't used anymore and it was the reason why
// it couldn't be exposed as a base class
class ServerListProxyHelper<TClient, TServer> : IList<TClient>
where TServer : ServerObject
where TClient : CompositionObject
{
private readonly IRegisterForSerialization _parent;
private bool _changed;
public interface IRegisterForSerialization
{
void RegisterForSerialization();
}
public ServerListProxyHelper(IRegisterForSerialization parent)
{
_parent = parent;
}
private readonly List<TClient> _list = new List<TClient>();
IEnumerator<TClient> IEnumerable<TClient>.GetEnumerator() => GetEnumerator();
public List<TClient>.Enumerator GetEnumerator() => _list.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Add(TClient item) => Insert(_list.Count, item);
public void Clear()
{
_list.Clear();
_changed = true;
_parent.RegisterForSerialization();
}
public bool Contains(TClient item) => _list.Contains(item);
public void CopyTo(TClient[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex);
public bool Remove(TClient item)
{
var idx = _list.IndexOf(item);
if (idx == -1)
return false;
RemoveAt(idx);
return true;
}
public int Count => _list.Count;
public bool IsReadOnly => false;
public int IndexOf(TClient item) => _list.IndexOf(item);
public void Insert(int index, TClient item)
{
_list.Insert(index, item);
_changed = true;
_parent.RegisterForSerialization();
}
public void RemoveAt(int index)
{
_list.RemoveAt(index);
_changed = true;
_parent.RegisterForSerialization();
}
public TClient this[int index]
{
get => _list[index];
set
{
_list[index] = value;
_changed = true;
_parent.RegisterForSerialization();
}
}
public void Serialize(BatchStreamWriter writer)
{
writer.Write((byte)(_changed ? 1 : 0));
if (_changed)
{
writer.Write(_list.Count);
foreach (var el in _list)
writer.WriteObject(el.Server);
}
_changed = false;
}
}
}

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

@ -0,0 +1,56 @@
using System;
using System.Numerics;
using Avalonia.Media;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.Composition
{
/// <summary>
/// The base visual object in the composition visual hierarchy.
/// </summary>
public abstract partial class CompositionVisual
{
private IBrush? _opacityMask;
private protected virtual void OnRootChangedCore()
{
}
partial void OnRootChanged() => OnRootChangedCore();
partial void OnParentChanged() => Root = Parent?.Root;
public IBrush? OpacityMask
{
get => _opacityMask;
set
{
if (_opacityMask == value)
return;
OpacityMaskBrush = (_opacityMask = value)?.ToImmutable();
}
}
internal Matrix4x4? TryGetServerTransform()
{
if (Root == null)
return null;
var i = Root.Server.Readback;
ref var readback = ref Server.GetReadback(i.ReadIndex);
// CompositionVisual wasn't visible or wasn't even attached to the composition target during the lat frame
if (!readback.Visible || readback.Revision < i.ReadRevision)
return null;
// CompositionVisual was reparented (potential race here)
if (readback.TargetId != Root.Server.Id)
return null;
return readback.Matrix;
}
internal object? Tag { get; set; }
internal virtual bool HitTest(Point point, Func<IVisual, bool>? filter) => true;
}
}

73
src/Avalonia.Base/Rendering/Composition/VisualCollection.cs

@ -0,0 +1,73 @@
using System;
using Avalonia.Rendering.Composition.Server;
namespace Avalonia.Rendering.Composition
{
/// <summary>
/// A collection of CompositionVisual objects
/// </summary>
public partial class CompositionVisualCollection : CompositionObject
{
private CompositionVisual _owner;
internal CompositionVisualCollection(CompositionVisual parent, ServerCompositionVisualCollection server) : base(parent.Compositor, server)
{
_owner = parent;
InitializeDefaults();
}
public void InsertAbove(CompositionVisual newChild, CompositionVisual sibling)
{
var idx = _list.IndexOf(sibling);
if (idx == -1)
throw new InvalidOperationException();
Insert(idx + 1, newChild);
}
public void InsertBelow(CompositionVisual newChild, CompositionVisual sibling)
{
var idx = _list.IndexOf(sibling);
if (idx == -1)
throw new InvalidOperationException();
Insert(idx, newChild);
}
public void InsertAtTop(CompositionVisual newChild) => Insert(_list.Count, newChild);
public void InsertAtBottom(CompositionVisual newChild) => Insert(0, newChild);
public void RemoveAll() => Clear();
partial void OnAdded(CompositionVisual item) => item.Parent = _owner;
partial void OnBeforeReplace(CompositionVisual oldItem, CompositionVisual newItem)
{
if (oldItem != newItem)
OnBeforeAdded(newItem);
}
partial void OnReplace(CompositionVisual oldItem, CompositionVisual newItem)
{
if (oldItem != newItem)
{
OnRemoved(oldItem);
OnAdded(newItem);
}
}
partial void OnRemoved(CompositionVisual item) => item.Parent = null;
partial void OnBeforeClear()
{
foreach (var i in this)
i.Parent = null;
}
partial void OnBeforeAdded(CompositionVisual item)
{
if (item.Parent != null)
throw new InvalidOperationException("Visual already has a parent");
item.Parent = item;
}
}
}

2
src/Avalonia.Base/Rendering/DefaultRenderTimer.cs

@ -59,6 +59,8 @@ namespace Avalonia.Rendering
}
}
public bool RunsInBackground => true;
/// <summary>
/// Starts the timer.
/// </summary>

6
src/Avalonia.Base/Rendering/DeferredRenderer.cs

@ -272,16 +272,18 @@ namespace Avalonia.Rendering
}
}
Scene? TryGetChildScene(IRef<IDrawOperation>? op) => (op?.Item as BrushDrawOperation)?.Aux as Scene;
/// <inheritdoc/>
Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
{
return (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty;
return TryGetChildScene(_currentDraw)?.Size ?? Size.Empty;
}
/// <inheritdoc/>
void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush)
{
var childScene = (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual];
var childScene = TryGetChildScene(_currentDraw);
if (childScene != null)
{

2
src/Avalonia.Base/Rendering/IRenderLoop.cs

@ -27,5 +27,7 @@ namespace Avalonia.Rendering
/// </summary>
/// <param name="i">The update task.</param>
void Remove(IRenderLoopTask i);
bool RunsInBackground { get; }
}
}

5
src/Avalonia.Base/Rendering/IRenderTimer.cs

@ -18,5 +18,10 @@ namespace Avalonia.Rendering
/// switch execution to the right thread.
/// </remarks>
event Action<TimeSpan> Tick;
/// <summary>
/// Indicates if the timer ticks on a non-UI thread
/// </summary>
bool RunsInBackground { get; }
}
}

6
src/Avalonia.Base/Rendering/IRenderer.cs

@ -1,6 +1,7 @@
using System;
using Avalonia.VisualTree;
using System.Collections.Generic;
using Avalonia.Rendering.Composition;
namespace Avalonia.Rendering
{
@ -87,4 +88,9 @@ namespace Avalonia.Rendering
/// </summary>
void Stop();
}
public interface IRendererWithCompositor : IRenderer
{
Compositor Compositor { get; }
}
}

6
src/Avalonia.Base/Rendering/ImmediateRenderer.cs

@ -331,7 +331,11 @@ namespace Avalonia.Rendering
if (_updateTransformedBounds)
visual.TransformedBounds = transformed;
foreach (var child in visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance))
var childrenEnumerable = visual.HasNonUniformZIndexChildren
? visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance)
: (IEnumerable<IVisual>)visual.VisualChildren;
foreach (var child in childrenEnumerable)
{
var childBounds = GetTransformedBounds(child);

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

@ -87,6 +87,8 @@ namespace Avalonia.Rendering
}
}
public bool RunsInBackground => Timer.RunsInBackground;
private void TimerTick(TimeSpan time)
{
if (Interlocked.CompareExchange(ref _inTick, 1, 0) == 0)

16
src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.VisualTree;
@ -9,14 +10,21 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
internal abstract class BrushDrawOperation : DrawOperation
{
public BrushDrawOperation(Rect bounds, Matrix transform)
public BrushDrawOperation(Rect bounds, Matrix transform, IDisposable? aux)
: base(bounds, transform)
{
Aux = aux;
}
/// <summary>
/// Gets a collection of child scenes that are needed to draw visual brushes.
/// Auxiliary data required to draw the brush
/// </summary>
public abstract IDictionary<IVisual, Scene>? ChildScenes { get; }
public IDisposable? Aux { get; }
public override void Dispose()
{
Aux?.Dispose();
base.Dispose();
}
}
}

5
src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Utilities;
@ -456,7 +457,7 @@ namespace Avalonia.Rendering.SceneGraph
return _drawOperationindex < _node!.DrawOperations.Count ? _node.DrawOperations[_drawOperationindex] as IRef<T> : null;
}
private IDictionary<IVisual, Scene>? CreateChildScene(IBrush? brush)
private IDisposable? CreateChildScene(IBrush? brush)
{
var visualBrush = brush as VisualBrush;
@ -469,7 +470,7 @@ namespace Avalonia.Rendering.SceneGraph
(visual as IVisualBrushInitialize)?.EnsureInitialized();
var scene = new Scene(visual);
_sceneBuilder.UpdateAll(scene);
return new Dictionary<IVisual, Scene> { { visualBrush.Visual, scene } };
return scene;
}
}

7
src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs

@ -17,14 +17,13 @@ namespace Avalonia.Rendering.SceneGraph
IBrush? brush,
IPen? pen,
Rect rect,
IDictionary<IVisual, Scene>? childScenes = null)
: base(rect.Inflate(pen?.Thickness ?? 0), transform)
IDisposable? aux = null)
: base(rect.Inflate(pen?.Thickness ?? 0), transform, aux)
{
Transform = transform;
Brush = brush?.ToImmutable();
Pen = pen?.ToImmutable();
Rect = rect;
ChildScenes = childScenes;
}
/// <summary>
@ -47,8 +46,6 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
public Rect Rect { get; }
public override IDictionary<IVisual, Scene>? ChildScenes { get; }
public bool Equals(Matrix transform, IBrush? brush, IPen? pen, Rect rect)
{
return transform == Transform &&

11
src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
@ -23,14 +24,13 @@ namespace Avalonia.Rendering.SceneGraph
IBrush? brush,
IPen? pen,
IGeometryImpl geometry,
IDictionary<IVisual, Scene>? childScenes = null)
: base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform)
IDisposable? aux)
: base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, aux)
{
Transform = transform;
Brush = brush?.ToImmutable();
Pen = pen?.ToImmutable();
Geometry = geometry;
ChildScenes = childScenes;
}
/// <summary>
@ -53,9 +53,6 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
public IGeometryImpl Geometry { get; }
/// <inheritdoc/>
public override IDictionary<IVisual, Scene>? ChildScenes { get; }
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>

11
src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Media.Immutable;
@ -23,13 +24,12 @@ namespace Avalonia.Rendering.SceneGraph
Matrix transform,
IBrush foreground,
GlyphRun glyphRun,
IDictionary<IVisual, Scene>? childScenes = null)
: base(new Rect(glyphRun.Size), transform)
IDisposable? aux = null)
: base(new Rect(glyphRun.Size), transform, aux)
{
Transform = transform;
Foreground = foreground.ToImmutable();
GlyphRun = glyphRun;
ChildScenes = childScenes;
}
/// <summary>
@ -47,9 +47,6 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
public GlyphRun GlyphRun { get; }
/// <inheritdoc/>
public override IDictionary<IVisual, Scene>? ChildScenes { get; }
/// <inheritdoc/>
public override void Render(IDrawingContextImpl context)
{

8
src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs

@ -25,14 +25,13 @@ namespace Avalonia.Rendering.SceneGraph
IPen pen,
Point p1,
Point p2,
IDictionary<IVisual, Scene>? childScenes = null)
: base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform)
IDisposable? aux = null)
: base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform, aux)
{
Transform = transform;
Pen = pen.ToImmutable();
P1 = p1;
P2 = p2;
ChildScenes = childScenes;
}
/// <summary>
@ -55,9 +54,6 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
public Point P2 { get; }
/// <inheritdoc/>
public override IDictionary<IVisual, Scene>? ChildScenes { get; }
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>

12
src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.VisualTree;
@ -17,12 +18,11 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="mask">The opacity mask to push.</param>
/// <param name="bounds">The bounds of the mask.</param>
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
public OpacityMaskNode(IBrush mask, Rect bounds, IDictionary<IVisual, Scene>? childScenes = null)
: base(Rect.Empty, Matrix.Identity)
public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null)
: base(Rect.Empty, Matrix.Identity, aux)
{
Mask = mask.ToImmutable();
MaskBounds = bounds;
ChildScenes = childScenes;
}
/// <summary>
@ -30,7 +30,7 @@ namespace Avalonia.Rendering.SceneGraph
/// opacity mask pop.
/// </summary>
public OpacityMaskNode()
: base(Rect.Empty, Matrix.Identity)
: base(Rect.Empty, Matrix.Identity, null)
{
}
@ -44,8 +44,6 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
public Rect? MaskBounds { get; }
/// <inheritdoc/>
public override IDictionary<IVisual, Scene>? ChildScenes { get; }
/// <inheritdoc/>
public override bool HitTest(Point p) => false;

11
src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
@ -26,14 +27,13 @@ namespace Avalonia.Rendering.SceneGraph
IPen? pen,
RoundedRect rect,
BoxShadows boxShadows,
IDictionary<IVisual, Scene>? childScenes = null)
: base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform)
IDisposable? aux = null)
: base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, aux)
{
Transform = transform;
Brush = brush?.ToImmutable();
Pen = pen?.ToImmutable();
Rect = rect;
ChildScenes = childScenes;
BoxShadows = boxShadows;
}
@ -62,9 +62,6 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
public BoxShadows BoxShadows { get; }
/// <inheritdoc/>
public override IDictionary<IVisual, Scene>? ChildScenes { get; }
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>

44
src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs

@ -275,26 +275,36 @@ namespace Avalonia.Rendering.SceneGraph
else if (visualChildren.Count > 1)
{
var count = visualChildren.Count;
var sortedChildren = new (IVisual visual, int index)[count];
for (var i = 0; i < count; i++)
if (visual.HasNonUniformZIndexChildren)
{
sortedChildren[i] = (visualChildren[i], i);
}
// Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements.
Array.Sort(sortedChildren, (lhs, rhs) =>
{
var result = ZIndexComparer.Instance.Compare(lhs.visual, rhs.visual);
return result == 0 ? lhs.index.CompareTo(rhs.index) : result;
});
foreach (var child in sortedChildren)
{
var childNode = GetOrCreateChildNode(scene, child.Item1, node);
Update(context, scene, (VisualNode)childNode, clip, forceRecurse);
var sortedChildren = new (IVisual visual, int index)[count];
for (var i = 0; i < count; i++)
{
sortedChildren[i] = (visualChildren[i], i);
}
// Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements.
Array.Sort(sortedChildren, (lhs, rhs) =>
{
var result = ZIndexComparer.Instance.Compare(lhs.visual, rhs.visual);
return result == 0 ? lhs.index.CompareTo(rhs.index) : result;
});
foreach (var child in sortedChildren)
{
var childNode = GetOrCreateChildNode(scene, child.Item1, node);
Update(context, scene, (VisualNode)childNode, clip, forceRecurse);
}
}
else
foreach (var child in visualChildren)
{
var childNode = GetOrCreateChildNode(scene, child, node);
Update(context, scene, (VisualNode)childNode, clip, forceRecurse);
}
}
node.SubTreeUpdated = true;

4
src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs

@ -43,6 +43,8 @@ namespace Avalonia.Rendering
}
}
public bool RunsInBackground => true;
void LoopProc()
{
var lastTick = _st.Elapsed;
@ -51,7 +53,7 @@ namespace Avalonia.Rendering
var now = _st.Elapsed;
var timeTillNextTick = lastTick + _timeBetweenTicks - now;
if (timeTillNextTick.TotalMilliseconds > 1) Thread.Sleep(timeTillNextTick);
lastTick = now;
lastTick = now = _st.Elapsed;
lock (_lock)
{
if (_count == 0)

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save