diff --git a/.ncrunch/ControlCatalog.v3.ncrunchproject b/.ncrunch/ControlCatalog.net6.0.v3.ncrunchproject
similarity index 100%
rename from .ncrunch/ControlCatalog.v3.ncrunchproject
rename to .ncrunch/ControlCatalog.net6.0.v3.ncrunchproject
diff --git a/.ncrunch/ControlCatalog.netstandard2.0.v3.ncrunchproject b/.ncrunch/ControlCatalog.netstandard2.0.v3.ncrunchproject
new file mode 100644
index 0000000000..319cd523ce
--- /dev/null
+++ b/.ncrunch/ControlCatalog.netstandard2.0.v3.ncrunchproject
@@ -0,0 +1,5 @@
+
+
+ True
+
+
\ No newline at end of file
diff --git a/Avalonia.sln b/Avalonia.sln
index 25c7daf080..4999719676 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -38,6 +38,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
+ src\Shared\IsExternalInit.cs = src\Shared\IsExternalInit.cs
src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs
src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs
EndProjectSection
@@ -205,14 +206,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\S
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.SourceGenerator", "src\Avalonia.SourceGenerator\Avalonia.SourceGenerator.csproj", "{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevAnalyzers", "src\tools\DevAnalyzers\DevAnalyzers.csproj", "{2B390431-288C-435C-BB6B-A374033BD8D1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ColorPicker", "src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj", "{7BF6C69D-FC14-43EB-9ED0-782C16F3D5D9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Tests", "tests\Avalonia.DesignerSupport.Tests\Avalonia.DesignerSupport.Tests.csproj", "{EABE2161-989B-42BF-BD8D-1E34B20C21F1}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevGenerators", "src\tools\DevGenerators\DevGenerators.csproj", "{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -485,10 +486,6 @@ Global
{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|Any CPU.Build.0 = Release|Any CPU
- {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.Build.0 = Release|Any CPU
{2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2B390431-288C-435C-BB6B-A374033BD8D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -501,6 +498,10 @@ Global
{EABE2161-989B-42BF-BD8D-1E34B20C21F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EABE2161-989B-42BF-BD8D-1E34B20C21F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EABE2161-989B-42BF-BD8D-1E34B20C21F1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -557,6 +558,8 @@ 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}
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}
diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml
new file mode 100644
index 0000000000..0b79758c76
--- /dev/null
+++ b/azure-pipelines-integrationtests.yml
@@ -0,0 +1,67 @@
+# Starter pipeline
+# Start with a minimal pipeline that you can customize to build and deploy your code.
+# Add steps that build, run tests, deploy, and more:
+# https://aka.ms/yaml
+
+trigger:
+- master
+
+jobs:
+- job: Mac
+ pool:
+ name: 'AvaloniaMacPool'
+
+ steps:
+ - 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
+ pool:
+ vmImage: 'windows-2022'
+
+ steps:
+ - task: UseDotNet@2
+ displayName: 'Use .NET Core SDK 6.0.202'
+ inputs:
+ version: 6.0.202
+
+ - task: Windows Application Driver@0
+ inputs:
+ OperationType: 'Start'
+ AgentResolution: '4K'
+ displayName: 'Start WinAppDriver'
+
+ - task: DotNetCoreCLI@2
+ inputs:
+ command: 'build'
+ projects: 'samples/IntegrationTestApp/IntegrationTestApp.csproj'
+
+ - task: DotNetCoreCLI@2
+ inputs:
+ command: 'test'
+ projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj'
+
+ - task: Windows Application Driver@0
+ inputs:
+ OperationType: 'Stop'
+ displayName: 'Stop WinAppDriver'
diff --git a/build/SourceGenerators.props b/build/SourceGenerators.props
index d000af1bf6..4929578b60 100644
--- a/build/SourceGenerators.props
+++ b/build/SourceGenerators.props
@@ -1,7 +1,7 @@
diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
index 6fc3977d4e..ace4a71a56 100644
--- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
+++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
@@ -49,6 +49,7 @@
AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */; };
BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */ = {isa = PBXBuildFile; fileRef = BC11A5BC2608D58F0017BAD0 /* automation.h */; };
BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */ = {isa = PBXBuildFile; fileRef = BC11A5BD2608D58F0017BAD0 /* automation.mm */; };
+ ED3791C42862E1F40080BD62 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -101,6 +102,7 @@
AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = platformthreading.mm; sourceTree = ""; };
BC11A5BC2608D58F0017BAD0 /* automation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = automation.h; sourceTree = ""; };
BC11A5BD2608D58F0017BAD0 /* automation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = automation.mm; sourceTree = ""; };
+ ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -108,6 +110,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ ED3791C42862E1F40080BD62 /* UniformTypeIdentifiers.framework in Frameworks */,
1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */,
1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */,
AB1E522C217613570091CD71 /* OpenGL.framework in Frameworks */,
@@ -122,6 +125,7 @@
AB661C1C2148230E00291242 /* Frameworks */ = {
isa = PBXGroup;
children = (
+ ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */,
522D5958258159C1006F7F7A /* Carbon.framework */,
1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */,
1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */,
diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm
index 01725ace03..4ae6ad5a00 100644
--- a/native/Avalonia.Native/src/OSX/AvnView.mm
+++ b/native/Avalonia.Native/src/OSX/AvnView.mm
@@ -127,7 +127,11 @@
[self updateRenderTarget];
auto reason = [self inLiveResize] ? ResizeUser : _resizeReason;
- _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason);
+
+ if(_parent->IsShown())
+ {
+ _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason);
+ }
}
}
diff --git a/native/Avalonia.Native/src/OSX/SystemDialogs.mm b/native/Avalonia.Native/src/OSX/SystemDialogs.mm
index 535b6c3b66..e133a5d31f 100644
--- a/native/Avalonia.Native/src/OSX/SystemDialogs.mm
+++ b/native/Avalonia.Native/src/OSX/SystemDialogs.mm
@@ -1,5 +1,6 @@
#include "common.h"
#include "INSWindowHolder.h"
+#import
class SystemDialogs : public ComSingleObject
{
@@ -7,6 +8,7 @@ public:
FORWARD_IUNKNOWN()
virtual void SelectFolderDialog (IAvnWindow* parentWindowHandle,
IAvnSystemDialogEvents* events,
+ bool allowMultiple,
const char* title,
const char* initialDirectory) override
{
@@ -14,6 +16,7 @@ public:
{
auto panel = [NSOpenPanel openPanel];
+ panel.allowsMultipleSelection = allowMultiple;
panel.canChooseDirectories = true;
panel.canCreateDirectories = true;
panel.canChooseFiles = false;
@@ -118,7 +121,15 @@ public:
{
auto allowedTypes = [filtersString componentsSeparatedByString:@";"];
- panel.allowedFileTypes = allowedTypes;
+ // Prefer allowedContentTypes if available
+ if (@available(macOS 11.0, *))
+ {
+ panel.allowedContentTypes = ConvertToUTType(allowedTypes);
+ }
+ else
+ {
+ panel.allowedFileTypes = allowedTypes;
+ }
}
}
@@ -207,7 +218,18 @@ public:
{
auto allowedTypes = [filtersString componentsSeparatedByString:@";"];
- panel.allowedFileTypes = allowedTypes;
+ // Prefer allowedContentTypes if available
+ if (@available(macOS 11.0, *))
+ {
+ panel.allowedContentTypes = ConvertToUTType(allowedTypes);
+ }
+ else
+ {
+ panel.allowedFileTypes = allowedTypes;
+ }
+
+ panel.allowsOtherFileTypes = false;
+ panel.extensionHidden = false;
}
}
@@ -250,6 +272,32 @@ public:
}
}
}
+
+private:
+ NSMutableArray* ConvertToUTType(NSArray* allowedTypes)
+ {
+ auto originalCount = [allowedTypes count];
+ auto mapped = [[NSMutableArray alloc] init];
+
+ if (@available(macOS 11.0, *))
+ {
+ for (int i = 0; i < originalCount; i++)
+ {
+ auto utTypeStr = allowedTypes[i];
+ auto utType = [UTType typeWithIdentifier:utTypeStr];
+ if (utType == nil)
+ {
+ utType = [UTType typeWithMIMEType:utTypeStr];
+ }
+ if (utType != nil)
+ {
+ [mapped addObject:utType];
+ }
+ }
+ }
+
+ return mapped;
+ }
};
diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
index 45d78b3926..77f53332cd 100644
--- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
+++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
@@ -48,7 +48,6 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl,
[Window setContentMaxSize:lastMaxSize];
[Window setOpaque:false];
- [Window setHasShadow:true];
}
HRESULT WindowBaseImpl::ObtainNSViewHandle(void **ret) {
diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm
index 85a89955f4..95f61422cb 100644
--- a/native/Avalonia.Native/src/OSX/WindowImpl.mm
+++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm
@@ -24,6 +24,8 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBase
_lastTitle = @"";
_parent = nullptr;
WindowEvents = events;
+
+ [Window setHasShadow:true];
OnInitialiseNSWindow();
}
diff --git a/native/Avalonia.Native/src/OSX/rendertarget.mm b/native/Avalonia.Native/src/OSX/rendertarget.mm
index 266d0345d1..2075cc85ab 100644
--- a/native/Avalonia.Native/src/OSX/rendertarget.mm
+++ b/native/Avalonia.Native/src/OSX/rendertarget.mm
@@ -13,6 +13,7 @@
{
@public IOSurfaceRef surface;
@public AvnPixelSize size;
+ @public bool hasContent;
@public float scale;
ComPtr _context;
GLuint _framebuffer, _texture, _renderbuffer;
@@ -41,6 +42,7 @@
self->scale = scale;
self->size = size;
self->_context = context;
+ self->hasContent = false;
return self;
}
@@ -92,6 +94,7 @@
_context->MakeCurrent(release.getPPV());
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
glFlush();
+ self->hasContent = true;
}
-(void) dealloc
@@ -170,6 +173,8 @@ static IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(IOSurfaceRenderTarget* ta
@synchronized (lock) {
if(_layer == nil)
return;
+ if(!surface->hasContent)
+ return;
[CATransaction begin];
[_layer setContents: nil];
if(surface != nil)
@@ -213,6 +218,7 @@ static IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(IOSurfaceRenderTarget* ta
memcpy(pSurface + y*sstride, pFb + y*fstride, wbytes);
}
IOSurfaceUnlock(surf, 0, nil);
+ surface->hasContent = true;
[self updateLayer];
return S_OK;
}
diff --git a/samples/BindingDemo/App.xaml b/samples/BindingDemo/App.xaml
index 9260dd280f..3e312c8685 100644
--- a/samples/BindingDemo/App.xaml
+++ b/samples/BindingDemo/App.xaml
@@ -3,7 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="BindingDemo.App">
-
+
-
\ No newline at end of file
+
diff --git a/samples/BindingDemo/BindingDemo.csproj b/samples/BindingDemo/BindingDemo.csproj
index bd6054327f..056d3bf552 100644
--- a/samples/BindingDemo/BindingDemo.csproj
+++ b/samples/BindingDemo/BindingDemo.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
index e52430f50b..a43ea4539a 100644
--- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
+++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
@@ -9,7 +9,6 @@
1.0
apk
true
- android-arm64;android-x64
@@ -21,12 +20,12 @@
True
-
+
True
diff --git a/samples/ControlCatalog.NetCore/NativeControls/Gtk/EmbedSample.Gtk.cs b/samples/ControlCatalog.NetCore/NativeControls/Gtk/EmbedSample.Gtk.cs
index 521d3674eb..81a5ba536f 100644
--- a/samples/ControlCatalog.NetCore/NativeControls/Gtk/EmbedSample.Gtk.cs
+++ b/samples/ControlCatalog.NetCore/NativeControls/Gtk/EmbedSample.Gtk.cs
@@ -22,8 +22,7 @@ public class EmbedSampleGtk : INativeDemoControl
var control = createDefault();
var nodes = Path.GetFullPath(Path.Combine(typeof(EmbedSample).Assembly.GetModules()[0].FullyQualifiedName,
- "..",
- "nodes.mp4"));
+ "..", "NativeControls", "Gtk", "nodes.mp4"));
_mplayer = Process.Start(new ProcessStartInfo("mplayer",
$"-vo x11 -zoom -loop 0 -wid {control.Handle.ToInt64()} \"{nodes}\"")
{
diff --git a/samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs b/samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs
index 456f77a44d..b1fef7c013 100644
--- a/samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs
+++ b/samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs
@@ -2,6 +2,7 @@ using System;
using System.Threading.Tasks;
using Avalonia.Controls.Platform;
using Avalonia.Platform.Interop;
+using Avalonia.X11.Interop;
using Avalonia.X11.NativeDialogs;
using static Avalonia.X11.NativeDialogs.Gtk;
using static Avalonia.X11.NativeDialogs.Glib;
@@ -10,8 +11,6 @@ namespace ControlCatalog.NetCore;
internal class GtkHelper
{
- private static Task s_gtkTask;
-
class FileChooser : INativeControlHostDestroyableControlHandle
{
private readonly IntPtr _widget;
@@ -38,11 +37,7 @@ internal class GtkHelper
public static INativeControlHostDestroyableControlHandle CreateGtkFileChooser(IntPtr parentXid)
{
- if (s_gtkTask == null)
- s_gtkTask = StartGtk();
- if (!s_gtkTask.Result)
- return null;
- return RunOnGlibThread(() =>
+ return GtkInteropHelper.RunOnGlibThread(() =>
{
using (var title = new Utf8Buffer("Embedded"))
{
diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs
index 13751b56b5..d98a068d84 100644
--- a/samples/ControlCatalog.NetCore/Program.cs
+++ b/samples/ControlCatalog.NetCore/Program.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
@@ -53,7 +53,11 @@ namespace ControlCatalog.NetCore
else if (args.Contains("--full-headless"))
{
return builder
- .UseHeadless(true)
+ .UseHeadless(new AvaloniaHeadlessPlatformOptions
+ {
+ UseHeadlessDrawing = true,
+ UseCompositor = true
+ })
.AfterSetup(_ =>
{
DispatcherTimer.RunOnce(async () =>
@@ -63,12 +67,11 @@ namespace ControlCatalog.NetCore
var tc = window.GetLogicalDescendants().OfType().First();
foreach (var page in tc.Items.Cast().ToList())
{
- // Skip DatePicker because of some layout bug in grid
- if (page.Header.ToString() == "DatePicker")
+ if (page.Header.ToString() == "DatePicker" || page.Header.ToString() == "TreeView")
continue;
Console.WriteLine("Selecting " + page.Header);
tc.SelectedItem = page;
- await Task.Delay(500);
+ await Task.Delay(50);
}
Console.WriteLine("Selecting the first page");
tc.SelectedItem = tc.Items.OfType
+
@@ -32,6 +35,11 @@
-
+
+
+
+
+
+
diff --git a/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs
index a6a5953827..cefbf642be 100644
--- a/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs
+++ b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
diff --git a/src/Avalonia.Base/Collections/Pooled/PooledList.cs b/src/Avalonia.Base/Collections/Pooled/PooledList.cs
index 803b8d60dc..267c403ab7 100644
--- a/src/Avalonia.Base/Collections/Pooled/PooledList.cs
+++ b/src/Avalonia.Base/Collections/Pooled/PooledList.cs
@@ -1434,7 +1434,7 @@ namespace Avalonia.Collections.Pooled
///
/// Returns the internal buffers to the ArrayPool.
///
- public void Dispose()
+ public virtual void Dispose()
{
ReturnArray();
_size = 0;
diff --git a/src/Avalonia.Base/Controls/Classes.cs b/src/Avalonia.Base/Controls/Classes.cs
index 50605661fa..c3d3fbca46 100644
--- a/src/Avalonia.Base/Controls/Classes.cs
+++ b/src/Avalonia.Base/Controls/Classes.cs
@@ -1,8 +1,7 @@
using System;
using System.Collections.Generic;
using Avalonia.Collections;
-
-#nullable enable
+using Avalonia.Utilities;
namespace Avalonia.Controls
{
@@ -14,6 +13,8 @@ namespace Avalonia.Controls
///
public class Classes : AvaloniaList, IPseudoClasses
{
+ private SafeEnumerableList? _listeners;
+
///
/// Initializes a new instance of the class.
///
@@ -39,6 +40,11 @@ namespace Avalonia.Controls
{
}
+ ///
+ /// Gets the number of listeners subscribed to this collection for unit testing purposes.
+ ///
+ internal int ListenerCount => _listeners?.Count ?? 0;
+
///
/// Parses a classes string.
///
@@ -62,6 +68,7 @@ namespace Avalonia.Controls
if (!Contains(name))
{
base.Add(name);
+ NotifyChanged();
}
}
@@ -89,6 +96,7 @@ namespace Avalonia.Controls
}
base.AddRange(c);
+ NotifyChanged();
}
///
@@ -103,6 +111,8 @@ namespace Avalonia.Controls
RemoveAt(i);
}
}
+
+ NotifyChanged();
}
///
@@ -122,6 +132,7 @@ namespace Avalonia.Controls
if (!Contains(name))
{
base.Insert(index, name);
+ NotifyChanged();
}
}
@@ -154,6 +165,7 @@ namespace Avalonia.Controls
if (toInsert != null)
{
base.InsertRange(index, toInsert);
+ NotifyChanged();
}
}
@@ -169,7 +181,14 @@ namespace Avalonia.Controls
public override bool Remove(string name)
{
ThrowIfPseudoclass(name, "removed");
- return base.Remove(name);
+
+ if (base.Remove(name))
+ {
+ NotifyChanged();
+ return true;
+ }
+
+ return false;
}
///
@@ -197,6 +216,7 @@ namespace Avalonia.Controls
if (toRemove != null)
{
base.RemoveAll(toRemove);
+ NotifyChanged();
}
}
@@ -214,6 +234,7 @@ namespace Avalonia.Controls
var name = this[index];
ThrowIfPseudoclass(name, "removed");
base.RemoveAt(index);
+ NotifyChanged();
}
///
@@ -224,6 +245,7 @@ namespace Avalonia.Controls
public override void RemoveRange(int index, int count)
{
base.RemoveRange(index, count);
+ NotifyChanged();
}
///
@@ -255,6 +277,7 @@ namespace Avalonia.Controls
}
base.AddRange(source);
+ NotifyChanged();
}
///
@@ -263,13 +286,38 @@ namespace Avalonia.Controls
if (!Contains(name))
{
base.Add(name);
+ NotifyChanged();
}
}
///
bool IPseudoClasses.Remove(string name)
{
- return base.Remove(name);
+ if (base.Remove(name))
+ {
+ NotifyChanged();
+ return true;
+ }
+
+ return false;
+ }
+
+ internal void AddListener(IClassesChangedListener listener)
+ {
+ (_listeners ??= new()).Add(listener);
+ }
+
+ internal void RemoveListener(IClassesChangedListener listener)
+ {
+ _listeners?.Remove(listener);
+ }
+
+ private void NotifyChanged()
+ {
+ if (_listeners is null)
+ return;
+ foreach (var listener in _listeners)
+ listener.Changed();
}
private void ThrowIfPseudoclass(string name, string operation)
diff --git a/src/Avalonia.Base/Controls/IClassesChangedListener.cs b/src/Avalonia.Base/Controls/IClassesChangedListener.cs
new file mode 100644
index 0000000000..b4de893c97
--- /dev/null
+++ b/src/Avalonia.Base/Controls/IClassesChangedListener.cs
@@ -0,0 +1,14 @@
+namespace Avalonia.Controls
+{
+ ///
+ /// Internal interface for listening to changes in in a more
+ /// performant manner than subscribing to CollectionChanged.
+ ///
+ internal interface IClassesChangedListener
+ {
+ ///
+ /// Notifies the listener that the collection has changed.
+ ///
+ void Changed();
+ }
+}
diff --git a/src/Avalonia.Base/Controls/IPseudoClasses.cs b/src/Avalonia.Base/Controls/IPseudoClasses.cs
index eda521727f..438b05a8cf 100644
--- a/src/Avalonia.Base/Controls/IPseudoClasses.cs
+++ b/src/Avalonia.Base/Controls/IPseudoClasses.cs
@@ -19,5 +19,12 @@ namespace Avalonia.Controls
///
/// The pseudoclass name.
bool Remove(string name);
+
+ ///
+ /// Returns whether a pseudoclass is present in the collection.
+ ///
+ /// The pseudoclass name.
+ /// Whether the pseudoclass is present.
+ bool Contains(string name);
}
}
diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
index b93bf87fdf..91d69b5d3d 100644
--- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
+++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
@@ -55,7 +55,7 @@ namespace Avalonia.Data.Core.Plugins
private PropertyInfo? GetFirstPropertyWithName(object instance, string propertyName)
{
- if (instance is IReflectableType reflectableType)
+ if (instance is IReflectableType reflectableType && instance is not Type)
return reflectableType.GetTypeInfo().GetProperty(propertyName, PropertyBindingFlags);
var type = instance.GetType();
diff --git a/src/Avalonia.Base/Input/Cursor.cs b/src/Avalonia.Base/Input/Cursor.cs
index 122838f682..98c4258a90 100644
--- a/src/Avalonia.Base/Input/Cursor.cs
+++ b/src/Avalonia.Base/Input/Cursor.cs
@@ -32,10 +32,7 @@ namespace Avalonia.Input
DragCopy,
DragLink,
None,
-
- [Obsolete("Use BottomSide")]
- BottomSize = BottomSide
-
+
// Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/
// We might enable them later, preferably, by loading pixmax directly from theme with fallback image
// SizeNorthWestSouthEast,
diff --git a/src/Avalonia.Base/Input/DragEventArgs.cs b/src/Avalonia.Base/Input/DragEventArgs.cs
index 22ca8358ff..0e613c0f21 100644
--- a/src/Avalonia.Base/Input/DragEventArgs.cs
+++ b/src/Avalonia.Base/Input/DragEventArgs.cs
@@ -13,9 +13,6 @@ namespace Avalonia.Input
public IDataObject Data { get; private set; }
- [Obsolete("Use KeyModifiers")]
- public InputModifiers Modifiers { get; private set; }
-
public KeyModifiers KeyModifiers { get; private set; }
public Point GetPosition(IVisual relativeTo)
@@ -35,17 +32,6 @@ namespace Avalonia.Input
return point;
}
- [Obsolete("Use constructor taking KeyModifiers")]
- public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, InputModifiers modifiers)
- : base(routedEvent)
- {
- Data = data;
- _target = target;
- _targetLocation = targetLocation;
- Modifiers = modifiers;
- KeyModifiers = (KeyModifiers)(((int)modifiers) & 0xF);
- }
-
public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers)
: base(routedEvent)
{
@@ -53,10 +39,6 @@ namespace Avalonia.Input
_target = target;
_targetLocation = targetLocation;
KeyModifiers = keyModifiers;
-#pragma warning disable CS0618 // Type or member is obsolete
- Modifiers = (InputModifiers)keyModifiers;
-#pragma warning restore CS0618 // Type or member is obsolete
}
-
}
}
diff --git a/src/Avalonia.Base/Input/GotFocusEventArgs.cs b/src/Avalonia.Base/Input/GotFocusEventArgs.cs
index 9d958823fe..5cce138ee0 100644
--- a/src/Avalonia.Base/Input/GotFocusEventArgs.cs
+++ b/src/Avalonia.Base/Input/GotFocusEventArgs.cs
@@ -1,4 +1,3 @@
-using System;
using Avalonia.Interactivity;
namespace Avalonia.Input
@@ -13,16 +12,6 @@ namespace Avalonia.Input
///
public NavigationMethod NavigationMethod { get; set; }
- ///
- /// Gets or sets any input modifiers active at the time of focus.
- ///
- [Obsolete("Use KeyModifiers")]
- public InputModifiers InputModifiers
- {
- get => (InputModifiers)KeyModifiers;
- set => KeyModifiers = (KeyModifiers)((int)value & 0xF);
- }
-
///
/// Gets or sets any key modifiers active at the time of focus.
///
diff --git a/src/Avalonia.Base/Input/IInputRoot.cs b/src/Avalonia.Base/Input/IInputRoot.cs
index 7edc69df52..344a4eefd7 100644
--- a/src/Avalonia.Base/Input/IInputRoot.cs
+++ b/src/Avalonia.Base/Input/IInputRoot.cs
@@ -27,10 +27,5 @@ namespace Avalonia.Input
/// Gets or sets a value indicating whether access keys are shown in the window.
///
bool ShowAccessKeys { get; set; }
-
- ///
- /// Gets associated mouse device
- ///
- IMouseDevice? MouseDevice { get; }
}
}
diff --git a/src/Avalonia.Base/Input/IKeyboardDevice.cs b/src/Avalonia.Base/Input/IKeyboardDevice.cs
index 80aebc02bc..0b7b5aaecc 100644
--- a/src/Avalonia.Base/Input/IKeyboardDevice.cs
+++ b/src/Avalonia.Base/Input/IKeyboardDevice.cs
@@ -4,19 +4,6 @@ using Avalonia.Metadata;
namespace Avalonia.Input
{
- [Flags, Obsolete("Use KeyModifiers and PointerPointProperties")]
- public enum InputModifiers
- {
- None = 0,
- Alt = 1,
- Control = 2,
- Shift = 4,
- Windows = 8,
- LeftMouseButton = 16,
- RightMouseButton = 32,
- MiddleMouseButton = 64
- }
-
[Flags]
public enum KeyModifiers
{
diff --git a/src/Avalonia.Base/Input/IMouseDevice.cs b/src/Avalonia.Base/Input/IMouseDevice.cs
index 2d66397d63..00c436bf21 100644
--- a/src/Avalonia.Base/Input/IMouseDevice.cs
+++ b/src/Avalonia.Base/Input/IMouseDevice.cs
@@ -1,4 +1,3 @@
-using System;
using Avalonia.Metadata;
namespace Avalonia.Input
@@ -9,16 +8,5 @@ namespace Avalonia.Input
[NotClientImplementable]
public interface IMouseDevice : IPointerDevice
{
- ///
- /// Gets the mouse position, in screen coordinates.
- ///
- [Obsolete("Use PointerEventArgs.GetPosition")]
- PixelPoint Position { get; }
-
- [Obsolete]
- void TopLevelClosed(IInputRoot root);
-
- [Obsolete]
- void SceneInvalidated(IInputRoot root, Rect rect);
}
}
diff --git a/src/Avalonia.Base/Input/IPointerDevice.cs b/src/Avalonia.Base/Input/IPointerDevice.cs
index 0993835feb..e0aebda9c5 100644
--- a/src/Avalonia.Base/Input/IPointerDevice.cs
+++ b/src/Avalonia.Base/Input/IPointerDevice.cs
@@ -1,5 +1,3 @@
-using System;
-using Avalonia.VisualTree;
using Avalonia.Input.Raw;
using Avalonia.Metadata;
@@ -8,18 +6,6 @@ namespace Avalonia.Input
[NotClientImplementable]
public interface IPointerDevice : IInputDevice
{
- ///
- [Obsolete("Use IPointer")]
- IInputElement? Captured { get; }
-
- ///
- [Obsolete("Use IPointer")]
- void Capture(IInputElement? control);
-
- ///
- [Obsolete("Use PointerEventArgs.GetPosition")]
- Point GetPosition(IVisual relativeTo);
-
///
/// Gets a pointer for specific event args.
///
diff --git a/src/Avalonia.Base/Input/KeyEventArgs.cs b/src/Avalonia.Base/Input/KeyEventArgs.cs
index 67cd5a520a..b8291e9096 100644
--- a/src/Avalonia.Base/Input/KeyEventArgs.cs
+++ b/src/Avalonia.Base/Input/KeyEventArgs.cs
@@ -9,8 +9,6 @@ namespace Avalonia.Input
public Key Key { get; set; }
- [Obsolete("Use KeyModifiers")]
- public InputModifiers Modifiers => (InputModifiers)KeyModifiers;
public KeyModifiers KeyModifiers { get; set; }
}
}
diff --git a/src/Avalonia.Base/Input/KeyGesture.cs b/src/Avalonia.Base/Input/KeyGesture.cs
index 3b7a828b86..e79e9341a9 100644
--- a/src/Avalonia.Base/Input/KeyGesture.cs
+++ b/src/Avalonia.Base/Input/KeyGesture.cs
@@ -15,13 +15,6 @@ namespace Avalonia.Input
{ "+", Key.OemPlus }, { "-", Key.OemMinus }, { ".", Key.OemPeriod }, { ",", Key.OemComma }
};
- [Obsolete("Use constructor taking KeyModifiers")]
- public KeyGesture(Key key, InputModifiers modifiers)
- {
- Key = key;
- KeyModifiers = (KeyModifiers)(((int)modifiers) & 0xf);
- }
-
public KeyGesture(Key key, KeyModifiers modifiers = KeyModifiers.None)
{
Key = key;
@@ -63,10 +56,7 @@ namespace Avalonia.Input
}
public Key Key { get; }
-
- [Obsolete("Use KeyModifiers")]
- public InputModifiers Modifiers => (InputModifiers)KeyModifiers;
-
+
public KeyModifiers KeyModifiers { get; }
public static KeyGesture Parse(string gesture)
diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs
index 5f8ab24b79..055c9cf1fd 100644
--- a/src/Avalonia.Base/Input/MouseDevice.cs
+++ b/src/Avalonia.Base/Input/MouseDevice.cs
@@ -21,7 +21,6 @@ namespace Avalonia.Input
private readonly Pointer _pointer;
private bool _disposed;
- private PixelPoint? _position;
private MouseButton _lastMouseDownButton;
public MouseDevice(Pointer? pointer = null)
@@ -29,43 +28,6 @@ namespace Avalonia.Input
_pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
}
- [Obsolete("Use IPointer instead")]
- public IInputElement? Captured => _pointer.Captured;
-
- [Obsolete("Use events instead")]
- public PixelPoint Position
- {
- get => _position ?? new PixelPoint(-1, -1);
- protected set => _position = value;
- }
-
- [Obsolete("Use IPointer instead")]
- public void Capture(IInputElement? control)
- {
- _pointer.Capture(control);
- }
-
- ///
- /// Gets the mouse position relative to a control.
- ///
- /// The control.
- /// The mouse position in the control's coordinates.
- public Point GetPosition(IVisual relativeTo)
- {
- relativeTo = relativeTo ?? throw new ArgumentNullException(nameof(relativeTo));
-
- if (relativeTo.VisualRoot == null)
- {
- throw new InvalidOperationException("Control is not attached to visual tree.");
- }
-
-#pragma warning disable CS0618 // Type or member is obsolete
- var rootPoint = relativeTo.VisualRoot.PointToClient(Position);
-#pragma warning restore CS0618 // Type or member is obsolete
- var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo);
- return rootPoint * transform!.Value;
- }
-
public void ProcessRawEvent(RawInputEventArgs e)
{
if (!e.Handled && e is RawPointerEventArgs margs)
@@ -96,7 +58,6 @@ namespace Avalonia.Input
if(mouse._disposed)
return;
- _position = e.Root.PointToScreen(e.Position);
var props = CreateProperties(e);
var keyModifiers = e.InputModifiers.ToKeyModifiers();
switch (e.Type)
@@ -145,7 +106,6 @@ namespace Avalonia.Input
private void LeaveWindow()
{
- _position = null;
}
PointerPointProperties CreateProperties(RawPointerEventArgs args)
@@ -324,19 +284,7 @@ namespace Avalonia.Input
_disposed = true;
_pointer?.Dispose();
}
-
- [Obsolete]
- public void TopLevelClosed(IInputRoot root)
- {
- // no-op
- }
-
- [Obsolete]
- public void SceneInvalidated(IInputRoot root, Rect rect)
- {
- // no-op
- }
-
+
public IPointer? TryGetPointer(RawPointerEventArgs ev)
{
return _pointer;
diff --git a/src/Avalonia.Base/Input/PenDevice.cs b/src/Avalonia.Base/Input/PenDevice.cs
index d22b48562c..f5f0e90a45 100644
--- a/src/Avalonia.Base/Input/PenDevice.cs
+++ b/src/Avalonia.Base/Input/PenDevice.cs
@@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Reactive.Linq;
using Avalonia.Input.Raw;
using Avalonia.Platform;
-using Avalonia.VisualTree;
namespace Avalonia.Input
{
@@ -14,7 +12,6 @@ namespace Avalonia.Input
public class PenDevice : IPenDevice, IDisposable
{
private readonly Dictionary _pointers = new();
- private readonly Dictionary _lastPositions = new();
private int _clickCount;
private Rect _lastClickRect;
private ulong _lastClickTime;
@@ -41,9 +38,7 @@ namespace Avalonia.Input
_pointers[e.RawPointerId] = pointer = new Pointer(Pointer.GetNextFreeId(),
PointerType.Pen, _pointers.Count == 0);
}
-
- _lastPositions[e.RawPointerId] = e.Root.PointToScreen(e.Position);
-
+
var props = new PointerPointProperties(e.InputModifiers, e.Type.ToUpdateKind(),
e.Point.Twist, e.Point.Pressure, e.Point.XTilt, e.Point.YTilt);
var keyModifiers = e.InputModifiers.ToKeyModifiers();
@@ -69,7 +64,6 @@ namespace Avalonia.Input
{
pointer.Dispose();
_pointers.Remove(e.RawPointerId);
- _lastPositions.Remove(e.RawPointerId);
}
}
@@ -153,17 +147,6 @@ namespace Avalonia.Input
p.Dispose();
}
- [Obsolete]
- IInputElement? IPointerDevice.Captured => _pointers.Values
- .FirstOrDefault(p => p.IsPrimary)?.Captured;
-
- [Obsolete]
- void IPointerDevice.Capture(IInputElement? control) => _pointers.Values
- .FirstOrDefault(p => p.IsPrimary)?.Capture(control);
-
- [Obsolete]
- Point IPointerDevice.GetPosition(IVisual relativeTo) => new Point(-1, -1);
-
public IPointer? TryGetPointer(RawPointerEventArgs ev)
{
return _pointers.TryGetValue(ev.RawPointerId, out var pointer)
diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs
index 058c2f9cc1..1f3c726e7b 100644
--- a/src/Avalonia.Base/Input/PointerEventArgs.cs
+++ b/src/Avalonia.Base/Input/PointerEventArgs.cs
@@ -11,7 +11,7 @@ namespace Avalonia.Input
private readonly IVisual? _rootVisual;
private readonly Point _rootVisualPosition;
private readonly PointerPointProperties _properties;
- private Lazy?>? _previousPoints;
+ private readonly Lazy?>? _previousPoints;
public PointerEventArgs(RoutedEvent routedEvent,
IInteractive? source,
@@ -43,29 +43,6 @@ namespace Avalonia.Input
{
_previousPoints = previousPoints;
}
-
-
- class EmulatedDevice : IPointerDevice
- {
- private readonly PointerEventArgs _ev;
-
- public EmulatedDevice(PointerEventArgs ev)
- {
- _ev = ev;
- }
-
- public void ProcessRawEvent(RawInputEventArgs ev) => throw new NotSupportedException();
-
- public IInputElement? Captured => _ev.Pointer.Captured;
- public void Capture(IInputElement? control)
- {
- _ev.Pointer.Capture(control);
- }
-
- public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo);
-
- public IPointer? TryGetPointer(RawPointerEventArgs ev) => _ev.Pointer;
- }
///
/// Gets specific pointer generated by input device.
@@ -77,28 +54,6 @@ namespace Avalonia.Input
///
public ulong Timestamp { get; }
- private IPointerDevice? _device;
-
- [Obsolete("Use Pointer to get pointer-specific information")]
- public IPointerDevice Device => _device ?? (_device = new EmulatedDevice(this));
-
- [Obsolete("Use KeyModifiers and PointerPointProperties")]
- public InputModifiers InputModifiers
- {
- get
- {
- var mods = (InputModifiers)KeyModifiers;
- if (_properties.IsLeftButtonPressed)
- mods |= InputModifiers.LeftMouseButton;
- if (_properties.IsMiddleButtonPressed)
- mods |= InputModifiers.MiddleMouseButton;
- if (_properties.IsRightButtonPressed)
- mods |= InputModifiers.RightMouseButton;
-
- return mods;
- }
- }
-
///
/// Gets a value that indicates which key modifiers were active at the time that the pointer event was initiated.
///
@@ -120,9 +75,6 @@ namespace Avalonia.Input
/// The pointer position in the control's coordinates.
public Point GetPosition(IVisual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo);
- [Obsolete("Use GetCurrentPoint")]
- public PointerPoint GetPointerPoint(IVisual? relativeTo) => GetCurrentPoint(relativeTo);
-
///
/// Returns the PointerPoint associated with the current event
///
@@ -171,8 +123,6 @@ namespace Avalonia.Input
public class PointerPressedEventArgs : PointerEventArgs
{
- private readonly int _clickCount;
-
public PointerPressedEventArgs(
IInteractive source,
IPointer pointer,
@@ -184,13 +134,10 @@ namespace Avalonia.Input
: base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition,
timestamp, properties, modifiers)
{
- _clickCount = clickCount;
+ ClickCount = clickCount;
}
- public int ClickCount => _clickCount;
-
- [Obsolete("Use PointerPressedEventArgs.GetCurrentPoint(this).Properties")]
- public MouseButton MouseButton => Properties.PointerUpdateKind.GetMouseButton();
+ public int ClickCount { get; }
}
public class PointerReleasedEventArgs : PointerEventArgs
@@ -210,9 +157,6 @@ namespace Avalonia.Input
/// Gets the mouse button that triggered the corresponding PointerPressed event
///
public MouseButton InitialPressMouseButton { get; }
-
- [Obsolete("Use InitialPressMouseButton")]
- public MouseButton MouseButton => InitialPressMouseButton;
}
public class PointerCaptureLostEventArgs : RoutedEventArgs
diff --git a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs
index 67d1eea7e3..a95d66346a 100644
--- a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs
+++ b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs
@@ -15,6 +15,8 @@ namespace Avalonia.Input
_inputRoot = inputRoot ?? throw new ArgumentNullException(nameof(inputRoot));
}
+ public PixelPoint? LastPosition => _lastPointer?.position;
+
public void OnCompleted()
{
ClearPointerOver();
diff --git a/src/Avalonia.Base/Input/Raw/RawDragEvent.cs b/src/Avalonia.Base/Input/Raw/RawDragEvent.cs
index 652bad7115..c903aad684 100644
--- a/src/Avalonia.Base/Input/Raw/RawDragEvent.cs
+++ b/src/Avalonia.Base/Input/Raw/RawDragEvent.cs
@@ -8,8 +8,6 @@ namespace Avalonia.Input.Raw
public IDataObject Data { get; }
public DragDropEffects Effects { get; set; }
public RawDragEventType Type { get; }
- [Obsolete("Use KeyModifiers")]
- public InputModifiers Modifiers { get; }
public KeyModifiers KeyModifiers { get; }
public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type,
@@ -21,9 +19,6 @@ namespace Avalonia.Input.Raw
Data = data;
Effects = effects;
KeyModifiers = modifiers.ToKeyModifiers();
-#pragma warning disable CS0618 // Type or member is obsolete
- Modifiers = (InputModifiers)modifiers;
-#pragma warning restore CS0618 // Type or member is obsolete
}
}
}
diff --git a/src/Avalonia.Base/Input/Raw/RawTouchEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawTouchEventArgs.cs
index 6706a45f48..3e842e6e3f 100644
--- a/src/Avalonia.Base/Input/Raw/RawTouchEventArgs.cs
+++ b/src/Avalonia.Base/Input/Raw/RawTouchEventArgs.cs
@@ -19,8 +19,5 @@ namespace Avalonia.Input.Raw
{
RawPointerId = rawPointerId;
}
-
- [Obsolete("Use RawPointerId")]
- public long TouchPointId { get => RawPointerId; set => RawPointerId = value; }
}
}
diff --git a/src/Avalonia.Base/Input/TouchDevice.cs b/src/Avalonia.Base/Input/TouchDevice.cs
index e914d860fd..1d5b1d6bbf 100644
--- a/src/Avalonia.Base/Input/TouchDevice.cs
+++ b/src/Avalonia.Base/Input/TouchDevice.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Linq;
using Avalonia.Input.Raw;
using Avalonia.Platform;
-using Avalonia.VisualTree;
namespace Avalonia.Input
{
@@ -20,9 +19,6 @@ namespace Avalonia.Input
private int _clickCount;
private Rect _lastClickRect;
private ulong _lastClickTime;
- private Pointer? _lastPointer;
-
- IInputElement? IPointerDevice.Captured => _lastPointer?.Captured;
RawInputModifiers GetModifiers(RawInputModifiers modifiers, bool isLeftButtonDown)
{
@@ -32,10 +28,6 @@ namespace Avalonia.Input
return rv;
}
- void IPointerDevice.Capture(IInputElement? control) => _lastPointer?.Capture(control);
-
- Point IPointerDevice.GetPosition(IVisual relativeTo) => default;
-
public void ProcessRawEvent(RawInputEventArgs ev)
{
if (ev.Handled || _disposed)
@@ -51,7 +43,6 @@ namespace Avalonia.Input
PointerType.Touch, _pointers.Count == 0);
pointer.Capture(hit);
}
- _lastPointer = pointer;
var target = pointer.Captured ?? args.Root;
var updateKind = args.Type.ToUpdateKind();
@@ -96,7 +87,6 @@ namespace Avalonia.Input
new PointerPointProperties(GetModifiers(args.InputModifiers, false), updateKind),
keyModifier, MouseButton.Left));
}
- _lastPointer = null;
}
if (args.Type == RawPointerEventType.TouchCancel)
@@ -104,7 +94,6 @@ namespace Avalonia.Input
_pointers.Remove(args.RawPointerId);
using (pointer)
pointer.Capture(null);
- _lastPointer = null;
}
if (args.Type == RawPointerEventType.TouchUpdate)
diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs
index 446f135c83..b9ca6bfbd7 100644
--- a/src/Avalonia.Base/Layout/LayoutManager.cs
+++ b/src/Avalonia.Base/Layout/LayoutManager.cs
@@ -350,7 +350,7 @@ namespace Avalonia.Layout
{
for (var i = 0; i < count; ++i)
{
- var l = _effectiveViewportChangedListeners[i];
+ var l = listeners[i];
if (!l.Listener.IsAttachedToVisualTree)
{
@@ -362,7 +362,7 @@ namespace Avalonia.Layout
if (viewport != l.Viewport)
{
l.Listener.EffectiveViewportChanged(new EffectiveViewportChangedEventArgs(viewport));
- _effectiveViewportChangedListeners[i] = new EffectiveViewportChangedListener(l.Listener, viewport);
+ l.Viewport = viewport;
}
}
}
@@ -414,7 +414,7 @@ namespace Avalonia.Layout
}
}
- private readonly struct EffectiveViewportChangedListener
+ private class EffectiveViewportChangedListener
{
public EffectiveViewportChangedListener(ILayoutable listener, Rect viewport)
{
@@ -423,7 +423,7 @@ namespace Avalonia.Layout
}
public ILayoutable Listener { get; }
- public Rect Viewport { get; }
+ public Rect Viewport { get; set; }
}
}
}
diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs
index c049f9e763..98ef6d2530 100644
--- a/src/Avalonia.Base/Logging/LogArea.cs
+++ b/src/Avalonia.Base/Logging/LogArea.cs
@@ -44,5 +44,15 @@ namespace Avalonia.Logging
/// The log event comes from X11Platform.
///
public const string X11Platform = nameof(X11Platform);
+
+ ///
+ /// The log event comes from AndroidPlatform.
+ ///
+ public const string AndroidPlatform = nameof(AndroidPlatform);
+
+ ///
+ /// The log event comes from IOSPlatform.
+ ///
+ public const string IOSPlatform = nameof(IOSPlatform);
}
}
diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs
index ac87d521a5..2a7f3360ad 100644
--- a/src/Avalonia.Base/Media/GlyphRun.cs
+++ b/src/Avalonia.Base/Media/GlyphRun.cs
@@ -445,7 +445,7 @@ namespace Avalonia.Media
///
public int FindGlyphIndex(int characterIndex)
{
- if (GlyphClusters == null)
+ if (GlyphClusters == null || GlyphClusters.Count == 0)
{
return characterIndex;
}
@@ -614,17 +614,29 @@ namespace Avalonia.Media
private GlyphRunMetrics CreateGlyphRunMetrics()
{
+ var firstCluster = 0;
+ var lastCluster = Characters.Length - 1;
+
+ if (!IsLeftToRight)
+ {
+ var cluster = firstCluster;
+ firstCluster = lastCluster;
+ lastCluster = cluster;
+ }
+
if (GlyphClusters != null && GlyphClusters.Count > 0)
{
- var firstCluster = GlyphClusters[0];
+ firstCluster = GlyphClusters[0];
+ lastCluster = GlyphClusters[GlyphClusters.Count - 1];
_offsetToFirstCharacter = Math.Max(0, Characters.Start - firstCluster);
}
+ var isReversed = firstCluster > lastCluster;
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
var widthIncludingTrailingWhitespace = 0d;
- var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength, out var glyphCount);
+ var trailingWhitespaceLength = GetTrailingWhitespaceLength(isReversed, out var newLineLength, out var glyphCount);
for (var index = 0; index < GlyphIndices.Count; index++)
{
@@ -635,16 +647,16 @@ namespace Avalonia.Media
var width = widthIncludingTrailingWhitespace;
- if (IsLeftToRight)
+ if (isReversed)
{
- for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++)
+ for (var index = 0; index < glyphCount; index++)
{
width -= GetGlyphAdvance(index, out _);
}
}
else
{
- for (var index = 0; index < glyphCount; index++)
+ for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++)
{
width -= GetGlyphAdvance(index, out _);
}
@@ -654,16 +666,15 @@ namespace Avalonia.Media
height);
}
- private int GetTrailingWhitespaceLength(out int newLineLength, out int glyphCount)
- {
- glyphCount = 0;
- newLineLength = 0;
-
- if (Characters.IsEmpty)
+ private int GetTrailingWhitespaceLength(bool isReversed, out int newLineLength, out int glyphCount)
+ {
+ if (isReversed)
{
- return 0;
+ return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount);
}
+ glyphCount = 0;
+ newLineLength = 0;
var trailingWhitespaceLength = 0;
if (GlyphClusters == null)
@@ -732,6 +743,78 @@ namespace Avalonia.Media
return trailingWhitespaceLength;
}
+ private int GetTralingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount)
+ {
+ glyphCount = 0;
+ newLineLength = 0;
+ var trailingWhitespaceLength = 0;
+
+ if (GlyphClusters == null)
+ {
+ for (var i = 0; i < Characters.Length;)
+ {
+ var codepoint = Codepoint.ReadAt(_characters, i, out var count);
+
+ if (!codepoint.IsWhiteSpace)
+ {
+ break;
+ }
+
+ if (codepoint.IsBreakChar)
+ {
+ newLineLength++;
+ }
+
+ trailingWhitespaceLength++;
+
+ i += count;
+ glyphCount++;
+ }
+ }
+ else
+ {
+ for (var i = 0; i < GlyphClusters.Count; i++)
+ {
+ var currentCluster = GlyphClusters[i];
+ var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset);
+ var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _);
+
+ if (!codepoint.IsWhiteSpace)
+ {
+ break;
+ }
+
+ var clusterLength = 1;
+
+ while (i - 1 >= 0)
+ {
+ var nextCluster = GlyphClusters[i - 1];
+
+ if (currentCluster == nextCluster)
+ {
+ clusterLength++;
+ i--;
+
+ continue;
+ }
+
+ break;
+ }
+
+ if (codepoint.IsBreakChar)
+ {
+ newLineLength += clusterLength;
+ }
+
+ trailingWhitespaceLength += clusterLength;
+
+ glyphCount++;
+ }
+ }
+
+ return trailingWhitespaceLength;
+ }
+
private void Set(ref T field, T value)
{
_glyphRunImpl?.Dispose();
diff --git a/src/Avalonia.Base/Media/TextDecoration.cs b/src/Avalonia.Base/Media/TextDecoration.cs
index 8eeb86c555..4c9764af96 100644
--- a/src/Avalonia.Base/Media/TextDecoration.cs
+++ b/src/Avalonia.Base/Media/TextDecoration.cs
@@ -209,7 +209,7 @@ namespace Avalonia.Media
var pen = new Pen(Stroke ?? defaultBrush, thickness,
new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeLineCap);
- drawingContext.DrawLine(pen, origin, origin + new Point(glyphRun.Size.Width, 0));
+ drawingContext.DrawLine(pen, origin, origin + new Point(glyphRun.Metrics.Width, 0));
}
}
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
index f3af240c58..f3e8b5969c 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
@@ -63,7 +63,7 @@ namespace Avalonia.Media.TextFormatting
MaxHeight = maxHeight;
- MaxLines = maxLines;
+ MaxLines = maxLines;
TextLines = CreateTextLines();
}
@@ -80,7 +80,7 @@ namespace Avalonia.Media.TextFormatting
/// The maximum number of text lines.
public TextLayout(
ITextSource textSource,
- TextParagraphProperties paragraphProperties,
+ TextParagraphProperties paragraphProperties,
TextTrimming? textTrimming = null,
double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity,
@@ -178,24 +178,18 @@ namespace Avalonia.Media.TextFormatting
return new Rect();
}
- if (textPosition < 0 || textPosition >= _textSourceLength)
+ if (textPosition < 0)
{
- var lastLine = TextLines[TextLines.Count - 1];
-
- var lineX = lastLine.Width;
-
- var lineY = Bounds.Bottom - lastLine.Height;
-
- return new Rect(lineX, lineY, 0, lastLine.Height);
+ textPosition = _textSourceLength;
}
var currentY = 0.0;
foreach (var textLine in TextLines)
{
- var end = textLine.FirstTextSourceIndex + textLine.Length - 1;
+ var end = textLine.FirstTextSourceIndex + textLine.Length;
- if (end < textPosition)
+ if (end <= textPosition && end < _textSourceLength)
{
currentY += textLine.Height;
@@ -224,7 +218,7 @@ namespace Avalonia.Media.TextFormatting
}
var result = new List(TextLines.Count);
-
+
var currentY = 0d;
foreach (var textLine in TextLines)
@@ -239,7 +233,7 @@ namespace Avalonia.Media.TextFormatting
var textBounds = textLine.GetTextBounds(start, length);
- if(textBounds.Count > 0)
+ if (textBounds.Count > 0)
{
foreach (var bounds in textBounds)
{
@@ -262,7 +256,7 @@ namespace Avalonia.Media.TextFormatting
}
}
- if(textLine.FirstTextSourceIndex + textLine.Length >= start + length)
+ if (textLine.FirstTextSourceIndex + textLine.Length >= start + length)
{
break;
}
@@ -305,7 +299,7 @@ namespace Avalonia.Media.TextFormatting
return GetHitTestResult(currentLine, characterHit, point);
}
-
+
public int GetLineIndexFromCharacterIndex(int charIndex, bool trailingEdge)
{
if (charIndex < 0)
@@ -327,7 +321,7 @@ namespace Avalonia.Media.TextFormatting
continue;
}
- if (charIndex >= textLine.FirstTextSourceIndex &&
+ if (charIndex >= textLine.FirstTextSourceIndex &&
charIndex <= textLine.FirstTextSourceIndex + textLine.Length - (trailingEdge ? 0 : 1))
{
return index;
@@ -398,7 +392,7 @@ namespace Avalonia.Media.TextFormatting
/// The current left.
/// The current width.
/// The current height.
- private static void UpdateBounds(TextLine textLine,ref double left, ref double width, ref double height)
+ private static void UpdateBounds(TextLine textLine, ref double left, ref double width, ref double height)
{
var lineWidth = textLine.WidthIncludingTrailingWhitespace;
@@ -421,7 +415,7 @@ namespace Avalonia.Media.TextFormatting
{
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties);
- Bounds = new Rect(0,0,0, textLine.Height);
+ Bounds = new Rect(0, 0, 0, textLine.Height);
return new List { textLine };
}
@@ -439,9 +433,9 @@ namespace Avalonia.Media.TextFormatting
var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak);
- if(textLine == null || textLine.Length == 0 || textLine.TextRuns.Count == 0 && textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
+ if (textLine == null || textLine.Length == 0 || textLine.TextRuns.Count == 0 && textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
{
- if(previousLine != null && previousLine.NewLineLength > 0)
+ if (previousLine != null && previousLine.NewLineLength > 0)
{
var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, _paragraphProperties);
@@ -454,7 +448,7 @@ namespace Avalonia.Media.TextFormatting
}
_textSourceLength += textLine.Length;
-
+
//Fulfill max height constraint
if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight)
{
@@ -485,12 +479,17 @@ namespace Avalonia.Media.TextFormatting
//Fulfill max lines constraint
if (MaxLines > 0 && textLines.Count >= MaxLines)
{
+ if(textLine.TextLineBreak is TextLineBreak lineBreak && lineBreak.RemainingRuns != null)
+ {
+ textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width));
+ }
+
break;
}
}
//Make sure the TextLayout always contains at least on empty line
- if(textLines.Count == 0)
+ if (textLines.Count == 0)
{
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties);
@@ -501,7 +500,7 @@ namespace Avalonia.Media.TextFormatting
Bounds = new Rect(left, 0, width, height);
- if(_paragraphProperties.TextAlignment == TextAlignment.Justify)
+ if (_paragraphProperties.TextAlignment == TextAlignment.Justify)
{
var whitespaceWidth = 0d;
@@ -509,7 +508,7 @@ namespace Avalonia.Media.TextFormatting
{
var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace;
- if(lineWhitespaceWidth > whitespaceWidth)
+ if (lineWhitespaceWidth > whitespaceWidth)
{
whitespaceWidth = lineWhitespaceWidth;
}
@@ -517,7 +516,7 @@ namespace Avalonia.Media.TextFormatting
var justificationWidth = width - whitespaceWidth;
- if(justificationWidth > 0)
+ if (justificationWidth > 0)
{
var justificationProperties = new InterWordJustification(justificationWidth);
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
index 7c686358e2..f3c62f4994 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
@@ -166,58 +166,74 @@ namespace Avalonia.Media.TextFormatting
if (distance <= 0)
{
- // hit happens before the line, return the first position
var firstRun = _textRuns[0];
- if (firstRun is ShapedTextCharacters shapedTextCharacters)
- {
- return shapedTextCharacters.GlyphRun.GetCharacterHitFromDistance(distance, out _);
- }
+ return GetRunCharacterHit(firstRun, FirstTextSourceIndex, 0);
+ }
- return _resolvedFlowDirection == FlowDirection.LeftToRight ?
- new CharacterHit(FirstTextSourceIndex) :
- new CharacterHit(FirstTextSourceIndex + Length);
+ if (distance > WidthIncludingTrailingWhitespace)
+ {
+ var lastRun = _textRuns[_textRuns.Count - 1];
+
+ return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.TextSourceLength, lastRun.Size.Width);
}
// process hit that happens within the line
var characterHit = new CharacterHit();
var currentPosition = FirstTextSourceIndex;
+ var currentDistance = 0.0;
foreach (var currentRun in _textRuns)
{
- switch (currentRun)
+ if (currentDistance + currentRun.Size.Width < distance)
{
- case ShapedTextCharacters shapedRun:
- {
- characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
+ currentDistance += currentRun.Size.Width;
+ currentPosition += currentRun.TextSourceLength;
- var offset = Math.Max(0, currentPosition - shapedRun.Text.Start);
+ continue;
+ }
- characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength);
+ characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
- break;
- }
- default:
+ break;
+ }
+
+ return characterHit;
+ }
+
+ private static CharacterHit GetRunCharacterHit(DrawableTextRun run, int currentPosition, double distance)
+ {
+ CharacterHit characterHit;
+
+ switch (run)
+ {
+ case ShapedTextCharacters shapedRun:
+ {
+ characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
+
+ var offset = Math.Max(0, currentPosition - shapedRun.Text.Start);
+
+ if (!shapedRun.GlyphRun.IsLeftToRight)
{
- if (distance < currentRun.Size.Width / 2)
- {
- characterHit = new CharacterHit(currentPosition);
- }
- else
- {
- characterHit = new CharacterHit(currentPosition, currentRun.TextSourceLength);
- }
- break;
+ offset = Math.Max(0, offset - shapedRun.Text.End);
}
- }
- if (distance <= currentRun.Size.Width)
- {
- break;
- }
+ characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength);
- distance -= currentRun.Size.Width;
- currentPosition += currentRun.TextSourceLength;
+ break;
+ }
+ default:
+ {
+ if (distance < run.Size.Width / 2)
+ {
+ characterHit = new CharacterHit(currentPosition);
+ }
+ else
+ {
+ characterHit = new CharacterHit(currentPosition, run.TextSourceLength);
+ }
+ break;
+ }
}
return characterHit;
@@ -226,136 +242,122 @@ namespace Avalonia.Media.TextFormatting
///
public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
- var isTrailingHit = characterHit.TrailingLength > 0;
+ var flowDirection = _paragraphProperties.FlowDirection;
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
- var currentDistance = Start;
var currentPosition = FirstTextSourceIndex;
var remainingLength = characterIndex - FirstTextSourceIndex;
- GlyphRun? lastRun = null;
+ var currentDistance = Start;
- for (var index = 0; index < _textRuns.Count; index++)
+ if (flowDirection == FlowDirection.LeftToRight)
+ {
+ for (var index = 0; index < _textRuns.Count; index++)
+ {
+ var currentRun = _textRuns[index];
+
+ if (TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength,
+ flowDirection, out var distance, out _))
+ {
+ return currentDistance + distance;
+ }
+
+ //No hit hit found so we add the full width
+ currentDistance += currentRun.Size.Width;
+ currentPosition += currentRun.TextSourceLength;
+ remainingLength -= currentRun.TextSourceLength;
+ }
+ }
+ else
{
- var textRun = _textRuns[index];
+ currentDistance += WidthIncludingTrailingWhitespace;
- switch (textRun)
+ for (var index = _textRuns.Count - 1; index >= 0; index--)
{
- case ShapedTextCharacters shapedTextCharacters:
- {
- var currentRun = shapedTextCharacters.GlyphRun;
+ var currentRun = _textRuns[index];
- if (lastRun != null)
- {
- if (!lastRun.IsLeftToRight && currentRun.IsLeftToRight &&
- currentRun.Characters.Start == characterHit.FirstCharacterIndex &&
- characterHit.TrailingLength == 0)
- {
- return currentDistance;
- }
- }
+ if (TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength,
+ flowDirection, out var distance, out var currentGlyphRun))
+ {
+ if (currentGlyphRun != null)
+ {
+ distance = currentGlyphRun.Size.Width - distance;
+ }
- //Look for a hit in within the current run
- if (currentPosition + remainingLength <= currentPosition + textRun.Text.Length)
- {
- characterHit = new CharacterHit(textRun.Text.Start + remainingLength);
+ return currentDistance - distance;
+ }
- var distance = currentRun.GetDistanceFromCharacterHit(characterHit);
+ //No hit hit found so we add the full width
+ currentDistance -= currentRun.Size.Width;
+ currentPosition += currentRun.TextSourceLength;
+ remainingLength -= currentRun.TextSourceLength;
+ }
+ }
- return currentDistance + distance;
- }
+ return currentDistance;
+ }
- //Look at the left and right edge of the current run
- if (currentRun.IsLeftToRight)
- {
- if (_resolvedFlowDirection == FlowDirection.LeftToRight && (lastRun == null || lastRun.IsLeftToRight))
- {
- if (characterIndex <= currentPosition)
- {
- return currentDistance;
- }
- }
- else
- {
- if (characterIndex == currentPosition)
- {
- return currentDistance;
- }
- }
+ private static bool TryGetDistanceFromCharacterHit(
+ DrawableTextRun currentRun,
+ CharacterHit characterHit,
+ int currentPosition,
+ int remainingLength,
+ FlowDirection flowDirection,
+ out double distance,
+ out GlyphRun? currentGlyphRun)
+ {
+ var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+ var isTrailingHit = characterHit.TrailingLength > 0;
- if (characterIndex == currentPosition + textRun.Text.Length && isTrailingHit)
- {
- return currentDistance + currentRun.Size.Width;
- }
- }
- else
- {
- if (characterIndex == currentPosition)
- {
- return currentDistance + currentRun.Size.Width;
- }
+ distance = 0;
+ currentGlyphRun = null;
- var nextRun = index + 1 < _textRuns.Count ?
- _textRuns[index + 1] as ShapedTextCharacters :
- null;
+ switch (currentRun)
+ {
+ case ShapedTextCharacters shapedTextCharacters:
+ {
+ currentGlyphRun = shapedTextCharacters.GlyphRun;
- if (nextRun != null)
- {
- if (nextRun.ShapedBuffer.IsLeftToRight)
- {
- if (characterIndex == currentPosition + textRun.Text.Length)
- {
- return currentDistance;
- }
- }
- else
- {
- if (currentPosition + nextRun.Text.Length == characterIndex)
- {
- return currentDistance;
- }
- }
- }
- else
- {
- if (characterIndex > currentPosition + textRun.Text.Length)
- {
- return currentDistance;
- }
- }
- }
+ if (currentPosition + remainingLength <= currentPosition + currentRun.Text.Length)
+ {
+ characterHit = new CharacterHit(currentRun.Text.Start + remainingLength);
- lastRun = currentRun;
+ distance = currentGlyphRun.GetDistanceFromCharacterHit(characterHit);
- break;
+ return true;
}
- default:
+
+ if (currentPosition + remainingLength == currentPosition + currentRun.Text.Length && isTrailingHit)
{
- if (characterIndex == currentPosition)
+ if (currentGlyphRun.IsLeftToRight || flowDirection == FlowDirection.RightToLeft)
{
- return currentDistance;
+ distance = currentGlyphRun.Size.Width;
}
- if (characterIndex == currentPosition + textRun.TextSourceLength)
- {
- return currentDistance + textRun.Size.Width;
- }
+ return true;
+ }
- break;
+ break;
+ }
+ default:
+ {
+ if (characterIndex == currentPosition)
+ {
+ return true;
}
- }
- //No hit hit found so we add the full width
- currentDistance += textRun.Size.Width;
- currentPosition += textRun.TextSourceLength;
- remainingLength -= textRun.TextSourceLength;
+ if (characterIndex == currentPosition + currentRun.TextSourceLength)
+ {
+ distance = currentRun.Size.Width;
- if (remainingLength <= 0)
- {
- break;
- }
+ return true;
+
+ }
+
+ break;
+ }
}
- return currentDistance;
+ return false;
}
///
@@ -460,20 +462,33 @@ namespace Avalonia.Media.TextFormatting
var startIndex = currentRun.Text.Start + offset;
- var endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(
- currentShapedRun.ShapedBuffer.IsLeftToRight ?
- new CharacterHit(startIndex + remainingLength) :
- new CharacterHit(startIndex));
+ double startOffset;
+ double endOffset;
- endX += endOffset;
+ if (currentShapedRun.ShapedBuffer.IsLeftToRight)
+ {
+ startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+
+ endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+ }
+ else
+ {
+ endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
- var startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(
- currentShapedRun.ShapedBuffer.IsLeftToRight ?
- new CharacterHit(startIndex) :
- new CharacterHit(startIndex + remainingLength));
+ if (currentPosition < startIndex)
+ {
+ startOffset = endOffset;
+ }
+ else
+ {
+ startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+ }
+ }
startX += startOffset;
+ endX += endOffset;
+
var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
@@ -504,47 +519,40 @@ namespace Avalonia.Media.TextFormatting
}
//Lines that only contain a linebreak need to be covered here
- if(characterLength == 0)
+ if (characterLength == 0)
{
characterLength = NewLineLength;
}
- var runwidth = endX - startX;
- var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runwidth, Height), currentPosition, characterLength, currentRun);
+ var runWidth = endX - startX;
+ var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
- if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
+ if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0)
{
- currentRect = currentRect.WithWidth(currentWidth + runwidth);
+ if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
+ {
+ currentRect = currentRect.WithWidth(currentWidth + runWidth);
- var textBounds = result[result.Count - 1];
+ var textBounds = result[result.Count - 1];
- textBounds.Rectangle = currentRect;
+ textBounds.Rectangle = currentRect;
- textBounds.TextRunBounds.Add(currentRunBounds);
- }
- else
- {
- currentRect = currentRunBounds.Rectangle;
+ textBounds.TextRunBounds.Add(currentRunBounds);
+ }
+ else
+ {
+ currentRect = currentRunBounds.Rectangle;
- result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds }));
+ result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds }));
+ }
}
- currentWidth += runwidth;
+ currentWidth += runWidth;
currentPosition += characterLength;
- if (currentDirection == FlowDirection.LeftToRight)
- {
- if (currentPosition > characterIndex)
- {
- break;
- }
- }
- else
+ if (currentPosition > characterIndex)
{
- if (currentPosition <= firstTextSourceIndex)
- {
- break;
- }
+ break;
}
startX = endX;
@@ -571,7 +579,7 @@ namespace Avalonia.Media.TextFormatting
var currentPosition = FirstTextSourceIndex;
var remainingLength = textLength;
- var startX = Start + WidthIncludingTrailingWhitespace;
+ var startX = WidthIncludingTrailingWhitespace;
double currentWidth = 0;
var currentRect = Rect.Empty;
@@ -582,7 +590,7 @@ namespace Avalonia.Media.TextFormatting
continue;
}
- if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
+ if (currentPosition + currentRun.TextSourceLength < firstTextSourceIndex)
{
startX -= currentRun.Size.Width;
@@ -601,20 +609,31 @@ namespace Avalonia.Media.TextFormatting
currentPosition += offset;
var startIndex = currentRun.Text.Start + offset;
+ double startOffset;
+ double endOffset;
- var endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(
- currentShapedRun.ShapedBuffer.IsLeftToRight ?
- new CharacterHit(startIndex + remainingLength) :
- new CharacterHit(startIndex));
+ if (currentShapedRun.ShapedBuffer.IsLeftToRight)
+ {
+ if (currentPosition < startIndex)
+ {
+ startOffset = endOffset = 0;
+ }
+ else
+ {
+ endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
- endX += endOffset - currentShapedRun.Size.Width;
+ startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+ }
+ }
+ else
+ {
+ endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
- var startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(
- currentShapedRun.ShapedBuffer.IsLeftToRight ?
- new CharacterHit(startIndex) :
- new CharacterHit(startIndex + remainingLength));
+ startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+ }
- startX += startOffset - currentShapedRun.Size.Width;
+ startX -= currentRun.Size.Width - startOffset;
+ endX -= currentRun.Size.Width - endOffset;
var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
@@ -652,41 +671,35 @@ namespace Avalonia.Media.TextFormatting
}
var runWidth = endX - startX;
- var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
- if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
+ var currentRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
+
+ if(!MathUtilities.IsZero(runWidth) || NewLineLength > 0)
{
- currentRect = currentRect.WithWidth(currentWidth + runWidth);
+ if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX))
+ {
+ currentRect = currentRect.WithWidth(currentWidth + runWidth);
- var textBounds = result[result.Count - 1];
+ var textBounds = result[result.Count - 1];
- textBounds.Rectangle = currentRect;
+ textBounds.Rectangle = currentRect;
- textBounds.TextRunBounds.Add(currentRunBounds);
- }
- else
- {
- currentRect = currentRunBounds.Rectangle;
+ textBounds.TextRunBounds.Add(currentRunBounds);
+ }
+ else
+ {
+ currentRect = currentRunBounds.Rectangle;
- result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds }));
- }
+ result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds }));
+ }
+ }
currentWidth += runWidth;
currentPosition += characterLength;
- if (currentDirection == FlowDirection.LeftToRight)
- {
- if (currentPosition > characterIndex)
- {
- break;
- }
- }
- else
+ if (currentPosition > characterIndex)
{
- if (currentPosition <= firstTextSourceIndex)
- {
- break;
- }
+ break;
}
lastDirection = currentDirection;
@@ -698,6 +711,8 @@ namespace Avalonia.Media.TextFormatting
}
}
+ result.Reverse();
+
return result;
}
@@ -1302,8 +1317,14 @@ namespace Avalonia.Media.TextFormatting
switch (textAlignment)
{
case TextAlignment.Center:
- return Math.Max(0, (_paragraphWidth - width) / 2);
+ var start = (_paragraphWidth - width) / 2;
+
+ if(paragraphFlowDirection == FlowDirection.RightToLeft)
+ {
+ start -= (widthIncludingTrailingWhitespace - width);
+ }
+ return Math.Max(0, start);
case TextAlignment.Right:
return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace);
diff --git a/src/Markup/Avalonia.Markup.Xaml/IAddChild.cs b/src/Avalonia.Base/Metadata/IAddChild.cs
similarity index 61%
rename from src/Markup/Avalonia.Markup.Xaml/IAddChild.cs
rename to src/Avalonia.Base/Metadata/IAddChild.cs
index 993eb6142d..8ef02912a6 100644
--- a/src/Markup/Avalonia.Markup.Xaml/IAddChild.cs
+++ b/src/Avalonia.Base/Metadata/IAddChild.cs
@@ -1,11 +1,11 @@
-namespace Avalonia.Markup.Xaml
+namespace Avalonia.Metadata
{
public interface IAddChild
{
void AddChild(object child);
}
- public interface IAddChild : IAddChild
+ public interface IAddChild
{
void AddChild(T child);
}
diff --git a/src/Avalonia.Base/Platform/IPlatformGpu.cs b/src/Avalonia.Base/Platform/IPlatformGpu.cs
new file mode 100644
index 0000000000..0507dea1d7
--- /dev/null
+++ b/src/Avalonia.Base/Platform/IPlatformGpu.cs
@@ -0,0 +1,16 @@
+using System;
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform;
+
+[Unstable]
+public interface IPlatformGpu
+{
+ IPlatformGpuContext PrimaryContext { get; }
+}
+
+[Unstable]
+public interface IPlatformGpuContext : IDisposable
+{
+ IDisposable EnsureCurrent();
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
new file mode 100644
index 0000000000..5af02219ce
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Security;
+using System.Threading.Tasks;
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform.Storage.FileIO;
+
+[Unstable]
+public class BclStorageFile : IStorageBookmarkFile
+{
+ private readonly FileInfo _fileInfo;
+
+ public BclStorageFile(FileInfo fileInfo)
+ {
+ _fileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo));
+ }
+
+ public bool CanOpenRead => true;
+
+ public bool CanOpenWrite => true;
+
+ public string Name => _fileInfo.Name;
+
+ public virtual bool CanBookmark => true;
+
+ public Task GetBasicPropertiesAsync()
+ {
+ var props = new StorageItemProperties();
+ if (_fileInfo.Exists)
+ {
+ props = new StorageItemProperties(
+ (ulong)_fileInfo.Length,
+ _fileInfo.CreationTimeUtc,
+ _fileInfo.LastAccessTimeUtc);
+ }
+ return Task.FromResult(props);
+ }
+
+ public Task GetParentAsync()
+ {
+ if (_fileInfo.Directory is { } directory)
+ {
+ return Task.FromResult(new BclStorageFolder(directory));
+ }
+ return Task.FromResult(null);
+ }
+
+ public Task OpenRead()
+ {
+ return Task.FromResult(_fileInfo.OpenRead());
+ }
+
+ public Task OpenWrite()
+ {
+ return Task.FromResult(_fileInfo.OpenWrite());
+ }
+
+ public virtual Task SaveBookmark()
+ {
+ return Task.FromResult(_fileInfo.FullName);
+ }
+
+ public Task ReleaseBookmark()
+ {
+ // No-op
+ return Task.CompletedTask;
+ }
+
+ public bool TryGetUri([NotNullWhen(true)] out Uri? uri)
+ {
+ try
+ {
+ if (_fileInfo.Directory is not null)
+ {
+ uri = Path.IsPathRooted(_fileInfo.FullName) ?
+ new Uri(new Uri("file://"), _fileInfo.FullName) :
+ new Uri(_fileInfo.FullName, UriKind.Relative);
+ return true;
+ }
+
+ uri = null;
+ return false;
+ }
+ catch (SecurityException)
+ {
+ uri = null;
+ return false;
+ }
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ }
+
+ ~BclStorageFile()
+ {
+ Dispose(disposing: false);
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
new file mode 100644
index 0000000000..7267017eaf
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Security;
+using System.Threading.Tasks;
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform.Storage.FileIO;
+
+[Unstable]
+public class BclStorageFolder : IStorageBookmarkFolder
+{
+ private readonly DirectoryInfo _directoryInfo;
+
+ public BclStorageFolder(DirectoryInfo directoryInfo)
+ {
+ _directoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo));
+ if (!_directoryInfo.Exists)
+ {
+ throw new ArgumentException("Directory must exist", nameof(directoryInfo));
+ }
+ }
+
+ public string Name => _directoryInfo.Name;
+
+ public bool CanBookmark => true;
+
+ public Task GetBasicPropertiesAsync()
+ {
+ var props = new StorageItemProperties(
+ null,
+ _directoryInfo.CreationTimeUtc,
+ _directoryInfo.LastAccessTimeUtc);
+ return Task.FromResult(props);
+ }
+
+ public Task GetParentAsync()
+ {
+ if (_directoryInfo.Parent is { } directory)
+ {
+ return Task.FromResult(new BclStorageFolder(directory));
+ }
+ return Task.FromResult(null);
+ }
+
+ public virtual Task SaveBookmark()
+ {
+ return Task.FromResult(_directoryInfo.FullName);
+ }
+
+ public Task ReleaseBookmark()
+ {
+ // No-op
+ return Task.CompletedTask;
+ }
+
+ public bool TryGetUri([NotNullWhen(true)] out Uri? uri)
+ {
+ try
+ {
+ uri = Path.IsPathRooted(_directoryInfo.FullName) ?
+ new Uri(new Uri("file://"), _directoryInfo.FullName) :
+ new Uri(_directoryInfo.FullName, UriKind.Relative);
+
+ return true;
+ }
+ catch (SecurityException)
+ {
+ uri = null;
+ return false;
+ }
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ }
+
+ ~BclStorageFolder()
+ {
+ Dispose(disposing: false);
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
new file mode 100644
index 0000000000..469388021e
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform.Storage.FileIO;
+
+[Unstable]
+public abstract class BclStorageProvider : IStorageProvider
+{
+ public abstract bool CanOpen { get; }
+ public abstract Task> OpenFilePickerAsync(FilePickerOpenOptions options);
+
+ public abstract bool CanSave { get; }
+ public abstract Task SaveFilePickerAsync(FilePickerSaveOptions options);
+
+ public abstract bool CanPickFolder { get; }
+ public abstract Task> OpenFolderPickerAsync(FolderPickerOpenOptions options);
+
+ public virtual Task OpenFileBookmarkAsync(string bookmark)
+ {
+ var file = new FileInfo(bookmark);
+ return file.Exists
+ ? Task.FromResult(new BclStorageFile(file))
+ : Task.FromResult(null);
+ }
+
+ public virtual Task OpenFolderBookmarkAsync(string bookmark)
+ {
+ var folder = new DirectoryInfo(bookmark);
+ return folder.Exists
+ ? Task.FromResult(new BclStorageFolder(folder))
+ : Task.FromResult(null);
+ }
+}
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
new file mode 100644
index 0000000000..f90d0a5a2f
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
@@ -0,0 +1,40 @@
+using System;
+using System.IO;
+using System.Linq;
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform.Storage.FileIO;
+
+[Unstable]
+public static class StorageProviderHelpers
+{
+ public static string NameWithExtension(string path, string? defaultExtension, FilePickerFileType? filter)
+ {
+ var name = Path.GetFileName(path);
+ if (name != null && !Path.HasExtension(name))
+ {
+ if (filter?.Patterns?.Count > 0)
+ {
+ if (defaultExtension != null
+ && filter.Patterns.Contains(defaultExtension))
+ {
+ return Path.ChangeExtension(path, defaultExtension.TrimStart('.'));
+ }
+
+ var ext = filter.Patterns.FirstOrDefault(x => x != "*.*");
+ ext = ext?.Split(new[] { "*." }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault();
+ if (ext != null)
+ {
+ return Path.ChangeExtension(path, ext);
+ }
+ }
+
+ if (defaultExtension != null)
+ {
+ return Path.ChangeExtension(path, defaultExtension);
+ }
+ }
+
+ return path;
+ }
+}
diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs b/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs
new file mode 100644
index 0000000000..75076e2bb8
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Platform.Storage;
+
+///
+/// Represents a name mapped to the associated file types (extensions).
+///
+public sealed class FilePickerFileType
+{
+ public FilePickerFileType(string name)
+ {
+ Name = name;
+ }
+
+ ///
+ /// File type name.
+ ///
+ public string Name { get; }
+
+ ///
+ /// List of extensions in GLOB format. I.e. "*.png" or "*.*".
+ ///
+ ///
+ /// Used on Windows and Linux systems.
+ ///
+ public IReadOnlyList? Patterns { get; set; }
+
+ ///
+ /// List of extensions in MIME format.
+ ///
+ ///
+ /// Used on Android, Browser and Linux systems.
+ ///
+ public IReadOnlyList? MimeTypes { get; set; }
+
+ ///
+ /// List of extensions in Apple uniform format.
+ ///
+ ///
+ /// Used only on Apple devices.
+ /// See https://developer.apple.com/documentation/uniformtypeidentifiers/system_declared_uniform_type_identifiers.
+ ///
+ public IReadOnlyList? AppleUniformTypeIdentifiers { get; set; }
+}
diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs b/src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs
new file mode 100644
index 0000000000..5da037999a
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs
@@ -0,0 +1,48 @@
+namespace Avalonia.Platform.Storage;
+
+///
+/// Dictionary of well known file types.
+///
+public static class FilePickerFileTypes
+{
+ public static FilePickerFileType All { get; } = new("All")
+ {
+ Patterns = new[] { "*.*" },
+ MimeTypes = new[] { "*/*" }
+ };
+
+ public static FilePickerFileType TextPlain { get; } = new("Plain Text")
+ {
+ Patterns = new[] { "*.txt" },
+ AppleUniformTypeIdentifiers = new[] { "public.plain-text" },
+ MimeTypes = new[] { "text/plain" }
+ };
+
+ public static FilePickerFileType ImageAll { get; } = new("All Images")
+ {
+ Patterns = new[] { "*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp" },
+ AppleUniformTypeIdentifiers = new[] { "public.image" },
+ MimeTypes = new[] { "image/*" }
+ };
+
+ public static FilePickerFileType ImageJpg { get; } = new("JPEG image")
+ {
+ Patterns = new[] { "*.jpg", "*.jpeg" },
+ AppleUniformTypeIdentifiers = new[] { "public.jpeg" },
+ MimeTypes = new[] { "image/jpeg" }
+ };
+
+ public static FilePickerFileType ImagePng { get; } = new("PNG image")
+ {
+ Patterns = new[] { "*.png" },
+ AppleUniformTypeIdentifiers = new[] { "public.png" },
+ MimeTypes = new[] { "image/png" }
+ };
+
+ public static FilePickerFileType Pdf { get; } = new("PDF document")
+ {
+ Patterns = new[] { "*.pdf" },
+ AppleUniformTypeIdentifiers = new[] { "com.adobe.pdf" },
+ MimeTypes = new[] { "application/pdf" }
+ };
+}
diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs b/src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs
new file mode 100644
index 0000000000..1674ec11c8
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Platform.Storage;
+
+///
+/// Options class for method.
+///
+public class FilePickerOpenOptions : PickerOptions
+{
+ ///
+ /// Gets or sets an option indicating whether open picker allows users to select multiple files.
+ ///
+ public bool AllowMultiple { get; set; }
+
+ ///
+ /// Gets or sets the collection of file types that the file open picker displays.
+ ///
+ public IReadOnlyList? FileTypeFilter { get; set; }
+}
diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs b/src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs
new file mode 100644
index 0000000000..fa4fccd47a
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Platform.Storage;
+
+///
+/// Options class for method.
+///
+public class FilePickerSaveOptions : PickerOptions
+{
+ ///
+ /// Gets or sets the file name that the file save picker suggests to the user.
+ ///
+ public string? SuggestedFileName { get; set; }
+
+ ///
+ /// Gets or sets the default extension to be used to save the file.
+ ///
+ public string? DefaultExtension { get; set; }
+
+ ///
+ /// Gets or sets the collection of valid file types that the user can choose to assign to a file.
+ ///
+ public IReadOnlyList? FileTypeChoices { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether file open picker displays a warning if the user specifies the name of a file that already exists.
+ ///
+ public bool? ShowOverwritePrompt { get; set; }
+}
diff --git a/src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs b/src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs
new file mode 100644
index 0000000000..df9fa8fd35
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs
@@ -0,0 +1,12 @@
+namespace Avalonia.Platform.Storage;
+
+///
+/// Options class for method.
+///
+public class FolderPickerOpenOptions : PickerOptions
+{
+ ///
+ /// Gets or sets an option indicating whether open picker allows users to select multiple folders.
+ ///
+ public bool AllowMultiple { get; set; }
+}
diff --git a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs
new file mode 100644
index 0000000000..d21c950862
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs
@@ -0,0 +1,20 @@
+using System.Threading.Tasks;
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform.Storage;
+
+[NotClientImplementable]
+public interface IStorageBookmarkItem : IStorageItem
+{
+ Task ReleaseBookmark();
+}
+
+[NotClientImplementable]
+public interface IStorageBookmarkFile : IStorageFile, IStorageBookmarkItem
+{
+}
+
+[NotClientImplementable]
+public interface IStorageBookmarkFolder : IStorageFolder, IStorageBookmarkItem
+{
+}
diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs
new file mode 100644
index 0000000000..965caf8216
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs
@@ -0,0 +1,32 @@
+using System.IO;
+using System.Threading.Tasks;
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform.Storage;
+
+///
+/// Represents a file. Provides information about the file and its contents, and ways to manipulate them.
+///
+[NotClientImplementable]
+public interface IStorageFile : IStorageItem
+{
+ ///
+ /// Returns true, if file is readable.
+ ///
+ bool CanOpenRead { get; }
+
+ ///
+ /// Opens a stream for read access.
+ ///
+ Task OpenRead();
+
+ ///
+ /// Returns true, if file is writeable.
+ ///
+ bool CanOpenWrite { get; }
+
+ ///
+ /// Opens stream for writing to the file.
+ ///
+ Task OpenWrite();
+}
diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs
new file mode 100644
index 0000000000..25b9f01a92
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs
@@ -0,0 +1,11 @@
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform.Storage;
+
+///
+/// Manipulates folders and their contents, and provides information about them.
+///
+[NotClientImplementable]
+public interface IStorageFolder : IStorageItem
+{
+}
diff --git a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs
new file mode 100644
index 0000000000..8513ebc7d9
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform.Storage;
+
+///
+/// Manipulates storage items (files and folders) and their contents, and provides information about them
+///
+///
+/// This interface inherits . It's recommended to dispose when it's not used anymore.
+///
+[NotClientImplementable]
+public interface IStorageItem : IDisposable
+{
+ ///
+ /// Gets the name of the item including the file name extension if there is one.
+ ///
+ string Name { get; }
+
+ ///
+ /// Gets the full file-system path of the item, if the item has a path.
+ ///
+ ///
+ /// Android backend might return file path with "content:" scheme.
+ /// Browser and iOS backends might return relative uris.
+ ///
+ bool TryGetUri([NotNullWhen(true)] out Uri? uri);
+
+ ///
+ /// Gets the basic properties of the current item.
+ ///
+ Task GetBasicPropertiesAsync();
+
+ ///
+ /// Returns true is item can be bookmarked and reused later.
+ ///
+ bool CanBookmark { get; }
+
+ ///
+ /// Saves items to a bookmark.
+ ///
+ ///
+ /// Returns identifier of a bookmark. Can be null if OS denied request.
+ ///
+ Task SaveBookmark();
+
+ ///
+ /// Gets the parent folder of the current storage item.
+ ///
+ Task GetParentAsync();
+}
diff --git a/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs
new file mode 100644
index 0000000000..0f5cf931d9
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs
@@ -0,0 +1,56 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform.Storage;
+
+[NotClientImplementable]
+public interface IStorageProvider
+{
+ ///
+ /// Returns true if it's possible to open file picker on the current platform.
+ ///
+ bool CanOpen { get; }
+
+ ///
+ /// Opens file picker dialog.
+ ///
+ /// Array of selected or empty collection if user canceled the dialog.
+ Task> OpenFilePickerAsync(FilePickerOpenOptions options);
+
+ ///
+ /// Returns true if it's possible to open save file picker on the current platform.
+ ///
+ bool CanSave { get; }
+
+ ///
+ /// Opens save file picker dialog.
+ ///
+ /// Saved or null if user canceled the dialog.
+ Task SaveFilePickerAsync(FilePickerSaveOptions options);
+
+ ///
+ /// Returns true if it's possible to open folder picker on the current platform.
+ ///
+ bool CanPickFolder { get; }
+
+ ///
+ /// Opens folder picker dialog.
+ ///
+ /// Array of selected or empty collection if user canceled the dialog.
+ Task> OpenFolderPickerAsync(FolderPickerOpenOptions options);
+
+ ///
+ /// Open from the bookmark ID.
+ ///
+ /// Bookmark ID.
+ /// Bookmarked file or null if OS denied request.
+ Task OpenFileBookmarkAsync(string bookmark);
+
+ ///
+ /// Open from the bookmark ID.
+ ///
+ /// Bookmark ID.
+ /// Bookmarked folder or null if OS denied request.
+ Task OpenFolderBookmarkAsync(string bookmark);
+}
diff --git a/src/Avalonia.Base/Platform/Storage/PickerOptions.cs b/src/Avalonia.Base/Platform/Storage/PickerOptions.cs
new file mode 100644
index 0000000000..6f97916a26
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/PickerOptions.cs
@@ -0,0 +1,17 @@
+namespace Avalonia.Platform.Storage;
+
+///
+/// Common options for , and methods.
+///
+public class PickerOptions
+{
+ ///
+ /// Gets or sets the text that appears in the title bar of a folder dialog.
+ ///
+ public string? Title { get; set; }
+
+ ///
+ /// Gets or sets the initial location where the file open picker looks for files to present to the user.
+ ///
+ public IStorageFolder? SuggestedStartLocation { get; set; }
+}
diff --git a/src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs b/src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs
new file mode 100644
index 0000000000..a63973ccf8
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs
@@ -0,0 +1,43 @@
+using System;
+
+namespace Avalonia.Platform.Storage;
+
+///
+/// Provides access to the content-related properties of an item (like a file or folder).
+///
+public class StorageItemProperties
+{
+ public StorageItemProperties(
+ ulong? size = null,
+ DateTimeOffset? dateCreated = null,
+ DateTimeOffset? dateModified = null)
+ {
+ Size = size;
+ DateCreated = dateCreated;
+ DateModified = dateModified;
+ }
+
+ ///
+ /// Gets the size of the file in bytes.
+ ///
+ ///
+ /// Can be null if property is not available.
+ ///
+ public ulong? Size { get; }
+
+ ///
+ /// Gets the date and time that the current folder was created.
+ ///
+ ///
+ /// Can be null if property is not available.
+ ///
+ public DateTimeOffset? DateCreated { get; }
+
+ ///
+ /// Gets the date and time of the last time the file was modified.
+ ///
+ ///
+ /// Can be null if property is not available.
+ ///
+ public DateTimeOffset? DateModified { get; }
+}
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs
new file mode 100644
index 0000000000..80e64118ee
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Rendering.Composition.Expressions;
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition.Animations;
+
+
+///
+/// The base class for both key-frame and expression animation instances
+/// Is responsible for activation tracking and for subscribing to properties used in dependencies
+///
+internal abstract class AnimationInstanceBase : IAnimationInstance
+{
+ private List<(ServerObject obj, CompositionProperty member)>? _trackedObjects;
+ protected PropertySetSnapshot Parameters { get; }
+ public ServerObject TargetObject { get; }
+ protected CompositionProperty Property { get; private set; } = null!;
+ private bool _invalidated;
+
+ public AnimationInstanceBase(ServerObject target, PropertySetSnapshot parameters)
+ {
+ Parameters = parameters;
+ TargetObject = target;
+ }
+
+ protected void Initialize(CompositionProperty property, HashSet<(string name, string member)> trackedObjects)
+ {
+ if (trackedObjects.Count > 0)
+ {
+ _trackedObjects = new ();
+ foreach (var t in trackedObjects)
+ {
+ var obj = Parameters.GetObjectParameter(t.name);
+ if (obj is ServerObject tracked)
+ {
+ var off = tracked.GetCompositionProperty(t.member);
+ if (off == null)
+#if DEBUG
+ throw new InvalidCastException("Attempting to subscribe to unknown field");
+#else
+ continue;
+#endif
+ _trackedObjects.Add((tracked, off));
+ }
+ }
+ }
+
+ Property = property;
+ }
+
+ public abstract void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property);
+ protected abstract ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue);
+
+ public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue)
+ {
+ _invalidated = false;
+ return EvaluateCore(now, currentValue);
+ }
+
+ public virtual void Activate()
+ {
+ if (_trackedObjects != null)
+ foreach (var tracked in _trackedObjects)
+ tracked.obj.SubscribeToInvalidation(tracked.member, this);
+ }
+
+ public virtual void Deactivate()
+ {
+ if (_trackedObjects != null)
+ foreach (var tracked in _trackedObjects)
+ tracked.obj.UnsubscribeFromInvalidation(tracked.member, this);
+ }
+
+ public void Invalidate()
+ {
+ if (_invalidated)
+ return;
+ _invalidated = true;
+ TargetObject.NotifyAnimatedValueChanged(Property);
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs
new file mode 100644
index 0000000000..c5102a2d7d
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs
@@ -0,0 +1,75 @@
+// ReSharper disable InconsistentNaming
+// ReSharper disable CheckNamespace
+
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using Avalonia.Rendering.Composition.Expressions;
+using Avalonia.Rendering.Composition.Server;
+using Avalonia.Rendering.Composition.Transport;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ ///
+ /// This is the base class for ExpressionAnimation and KeyFrameAnimation.
+ ///
+ ///
+ /// Use the method to start the animation.
+ /// Value parameters (as opposed to reference parameters which are set using )
+ /// are copied and "embedded" into an expression at the time CompositionObject.StartAnimation is called.
+ /// Changing the value of the variable after is called will not affect
+ /// the value of the ExpressionAnimation.
+ /// See the remarks section of ExpressionAnimation for additional information.
+ ///
+ public abstract class CompositionAnimation : CompositionObject, ICompositionAnimationBase
+ {
+ private readonly CompositionPropertySet _propertySet;
+ internal CompositionAnimation(Compositor compositor) : base(compositor, null!)
+ {
+ _propertySet = new CompositionPropertySet(compositor);
+ }
+
+ ///
+ /// Clears all of the parameters of the animation.
+ ///
+ public void ClearAllParameters() => _propertySet.ClearAll();
+
+ ///
+ /// Clears a parameter from the animation.
+ ///
+ public void ClearParameter(string key) => _propertySet.Clear(key);
+
+ void SetVariant(string key, ExpressionVariant value) => _propertySet.Set(key, value);
+
+ public void SetColorParameter(string key, Media.Color value) => SetVariant(key, value);
+
+ public void SetMatrix3x2Parameter(string key, Matrix3x2 value) => SetVariant(key, value);
+
+ public void SetMatrix4x4Parameter(string key, Matrix4x4 value) => SetVariant(key, value);
+
+ public void SetQuaternionParameter(string key, Quaternion value) => SetVariant(key, value);
+
+ public void SetReferenceParameter(string key, CompositionObject compositionObject) =>
+ _propertySet.Set(key, compositionObject);
+
+ public void SetScalarParameter(string key, float value) => SetVariant(key, value);
+
+ public void SetVector2Parameter(string key, Vector2 value) => SetVariant(key, value);
+
+ public void SetVector3Parameter(string key, Vector3 value) => SetVariant(key, value);
+
+ public void SetVector4Parameter(string key, Vector4 value) => SetVariant(key, value);
+
+ public string? Target { get; set; }
+
+ internal abstract IAnimationInstance CreateInstance(ServerObject targetObject,
+ ExpressionVariant? finalValue);
+
+ internal PropertySetSnapshot CreateSnapshot() => _propertySet.Snapshot();
+
+ void ICompositionAnimationBase.InternalOnly()
+ {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs
new file mode 100644
index 0000000000..89f8ba411d
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Rendering.Composition.Transport;
+
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ public class CompositionAnimationGroup : CompositionObject, ICompositionAnimationBase
+ {
+ internal List Animations { get; } = new List();
+ void ICompositionAnimationBase.InternalOnly()
+ {
+
+ }
+
+ public void Add(CompositionAnimation value) => Animations.Add(value);
+ public void Remove(CompositionAnimation value) => Animations.Remove(value);
+ public void RemoveAll() => Animations.Clear();
+
+ public CompositionAnimationGroup(Compositor compositor) : base(compositor, null!)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs
new file mode 100644
index 0000000000..163f4e99ba
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs
@@ -0,0 +1,53 @@
+// ReSharper disable CheckNamespace
+using System;
+using Avalonia.Rendering.Composition.Expressions;
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ ///
+ /// A Composition Animation that uses a mathematical equation to calculate the value for an animating property every frame.
+ ///
+ ///
+ /// The core of ExpressionAnimations allows a developer to define a mathematical equation that can be used to calculate the value
+ /// of a targeted animating property each frame.
+ /// This contrasts s, which use an interpolator to define how the animating
+ /// property changes over time. The mathematical equation can be defined using references to properties
+ /// of Composition objects, mathematical functions and operators and Input.
+ /// Use the method to start the animation.
+ ///
+ public class ExpressionAnimation : CompositionAnimation
+ {
+ private string? _expression;
+ private Expression? _parsedExpression;
+
+ internal ExpressionAnimation(Compositor compositor) : base(compositor)
+ {
+ }
+
+ ///
+ /// The mathematical equation specifying how the animated value is calculated each frame.
+ /// The Expression is the core of an and represents the equation
+ /// the system will use to calculate the value of the animation property each frame.
+ /// The equation is set on this property in the form of a string.
+ /// Although expressions can be defined by simple mathematical equations such as "2+2",
+ /// the real power lies in creating mathematical relationships where the input values can change frame over frame.
+ ///
+ public string? Expression
+ {
+ get => _expression;
+ set
+ {
+ _expression = value;
+ _parsedExpression = null;
+ }
+ }
+
+ private Expression ParsedExpression => _parsedExpression ??= ExpressionParser.Parse(_expression.AsSpan());
+
+ internal override IAnimationInstance CreateInstance(
+ ServerObject targetObject, ExpressionVariant? finalValue)
+ => new ExpressionAnimationInstance(ParsedExpression,
+ targetObject, finalValue, CreateSnapshot());
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs
new file mode 100644
index 0000000000..764bac9931
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Rendering.Composition.Expressions;
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+
+ ///
+ /// Server-side counterpart of with values baked-in.
+ ///
+ internal class ExpressionAnimationInstance : AnimationInstanceBase, IAnimationInstance
+ {
+ private readonly Expression _expression;
+ private ExpressionVariant _startingValue;
+ private readonly ExpressionVariant? _finalValue;
+
+ protected override ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue)
+ {
+ var ctx = new ExpressionEvaluationContext
+ {
+ Parameters = Parameters,
+ Target = TargetObject,
+ ForeignFunctionInterface = BuiltInExpressionFfi.Instance,
+ StartingValue = _startingValue,
+ FinalValue = _finalValue ?? _startingValue,
+ CurrentValue = currentValue
+ };
+ return _expression.Evaluate(ref ctx);
+ }
+
+ public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property)
+ {
+ _startingValue = startingValue;
+ var hs = new HashSet<(string, string)>();
+ _expression.CollectReferences(hs);
+ base.Initialize(property, hs);
+ }
+
+ public ExpressionAnimationInstance(Expression expression,
+ ServerObject target,
+ ExpressionVariant? finalValue,
+ PropertySetSnapshot parameters) : base(target, parameters)
+ {
+ _expression = expression;
+ _finalValue = finalValue;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs
new file mode 100644
index 0000000000..4e1972f2c6
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs
@@ -0,0 +1,16 @@
+using System;
+using Avalonia.Rendering.Composition.Expressions;
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ internal interface IAnimationInstance
+ {
+ ServerObject TargetObject { get; }
+ ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue);
+ void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property);
+ void Activate();
+ void Deactivate();
+ void Invalidate();
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs
new file mode 100644
index 0000000000..87e5ad757a
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs
@@ -0,0 +1,15 @@
+// ReSharper disable CheckNamespace
+
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ ///
+ /// Base class for composition animations.
+ ///
+ public interface ICompositionAnimationBase
+ {
+ internal void InternalOnly();
+ }
+
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs
new file mode 100644
index 0000000000..f4bcc6ff38
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Avalonia.Rendering.Composition.Transport;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ ///
+ /// A collection of animations triggered when a condition is met.
+ ///
+ ///
+ /// Implicit animations let you drive animations by specifying trigger conditions rather than requiring the manual definition of animation behavior.
+ /// They help decouple animation start logic from core app logic. You define animations and the events that should trigger these animations.
+ /// Currently the only available trigger is animated property change.
+ ///
+ /// When expression is used in ImplicitAnimationCollection a special keyword `this.FinalValue` will represent
+ /// the final value of the animated property that was changed
+ ///
+ public class ImplicitAnimationCollection : CompositionObject, IDictionary
+ {
+ private Dictionary _inner = new Dictionary();
+ private IDictionary _innerface;
+ internal ImplicitAnimationCollection(Compositor compositor) : base(compositor, null!)
+ {
+ _innerface = _inner;
+ }
+
+ public IEnumerator> GetEnumerator() => _inner.GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable) _inner).GetEnumerator();
+
+ void ICollection>.Add(KeyValuePair item) => _innerface.Add(item);
+
+ public void Clear() => _inner.Clear();
+
+ bool ICollection>.Contains(KeyValuePair item) => _innerface.Contains(item);
+
+ void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => _innerface.CopyTo(array, arrayIndex);
+
+ bool ICollection>.Remove(KeyValuePair item) => _innerface.Remove(item);
+
+ public int Count => _inner.Count;
+
+ bool ICollection>.IsReadOnly => _innerface.IsReadOnly;
+
+ public void Add(string key, ICompositionAnimationBase value) => _inner.Add(key, value);
+
+ public bool ContainsKey(string key) => _inner.ContainsKey(key);
+
+ public bool Remove(string key) => _inner.Remove(key);
+
+ public bool TryGetValue(string key, [MaybeNullWhen(false)] out ICompositionAnimationBase value) =>
+ _inner.TryGetValue(key, out value);
+
+ public ICompositionAnimationBase this[string key]
+ {
+ get => _inner[key];
+ set => _inner[key] = value;
+ }
+
+ ICollection IDictionary.Keys => _innerface.Keys;
+
+ ICollection IDictionary.Values =>
+ _innerface.Values;
+
+ // UWP compat
+ public uint Size => (uint) Count;
+
+ public IReadOnlyDictionary GetView() =>
+ new Dictionary(this);
+
+ public bool HasKey(string key) => ContainsKey(key);
+ public void Insert(string key, ICompositionAnimationBase animation) => Add(key, animation);
+
+ public ICompositionAnimationBase? Lookup(string key)
+ {
+ _inner.TryGetValue(key, out var rv);
+ return rv;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs b/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs
new file mode 100644
index 0000000000..a4eeacef32
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Numerics;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ ///
+ /// An interface to define interpolation logic for a particular type
+ ///
+ internal interface IInterpolator
+ {
+ T Interpolate(T from, T to, float progress);
+ }
+
+ class ScalarInterpolator : IInterpolator
+ {
+ public float Interpolate(float @from, float to, float progress) => @from + (to - @from) * progress;
+
+ public static ScalarInterpolator Instance { get; } = new ScalarInterpolator();
+ }
+
+ class Vector2Interpolator : IInterpolator
+ {
+ public Vector2 Interpolate(Vector2 @from, Vector2 to, float progress)
+ => Vector2.Lerp(@from, to, progress);
+
+ public static Vector2Interpolator Instance { get; } = new Vector2Interpolator();
+ }
+
+ class Vector3Interpolator : IInterpolator
+ {
+ public Vector3 Interpolate(Vector3 @from, Vector3 to, float progress)
+ => Vector3.Lerp(@from, to, progress);
+
+ public static Vector3Interpolator Instance { get; } = new Vector3Interpolator();
+ }
+
+ class Vector4Interpolator : IInterpolator
+ {
+ public Vector4 Interpolate(Vector4 @from, Vector4 to, float progress)
+ => Vector4.Lerp(@from, to, progress);
+
+ public static Vector4Interpolator Instance { get; } = new Vector4Interpolator();
+ }
+
+ class QuaternionInterpolator : IInterpolator
+ {
+ public Quaternion Interpolate(Quaternion @from, Quaternion to, float progress)
+ => Quaternion.Lerp(@from, to, progress);
+
+ public static QuaternionInterpolator Instance { get; } = new QuaternionInterpolator();
+ }
+
+ class ColorInterpolator : IInterpolator
+ {
+ static byte Lerp(float a, float b, float p) => (byte) Math.Max(0, Math.Min(255, (p * (b - a) + a)));
+
+ public static Avalonia.Media.Color
+ LerpRGB(Avalonia.Media.Color to, Avalonia.Media.Color from, float progress) =>
+ new Avalonia.Media.Color(Lerp(to.A, @from.A, progress),
+ Lerp(to.R, @from.R, progress),
+ Lerp(to.G, @from.G, progress),
+ Lerp(to.B, @from.B, progress));
+
+ public Avalonia.Media.Color Interpolate(Avalonia.Media.Color @from, Avalonia.Media.Color to, float progress)
+ => LerpRGB(@from, to, progress);
+
+ public static ColorInterpolator Instance { get; } = new ColorInterpolator();
+ }
+
+ class BooleanInterpolator : IInterpolator
+ {
+ public bool Interpolate(bool @from, bool to, float progress) => progress >= 1 ? to : @from;
+
+ public static BooleanInterpolator Instance { get; } = new BooleanInterpolator();
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs
new file mode 100644
index 0000000000..49b3ab753a
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs
@@ -0,0 +1,134 @@
+using System;
+using Avalonia.Animation;
+using Avalonia.Animation.Easings;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+
+ ///
+ /// A time-based animation with one or more key frames.
+ /// These frames are markers, allowing developers to specify values at specific times for the animating property.
+ /// KeyFrame animations can be further customized by specifying how the animation interpolates between keyframes.
+ ///
+ public abstract class KeyFrameAnimation : CompositionAnimation
+ {
+ private TimeSpan _duration = TimeSpan.FromMilliseconds(1);
+
+ internal KeyFrameAnimation(Compositor compositor) : base(compositor)
+ {
+ }
+
+ ///
+ /// The delay behavior of the key frame animation.
+ ///
+ public AnimationDelayBehavior DelayBehavior { get; set; }
+
+ ///
+ /// Delay before the animation starts after is called.
+ ///
+ public System.TimeSpan DelayTime { get; set; }
+
+ ///
+ /// The direction the animation is playing.
+ /// The Direction property allows you to drive your animation from start to end or end to start or alternate
+ /// between start and end or end to start if animation has an greater than one.
+ /// This gives an easy way for customizing animation definitions.
+ ///
+ public PlaybackDirection Direction { get; set; }
+
+ ///
+ /// The duration of the animation.
+ /// Minimum allowed value is 1ms and maximum allowed value is 24 days.
+ ///
+ public TimeSpan Duration
+ {
+ get => _duration;
+ set
+ {
+ if (_duration < TimeSpan.FromMilliseconds(1) || _duration > TimeSpan.FromDays(1))
+ throw new ArgumentException("Minimum allowed value is 1ms and maximum allowed value is 24 days.");
+ _duration = value;
+ }
+ }
+
+ ///
+ /// The iteration behavior for the key frame animation.
+ ///
+ public AnimationIterationBehavior IterationBehavior { get; set; }
+
+ ///
+ /// The number of times to repeat the key frame animation.
+ ///
+ public int IterationCount { get; set; } = 1;
+
+ ///
+ /// Specifies how to set the property value when animation is stopped
+ ///
+ public AnimationStopBehavior StopBehavior { get; set; }
+
+ private protected abstract IKeyFrames KeyFrames { get; }
+
+ ///
+ /// Inserts an expression keyframe.
+ ///
+ ///
+ /// The time the key frame should occur at, expressed as a percentage of the animation Duration. Allowed value is from 0.0 to 1.0.
+ ///
+ /// The expression used to calculate the value of the key frame.
+ /// The easing function to use when interpolating between frames.
+ public void InsertExpressionKeyFrame(float normalizedProgressKey, string value,
+ Easing? easingFunction = null) =>
+ KeyFrames.InsertExpressionKeyFrame(normalizedProgressKey, value, easingFunction ?? Compositor.DefaultEasing);
+ }
+
+ ///
+ /// Specifies the animation delay behavior.
+ ///
+ public enum AnimationDelayBehavior
+ {
+ ///
+ /// If a DelayTime is specified, it delays starting the animation according to delay time and after delay
+ /// has expired it applies animation to the object property.
+ ///
+ SetInitialValueAfterDelay,
+ ///
+ /// Applies the initial value of the animation (i.e. the value at Keyframe 0) to the object before the delay time
+ /// is elapsed (when there is a DelayTime specified), it then delays starting the animation according to the DelayTime.
+ ///
+ SetInitialValueBeforeDelay
+ }
+
+ ///
+ /// Specifies if the animation should loop.
+ ///
+ public enum AnimationIterationBehavior
+ {
+ ///
+ /// The animation should loop the specified number of times.
+ ///
+ Count,
+ ///
+ /// The animation should loop forever.
+ ///
+ Forever
+ }
+
+ ///
+ /// Specifies the behavior of an animation when it stops.
+ ///
+ public enum AnimationStopBehavior
+ {
+ ///
+ /// Leave the animation at its current value.
+ ///
+ LeaveCurrentValue,
+ ///
+ /// Reset the animation to its initial value.
+ ///
+ SetToInitialValue,
+ ///
+ /// Set the animation to its final value.
+ ///
+ SetToFinalValue
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs
new file mode 100644
index 0000000000..0c0fcfaf2b
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs
@@ -0,0 +1,178 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Animation;
+using Avalonia.Rendering.Composition.Expressions;
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ ///
+ /// Server-side counterpart of KeyFrameAnimation with values baked-in
+ ///
+ class KeyFrameAnimationInstance : AnimationInstanceBase, IAnimationInstance where T : struct
+ {
+ private readonly IInterpolator _interpolator;
+ private readonly ServerKeyFrame[] _keyFrames;
+ private readonly ExpressionVariant? _finalValue;
+ private readonly AnimationDelayBehavior _delayBehavior;
+ private readonly TimeSpan _delayTime;
+ private readonly PlaybackDirection _direction;
+ private readonly TimeSpan _duration;
+ private readonly AnimationIterationBehavior _iterationBehavior;
+ private readonly int _iterationCount;
+ private readonly AnimationStopBehavior _stopBehavior;
+ private TimeSpan _startedAt;
+ private T _startingValue;
+ private readonly TimeSpan _totalDuration;
+ private bool _finished;
+
+ public KeyFrameAnimationInstance(
+ IInterpolator interpolator, ServerKeyFrame[] keyFrames,
+ PropertySetSnapshot snapshot, ExpressionVariant? finalValue,
+ ServerObject target,
+ AnimationDelayBehavior delayBehavior, TimeSpan delayTime,
+ PlaybackDirection direction, TimeSpan duration,
+ AnimationIterationBehavior iterationBehavior,
+ int iterationCount, AnimationStopBehavior stopBehavior) : base(target, snapshot)
+ {
+ _interpolator = interpolator;
+ _keyFrames = keyFrames;
+ _finalValue = finalValue;
+ _delayBehavior = delayBehavior;
+ _delayTime = delayTime;
+ _direction = direction;
+ _duration = duration;
+ _iterationBehavior = iterationBehavior;
+ _iterationCount = iterationCount;
+ _stopBehavior = stopBehavior;
+ if (_iterationBehavior == AnimationIterationBehavior.Count)
+ _totalDuration = delayTime.Add(TimeSpan.FromTicks(iterationCount * _duration.Ticks));
+ if (_keyFrames.Length == 0)
+ throw new InvalidOperationException("Animation has no key frames");
+ if(_duration.Ticks <= 0)
+ throw new InvalidOperationException("Invalid animation duration");
+ }
+
+
+ protected override ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue)
+ {
+ var starting = ExpressionVariant.Create(_startingValue);
+ var ctx = new ExpressionEvaluationContext
+ {
+ Parameters = Parameters,
+ Target = TargetObject,
+ CurrentValue = currentValue,
+ FinalValue = _finalValue ?? starting,
+ StartingValue = starting,
+ ForeignFunctionInterface = BuiltInExpressionFfi.Instance
+ };
+ var elapsed = now - _startedAt;
+ var res = EvaluateImpl(elapsed, currentValue, ref ctx);
+
+ if (_iterationBehavior == AnimationIterationBehavior.Count
+ && !_finished
+ && elapsed > _totalDuration)
+ {
+ // Active check?
+ TargetObject.Compositor.RemoveFromClock(this);
+ _finished = true;
+ }
+ return res;
+ }
+
+ private ExpressionVariant EvaluateImpl(TimeSpan elapsed, ExpressionVariant currentValue, ref ExpressionEvaluationContext ctx)
+ {
+ if (elapsed < _delayTime)
+ {
+ if (_delayBehavior == AnimationDelayBehavior.SetInitialValueBeforeDelay)
+ return ExpressionVariant.Create(GetKeyFrame(ref ctx, _keyFrames[0]));
+ return currentValue;
+ }
+
+ elapsed -= _delayTime;
+ var iterationNumber = elapsed.Ticks / _duration.Ticks;
+ if (_iterationBehavior == AnimationIterationBehavior.Count
+ && iterationNumber >= _iterationCount)
+ return ExpressionVariant.Create(GetKeyFrame(ref ctx, _keyFrames[_keyFrames.Length - 1]));
+
+
+ var evenIterationNumber = iterationNumber % 2 == 0;
+ elapsed = TimeSpan.FromTicks(elapsed.Ticks % _duration.Ticks);
+
+ var reverse =
+ _direction == PlaybackDirection.Alternate
+ ? !evenIterationNumber
+ : _direction == PlaybackDirection.AlternateReverse
+ ? evenIterationNumber
+ : _direction == PlaybackDirection.Reverse;
+
+ var iterationProgress = elapsed.TotalSeconds / _duration.TotalSeconds;
+ if (reverse)
+ iterationProgress = 1 - iterationProgress;
+
+ var left = new ServerKeyFrame
+ {
+ Value = _startingValue
+ };
+ var right = _keyFrames[_keyFrames.Length - 1];
+ for (var c = 0; c < _keyFrames.Length; c++)
+ {
+ var kf = _keyFrames[c];
+ if (kf.Key < iterationProgress)
+ {
+ // this is the last frame
+ if (c == _keyFrames.Length - 1)
+ return ExpressionVariant.Create(GetKeyFrame(ref ctx, kf));
+
+ left = kf;
+ right = _keyFrames[c + 1];
+ break;
+ }
+ }
+
+ var keyProgress = Math.Max(0, Math.Min(1, (iterationProgress - left.Key) / (right.Key - left.Key)));
+
+ var easedKeyProgress = (float)right.EasingFunction.Ease(keyProgress);
+ if (float.IsNaN(easedKeyProgress) || float.IsInfinity(easedKeyProgress))
+ return currentValue;
+
+ return ExpressionVariant.Create(_interpolator.Interpolate(
+ GetKeyFrame(ref ctx, left),
+ GetKeyFrame(ref ctx, right),
+ easedKeyProgress
+ ));
+ }
+
+ T GetKeyFrame(ref ExpressionEvaluationContext ctx, ServerKeyFrame f)
+ {
+ if (f.Expression != null)
+ return f.Expression.Evaluate(ref ctx).CastOrDefault();
+ else
+ return f.Value;
+ }
+
+ public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property)
+ {
+ _startedAt = startedAt;
+ _startingValue = startingValue.CastOrDefault();
+ var hs = new HashSet<(string, string)>();
+
+ // TODO: Update subscriptions based on the current keyframe rather than keeping subscriptions to all of them
+ foreach (var frame in _keyFrames)
+ frame.Expression?.CollectReferences(hs);
+ Initialize(property, hs);
+ }
+
+ public override void Activate()
+ {
+ TargetObject.Compositor.AddToClock(this);
+ base.Activate();
+ }
+
+ public override void Deactivate()
+ {
+ TargetObject.Compositor.RemoveFromClock(this);
+ base.Deactivate();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs
new file mode 100644
index 0000000000..369cc80b95
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Animation.Easings;
+using Avalonia.Rendering.Composition.Expressions;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+
+ ///
+ /// Collection of composition animation key frames
+ ///
+ ///
+ class KeyFrames : List>, IKeyFrames
+ {
+ void Validate(float key)
+ {
+ if (key < 0 || key > 1)
+ throw new ArgumentException("Key frame key");
+ if (Count > 0 && this[Count - 1].NormalizedProgressKey > key)
+ throw new ArgumentException("Key frame key " + key + " is less than the previous one");
+ }
+
+ public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, IEasing easingFunction)
+ {
+ Validate(normalizedProgressKey);
+ Add(new KeyFrame
+ {
+ NormalizedProgressKey = normalizedProgressKey,
+ Expression = Expression.Parse(value),
+ EasingFunction = easingFunction
+ });
+ }
+
+ public void Insert(float normalizedProgressKey, T value, IEasing easingFunction)
+ {
+ Validate(normalizedProgressKey);
+ Add(new KeyFrame
+ {
+ NormalizedProgressKey = normalizedProgressKey,
+ Value = value,
+ EasingFunction = easingFunction
+ });
+ }
+
+ public ServerKeyFrame[] Snapshot()
+ {
+ var frames = new ServerKeyFrame[Count];
+ for (var c = 0; c < Count; c++)
+ {
+ var f = this[c];
+ frames[c] = new ServerKeyFrame
+ {
+ Expression = f.Expression,
+ Value = f.Value,
+ EasingFunction = f.EasingFunction,
+ Key = f.NormalizedProgressKey
+ };
+ }
+ return frames;
+ }
+ }
+
+ ///
+ /// Composition animation key frame
+ ///
+ struct KeyFrame
+ {
+ public float NormalizedProgressKey;
+ public T Value;
+ public Expression Expression;
+ public IEasing EasingFunction;
+ }
+
+ ///
+ /// Server-side composition animation key frame
+ ///
+ struct ServerKeyFrame
+ {
+ public T Value;
+ public Expression? Expression;
+ public IEasing EasingFunction;
+ public float Key;
+ }
+
+ interface IKeyFrames
+ {
+ public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, IEasing easingFunction);
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs b/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs
new file mode 100644
index 0000000000..fc6cfc9f3d
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using Avalonia.Rendering.Composition.Expressions;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ ///
+ /// A snapshot of properties used by an animation
+ ///
+ internal class PropertySetSnapshot : IExpressionParameterCollection, IExpressionObject
+ {
+ private readonly Dictionary _dic;
+
+ public struct Value
+ {
+ public ExpressionVariant Variant;
+ public IExpressionObject Object;
+
+ public Value(IExpressionObject o)
+ {
+ Object = o;
+ Variant = default;
+ }
+
+ public static implicit operator Value(ExpressionVariant v) => new Value
+ {
+ Variant = v
+ };
+ }
+
+ public PropertySetSnapshot(Dictionary dic)
+ {
+ _dic = dic;
+ }
+
+ public ExpressionVariant GetParameter(string name)
+ {
+ _dic.TryGetValue(name, out var v);
+ return v.Variant;
+ }
+
+ public IExpressionObject GetObjectParameter(string name)
+ {
+ _dic.TryGetValue(name, out var v);
+ return v.Object;
+ }
+
+ public ExpressionVariant GetProperty(string name) => GetParameter(name);
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
new file mode 100644
index 0000000000..a571a0518b
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
@@ -0,0 +1,278 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Runtime.InteropServices;
+using Avalonia.Collections;
+using Avalonia.Collections.Pooled;
+using Avalonia.Media;
+using Avalonia.Rendering.Composition.Drawing;
+using Avalonia.Rendering.Composition.Server;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.Composition;
+
+///
+/// A renderer that utilizes to render the visual tree
+///
+public class CompositingRenderer : IRendererWithCompositor
+{
+ private readonly IRenderRoot _root;
+ private readonly Compositor _compositor;
+ CompositionDrawingContext _recorder = new();
+ DrawingContext _recordingContext;
+ private HashSet _dirty = new();
+ private HashSet _recalculateChildren = new();
+ private bool _queuedUpdate;
+ private Action _update;
+ private Action _invalidateScene;
+
+ internal CompositionTarget CompositionTarget;
+
+ ///
+ /// Asks the renderer to only draw frames on the render thread. Makes Paint to wait until frame is rendered.
+ ///
+ public bool RenderOnlyOnRenderThread { get; set; } = true;
+
+ public CompositingRenderer(IRenderRoot root,
+ Compositor compositor)
+ {
+ _root = root;
+ _compositor = compositor;
+ _recordingContext = new DrawingContext(_recorder);
+ CompositionTarget = compositor.CreateCompositionTarget(root.CreateRenderTarget);
+ CompositionTarget.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor);
+ _update = Update;
+ _invalidateScene = InvalidateScene;
+ }
+
+ ///
+ public bool DrawFps
+ {
+ get => CompositionTarget.DrawFps;
+ set => CompositionTarget.DrawFps = value;
+ }
+
+ ///
+ public bool DrawDirtyRects
+ {
+ get => CompositionTarget.DrawDirtyRects;
+ set => CompositionTarget.DrawDirtyRects = value;
+ }
+
+ ///
+ public event EventHandler? SceneInvalidated;
+
+ void QueueUpdate()
+ {
+ if(_queuedUpdate)
+ return;
+ _queuedUpdate = true;
+ Dispatcher.UIThread.Post(_update, DispatcherPriority.Composition);
+ }
+
+ ///
+ public void AddDirty(IVisual visual)
+ {
+ _dirty.Add((Visual)visual);
+ QueueUpdate();
+ }
+
+ ///
+ public IEnumerable HitTest(Point p, IVisual root, Func? filter)
+ {
+ var res = CompositionTarget.TryHitTest(p, filter);
+ if(res == null)
+ yield break;
+ foreach(var v in res)
+ {
+ if (v is CompositionDrawListVisual dv)
+ {
+ if (filter == null || filter(dv.Visual))
+ yield return dv.Visual;
+ }
+ }
+ }
+
+ ///
+ public IVisual? HitTestFirst(Point p, IVisual root, Func? filter)
+ {
+ // TODO: Optimize
+ return HitTest(p, root, filter).FirstOrDefault();
+ }
+
+ ///
+ public void RecalculateChildren(IVisual visual)
+ {
+ _recalculateChildren.Add((Visual)visual);
+ QueueUpdate();
+ }
+
+ private void SyncChildren(Visual v)
+ {
+ //TODO: Optimize by moving that logic to Visual itself
+ if(v.CompositionVisual == null)
+ return;
+ var compositionChildren = v.CompositionVisual.Children;
+ var visualChildren = (AvaloniaList)v.GetVisualChildren();
+
+ PooledList<(IVisual visual, int index)>? sortedChildren = null;
+ if (v.HasNonUniformZIndexChildren && visualChildren.Count > 1)
+ {
+ sortedChildren = new (visualChildren.Count);
+ for (var c = 0; c < visualChildren.Count; c++)
+ sortedChildren.Add((visualChildren[c], c));
+
+ // Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements.
+ sortedChildren.Sort(static (lhs, rhs) =>
+ {
+ var result = lhs.visual.ZIndex.CompareTo(rhs.visual.ZIndex);
+ return result == 0 ? lhs.index.CompareTo(rhs.index) : result;
+ });
+ }
+
+ if (compositionChildren.Count == visualChildren.Count)
+ {
+ bool mismatch = false;
+ if (v.HasNonUniformZIndexChildren)
+ {
+
+
+ }
+
+ if (sortedChildren != null)
+ for (var c = 0; c < visualChildren.Count; c++)
+ {
+ if (!ReferenceEquals(compositionChildren[c], ((Visual)sortedChildren[c].visual).CompositionVisual))
+ {
+ mismatch = true;
+ break;
+ }
+ }
+ else
+ for (var c = 0; c < visualChildren.Count; c++)
+ if (!ReferenceEquals(compositionChildren[c], ((Visual)visualChildren[c]).CompositionVisual))
+ {
+ mismatch = true;
+ break;
+ }
+
+
+ if (!mismatch)
+ {
+ sortedChildren?.Dispose();
+ return;
+ }
+ }
+
+ compositionChildren.Clear();
+ if (sortedChildren != null)
+ {
+ foreach (var ch in sortedChildren)
+ {
+ var compositionChild = ((Visual)ch.visual).CompositionVisual;
+ if (compositionChild != null)
+ compositionChildren.Add(compositionChild);
+ }
+ sortedChildren.Dispose();
+ }
+ else
+ foreach (var ch in v.GetVisualChildren())
+ {
+ var compositionChild = ((Visual)ch).CompositionVisual;
+ if (compositionChild != null)
+ compositionChildren.Add(compositionChild);
+ }
+ }
+
+ private void InvalidateScene() =>
+ SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize)));
+
+ private void Update()
+ {
+ _queuedUpdate = false;
+ foreach (var visual in _dirty)
+ {
+ var comp = visual.CompositionVisual;
+ if(comp == null)
+ continue;
+
+ // TODO: Optimize all of that by moving to the Visual itself, so we won't have to recalculate every time
+ comp.Offset = new Vector3((float)visual.Bounds.Left, (float)visual.Bounds.Top, 0);
+ comp.Size = new Vector2((float)visual.Bounds.Width, (float)visual.Bounds.Height);
+ comp.Visible = visual.IsVisible;
+ comp.Opacity = (float)visual.Opacity;
+ comp.ClipToBounds = visual.ClipToBounds;
+ comp.Clip = visual.Clip?.PlatformImpl;
+ comp.OpacityMask = visual.OpacityMask;
+
+ var renderTransform = Matrix.Identity;
+
+ if (visual.HasMirrorTransform)
+ renderTransform = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0);
+
+ if (visual.RenderTransform != null)
+ {
+ var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
+ var offset = Matrix.CreateTranslation(origin);
+ renderTransform *= (-offset) * visual.RenderTransform.Value * (offset);
+ }
+
+
+
+ comp.TransformMatrix = MatrixUtils.ToMatrix4x4(renderTransform);
+
+ _recorder.BeginUpdate(comp.DrawList);
+ visual.Render(_recordingContext);
+ comp.DrawList = _recorder.EndUpdate();
+
+ SyncChildren(visual);
+ }
+ foreach(var v in _recalculateChildren)
+ if (!_dirty.Contains(v))
+ SyncChildren(v);
+ _dirty.Clear();
+ _recalculateChildren.Clear();
+ CompositionTarget.Size = _root.ClientSize;
+ CompositionTarget.Scaling = _root.RenderScaling;
+ Compositor.InvokeOnNextCommit(_invalidateScene);
+ }
+
+ public void Resized(Size size)
+ {
+ }
+
+ public void Paint(Rect rect)
+ {
+ Update();
+ CompositionTarget.RequestRedraw();
+ if(RenderOnlyOnRenderThread && Compositor.Loop.RunsInBackground)
+ Compositor.RequestCommitAsync().Wait();
+ else
+ CompositionTarget.ImmediateUIThreadRender();
+ }
+
+ public void Start() => CompositionTarget.IsEnabled = true;
+
+ public void Stop()
+ {
+ CompositionTarget.IsEnabled = false;
+ }
+
+ public void Dispose()
+ {
+ Stop();
+ CompositionTarget.Dispose();
+
+ // Wait for the composition batch to be applied and rendered to guarantee that
+ // render target is not used anymore and can be safely disposed
+ if (Compositor.Loop.RunsInBackground)
+ _compositor.RequestCommitAsync().Wait();
+ }
+
+ ///
+ /// The associated object
+ ///
+ public Compositor Compositor => _compositor;
+}
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
new file mode 100644
index 0000000000..49aea1c3dc
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Numerics;
+using Avalonia.Rendering.Composition.Drawing;
+using Avalonia.Rendering.Composition.Server;
+using Avalonia.Rendering.Composition.Transport;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.Composition;
+
+
+///
+/// A composition visual that holds a list of drawing commands issued by
+///
+internal class CompositionDrawListVisual : CompositionContainerVisual
+{
+ ///
+ /// The associated
+ ///
+ public Visual Visual { get; }
+
+ private bool _drawListChanged;
+ private CompositionDrawList? _drawList;
+
+ ///
+ /// The list of drawing commands
+ ///
+ public CompositionDrawList? DrawList
+ {
+ get => _drawList;
+ set
+ {
+ _drawList?.Dispose();
+ _drawList = value;
+ _drawListChanged = true;
+ RegisterForSerialization();
+ }
+ }
+
+ private protected override void SerializeChangesCore(BatchStreamWriter writer)
+ {
+ writer.Write((byte)(_drawListChanged ? 1 : 0));
+ if (_drawListChanged)
+ {
+ writer.WriteObject(DrawList?.Clone());
+ _drawListChanged = false;
+ }
+ base.SerializeChangesCore(writer);
+ }
+
+ internal CompositionDrawListVisual(Compositor compositor, ServerCompositionDrawListVisual server, Visual visual) : base(compositor, server)
+ {
+ Visual = visual;
+ }
+
+ internal override bool HitTest(Point pt, Func? filter)
+ {
+ var custom = Visual as ICustomHitTest;
+ if (DrawList == null && custom == null)
+ return false;
+ if (filter != null && !filter(Visual))
+ return false;
+ if (custom != null)
+ {
+ // Simulate the old behavior
+ // TODO: Change behavior once legacy renderers are removed
+ pt += new Point(Offset.X, Offset.Y);
+ return custom.HitTest(pt);
+ }
+
+ foreach (var op in DrawList!)
+ if (op.Item.HitTest(pt))
+ return true;
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs
new file mode 100644
index 0000000000..f529ee9cff
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs
@@ -0,0 +1,141 @@
+using System;
+using Avalonia.Rendering.Composition.Animations;
+using Avalonia.Rendering.Composition.Expressions;
+using Avalonia.Rendering.Composition.Server;
+using Avalonia.Rendering.Composition.Transport;
+using Avalonia.Utilities;
+
+namespace Avalonia.Rendering.Composition
+{
+ ///
+ /// Base class of the composition API representing a node in the visual tree structure.
+ /// Composition objects are the visual tree structure on which all other features of the composition API use and build on.
+ /// The API allows developers to define and create one or many objects each representing a single node in a Visual tree.
+ ///
+ public abstract class CompositionObject : IDisposable
+ {
+ ///
+ /// The collection of implicit animations attached to this object.
+ ///
+ public ImplicitAnimationCollection? ImplicitAnimations { get; set; }
+
+ private protected InlineDictionary PendingAnimations;
+ internal CompositionObject(Compositor compositor, ServerObject server)
+ {
+ Compositor = compositor;
+ Server = server;
+ }
+
+ ///
+ /// The associated Compositor
+ ///
+ public Compositor Compositor { get; }
+ internal ServerObject Server { get; }
+ public bool IsDisposed { get; private set; }
+ private bool _registeredForSerialization;
+
+ private static void ThrowInvalidOperation() =>
+ throw new InvalidOperationException("There is no server-side counterpart for this object");
+
+ public void Dispose()
+ {
+ RegisterForSerialization();
+ IsDisposed = true;
+ }
+
+ ///
+ /// Connects an animation with the specified property of the object and starts the animation.
+ ///
+ public void StartAnimation(string propertyName, CompositionAnimation animation)
+ => StartAnimation(propertyName, animation, null);
+
+ internal virtual void StartAnimation(string propertyName, CompositionAnimation animation, ExpressionVariant? finalValue)
+ {
+ throw new ArgumentException("Unknown property " + propertyName);
+ }
+
+ ///
+ /// Starts an animation group.
+ /// The StartAnimationGroup method on CompositionObject lets you start CompositionAnimationGroup.
+ /// All the animations in the group will be started at the same time on the object.
+ ///
+ public void StartAnimationGroup(ICompositionAnimationBase grp)
+ {
+ if (grp is CompositionAnimation animation)
+ {
+ if(animation.Target == null)
+ throw new ArgumentException("Animation Target can't be null");
+ StartAnimation(animation.Target, animation);
+ }
+ else if (grp is CompositionAnimationGroup group)
+ {
+ foreach (var a in group.Animations)
+ {
+ if (a.Target == null)
+ throw new ArgumentException("Animation Target can't be null");
+ StartAnimation(a.Target, a);
+ }
+ }
+ }
+
+ bool StartAnimationGroupPart(CompositionAnimation animation, string target, ExpressionVariant finalValue)
+ {
+ if(animation.Target == null)
+ throw new ArgumentException("Animation Target can't be null");
+ if (animation.Target == target)
+ {
+ StartAnimation(animation.Target, animation, finalValue);
+ return true;
+ }
+ else
+ {
+ StartAnimation(animation.Target, animation);
+ return false;
+ }
+ }
+
+ internal bool StartAnimationGroup(ICompositionAnimationBase grp, string target, ExpressionVariant finalValue)
+ {
+ if (grp is CompositionAnimation animation)
+ return StartAnimationGroupPart(animation, target, finalValue);
+ if (grp is CompositionAnimationGroup group)
+ {
+ var matched = false;
+ foreach (var a in group.Animations)
+ {
+ if (a.Target == null)
+ throw new ArgumentException("Animation Target can't be null");
+ if (StartAnimationGroupPart(a, target, finalValue))
+ matched = true;
+ }
+
+ return matched;
+ }
+
+ throw new ArgumentException();
+ }
+
+ protected void RegisterForSerialization()
+ {
+ if (Server == null)
+ throw new InvalidOperationException("The object doesn't have an associated server counterpart");
+
+ if(_registeredForSerialization)
+ return;
+ _registeredForSerialization = true;
+ Compositor.RegisterForSerialization(this);
+ }
+
+ internal void SerializeChanges(BatchStreamWriter writer)
+ {
+ _registeredForSerialization = false;
+ SerializeChangesCore(writer);
+ }
+
+ private protected virtual void SerializeChangesCore(BatchStreamWriter writer)
+ {
+ if (Server is IDisposable)
+ writer.Write((byte)(IsDisposed ? 1 : 0));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs
new file mode 100644
index 0000000000..ee4552d154
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using Avalonia.Rendering.Composition.Animations;
+using Avalonia.Rendering.Composition.Expressions;
+using Avalonia.Rendering.Composition.Transport;
+
+namespace Avalonia.Rendering.Composition
+{
+ ///
+ /// s are s that allow storage of key values pairs
+ /// that can be shared across the application and are not tied to the lifetime of another composition object.
+ /// s are most commonly used with animations, where they maintain key-value pairs
+ /// that are referenced to drive portions of composition animations. s
+ /// provide the ability to insert key-value pairs or retrieve a value for a given key.
+ /// does not support a delete function – ensure you use
+ /// to store values that will be shared across the application.
+ ///
+ public class CompositionPropertySet : CompositionObject
+ {
+ private readonly Dictionary _variants = new Dictionary();
+ private readonly Dictionary _objects = new Dictionary();
+
+ internal CompositionPropertySet(Compositor compositor) : base(compositor, null!)
+ {
+ }
+
+ internal void Set(string key, ExpressionVariant value)
+ {
+ _objects.Remove(key);
+ _variants[key] = value;
+ }
+
+ /*
+ For INTERNAL USE by CompositionAnimation ONLY, we DON'T support expression
+ paths like SomeParam.SomePropertyObject.SomeValue
+ */
+ internal void Set(string key, CompositionObject obj)
+ {
+ _objects[key] = obj ?? throw new ArgumentNullException(nameof(obj));
+ _variants.Remove(key);
+ }
+
+ public void InsertColor(string propertyName, Avalonia.Media.Color value) => Set(propertyName, value);
+
+ public void InsertMatrix3x2(string propertyName, Matrix3x2 value) => Set(propertyName, value);
+
+ public void InsertMatrix4x4(string propertyName, Matrix4x4 value) => Set(propertyName, value);
+
+ public void InsertQuaternion(string propertyName, Quaternion value) => Set(propertyName, value);
+
+ public void InsertScalar(string propertyName, float value) => Set(propertyName, value);
+ public void InsertVector2(string propertyName, Vector2 value) => Set(propertyName, value);
+
+ public void InsertVector3(string propertyName, Vector3 value) => Set(propertyName, value);
+
+ public void InsertVector4(string propertyName, Vector4 value) => Set(propertyName, value);
+
+
+ CompositionGetValueStatus TryGetVariant(string key, out T value) where T : struct
+ {
+ value = default;
+ if (!_variants.TryGetValue(key, out var v))
+ return _objects.ContainsKey(key)
+ ? CompositionGetValueStatus.TypeMismatch
+ : CompositionGetValueStatus.NotFound;
+
+ return v.TryCast(out value) ? CompositionGetValueStatus.Succeeded : CompositionGetValueStatus.TypeMismatch;
+ }
+
+ public CompositionGetValueStatus TryGetColor(string propertyName, out Avalonia.Media.Color value)
+ => TryGetVariant(propertyName, out value);
+
+ public CompositionGetValueStatus TryGetMatrix3x2(string propertyName, out Matrix3x2 value)
+ => TryGetVariant(propertyName, out value);
+
+ public CompositionGetValueStatus TryGetMatrix4x4(string propertyName, out Matrix4x4 value)
+ => TryGetVariant(propertyName, out value);
+
+ public CompositionGetValueStatus TryGetQuaternion(string propertyName, out Quaternion value)
+ => TryGetVariant(propertyName, out value);
+
+
+ public CompositionGetValueStatus TryGetScalar(string propertyName, out float value)
+ => TryGetVariant(propertyName, out value);
+
+ public CompositionGetValueStatus TryGetVector2(string propertyName, out Vector2 value)
+ => TryGetVariant(propertyName, out value);
+
+ public CompositionGetValueStatus TryGetVector3(string propertyName, out Vector3 value)
+ => TryGetVariant(propertyName, out value);
+
+ public CompositionGetValueStatus TryGetVector4(string propertyName, out Vector4 value)
+ => TryGetVariant(propertyName, out value);
+
+
+ public void InsertBoolean(string propertyName, bool value) => Set(propertyName, value);
+
+ public CompositionGetValueStatus TryGetBoolean(string propertyName, out bool value)
+ => TryGetVariant(propertyName, out value);
+
+ internal void ClearAll()
+ {
+ _objects.Clear();
+ _variants.Clear();
+ }
+
+ internal void Clear(string key)
+ {
+ _objects.Remove(key);
+ _variants.Remove(key);
+ }
+
+ internal PropertySetSnapshot Snapshot() =>
+ SnapshotCore(1);
+
+ private PropertySetSnapshot SnapshotCore(int allowedNestingLevel)
+ {
+ var dic = new Dictionary(_objects.Count + _variants.Count);
+ foreach (var o in _objects)
+ {
+ if (o.Value is CompositionPropertySet ps)
+ {
+ if (allowedNestingLevel <= 0)
+ throw new InvalidOperationException("PropertySet depth limit reached");
+ dic[o.Key] = new PropertySetSnapshot.Value(ps.SnapshotCore(allowedNestingLevel - 1));
+ }
+ else if (o.Value.Server == null)
+ throw new InvalidOperationException($"Object of type {o.Value.GetType()} is not allowed");
+ else
+ dic[o.Key] = new PropertySetSnapshot.Value(o.Value.Server);
+ }
+
+ foreach (var v in _variants)
+ dic[v.Key] = v.Value;
+
+ return new PropertySetSnapshot(dic);
+ }
+ }
+
+ public enum CompositionGetValueStatus
+ {
+ Succeeded,
+ TypeMismatch,
+ NotFound
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
new file mode 100644
index 0000000000..01b2d0d5d9
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using Avalonia.Collections.Pooled;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.Composition
+{
+ ///
+ /// Represents the composition output (e. g. a window, embedded control, entire screen)
+ ///
+ public partial class CompositionTarget
+ {
+ partial void OnRootChanged()
+ {
+ if (Root != null)
+ Root.Root = this;
+ }
+
+ partial void OnRootChanging()
+ {
+ if (Root != null)
+ Root.Root = null;
+ }
+
+ ///
+ /// Attempts to perform a hit-tst
+ ///
+ ///
+ ///
+ ///
+ public PooledList? TryHitTest(Point point, Func? filter)
+ {
+ Server.Readback.NextRead();
+ if (Root == null)
+ return null;
+ var res = new PooledList();
+ HitTestCore(Root, point, res, filter);
+ return res;
+ }
+
+ ///
+ /// Attempts to transform a point to a particular CompositionVisual coordinate space
+ ///
+ ///
+ public Point? TryTransformToVisual(CompositionVisual visual, Point point)
+ {
+ if (visual.Root != this)
+ return null;
+ var v = visual;
+ var m = Matrix.Identity;
+ while (v != null)
+ {
+ if (!TryGetInvertedTransform(v, out var cm))
+ return null;
+ m = m * cm;
+ v = v.Parent;
+ }
+
+ return point * m;
+ }
+
+ bool TryGetInvertedTransform(CompositionVisual visual, out Matrix matrix)
+ {
+ var m = visual.TryGetServerGlobalTransform();
+ if (m == null)
+ {
+ matrix = default;
+ return false;
+ }
+
+ var m33 = MatrixUtils.ToMatrix(m.Value);
+ return m33.TryInvert(out matrix);
+ }
+
+ bool TryTransformTo(CompositionVisual visual, Point globalPoint, out Point v)
+ {
+ v = default;
+ if (TryGetInvertedTransform(visual, out var m))
+ {
+ v = globalPoint * m;
+ return true;
+ }
+
+ return false;
+ }
+
+ void HitTestCore(CompositionVisual visual, Point globalPoint, PooledList result,
+ Func? filter)
+ {
+ if (visual.Visible == false)
+ return;
+ if (!TryTransformTo(visual, globalPoint, out var point))
+ return;
+
+ if (visual.ClipToBounds
+ && (point.X < 0 || point.Y < 0 || point.X > visual.Size.X || point.Y > visual.Size.Y))
+ return;
+
+ if (visual.Clip?.FillContains(point) == false)
+ return;
+
+ // Inspect children
+ if (visual is CompositionContainerVisual cv)
+ for (var c = cv.Children.Count - 1; c >= 0; c--)
+ {
+ var ch = cv.Children[c];
+ HitTestCore(ch, globalPoint, result, filter);
+ }
+
+ // Hit-test the current node
+ if (visual.HitTest(point, filter))
+ result.Add(visual);
+ }
+
+ ///
+ /// Registers the composition target for explicit redraw
+ ///
+ public void RequestRedraw() => RegisterForSerialization();
+
+ ///
+ /// Performs composition directly on the UI thread
+ ///
+ internal void ImmediateUIThreadRender()
+ {
+ Compositor.RequestCommitAsync();
+ Compositor.Server.Render();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs
new file mode 100644
index 0000000000..1bdae44cb9
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Numerics;
+using System.Threading.Tasks;
+using Avalonia.Animation.Easings;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.Rendering.Composition.Animations;
+using Avalonia.Rendering.Composition.Server;
+using Avalonia.Rendering.Composition.Transport;
+using Avalonia.Threading;
+
+
+namespace Avalonia.Rendering.Composition
+{
+ ///
+ /// The Compositor class manages communication between UI-thread and render-thread parts of the composition engine.
+ /// It also serves as a factory to create UI-thread parts of various composition objects
+ ///
+ public partial class Compositor
+ {
+ internal IRenderLoop Loop { get; }
+ private ServerCompositor _server;
+ private bool _implicitBatchCommitQueued;
+ private Action _implicitBatchCommit;
+ private BatchStreamObjectPool