diff --git a/.gitignore b/.gitignore
index 4e61c3c8e..b3124ebd6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -232,5 +232,3 @@ SixLabors.Shapes.Coverage.xml
.codex-*
.claude
/plans/
-
-samples/Avalonia**
diff --git a/ImageSharp.Drawing.All.sln b/ImageSharp.Drawing.All.sln
index 7b317194c..bac39415d 100644
--- a/ImageSharp.Drawing.All.sln
+++ b/ImageSharp.Drawing.All.sln
@@ -318,10 +318,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Png", "Png", "{E1C42A6F-913
tests\Images\Input\Png\zlib-overflow.png = tests\Images\Input\Png\zlib-overflow.png
EndProjectSection
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp.Drawing.Tests", "tests\ImageSharp.Drawing.Tests\ImageSharp.Drawing.Tests.csproj", "{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp.Drawing.Benchmarks", "tests\ImageSharp.Drawing.Benchmarks\ImageSharp.Drawing.Benchmarks.csproj", "{59804113-1DD4-4F80-8D06-35FF71652508}"
-EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SharedInfrastructure", "shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.shproj", "{68A8CC40-6AED-4E96-B524-31B1158FDEEA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{23859314-5693-4E6C-BE5C-80A433439D2A}"
@@ -329,22 +325,30 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
.github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml
EndProjectSection
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.WebGPU", "src\ImageSharp.Drawing.WebGPU\ImageSharp.Drawing.WebGPU.csproj", "{061582C2-658F-40AE-A978-7D74A4EB2C0A}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.WebGPU.ShaderGen", "src\ImageSharp.Drawing.WebGPU.ShaderGen\ImageSharp.Drawing.WebGPU.ShaderGen.csproj", "{C7606104-5D58-4670-912C-3F336606B02D}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.ManualBenchmarks", "tests\ImageSharp.Drawing.ManualBenchmarks\ImageSharp.Drawing.ManualBenchmarks.csproj", "{70193989-E587-451A-AF34-4C74FEE5DA5A}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebGPUWindowDemo", "samples\WebGPUWindowDemo\WebGPUWindowDemo.csproj", "{999D90DE-6940-4693-9F80-701F2D5DB77A}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrawShapesWithImageSharp", "samples\DrawShapesWithImageSharp\DrawShapesWithImageSharp.csproj", "{43A3CA46-2675-4AD4-88FC-DDC2E657F0B7}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebGPUExternalSurfaceDemo", "samples\WebGPUExternalSurfaceDemo\WebGPUExternalSurfaceDemo.csproj", "{F0AAE591-A44E-48A5-8C74-395BDF98F646}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AvaloniaControlCatalog", "samples\AvaloniaControlCatalog\AvaloniaControlCatalog.csproj", "{333F3D4F-D251-5601-4851-307FF0F13D23}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrawingBackendBenchmark", "samples\DrawingBackendBenchmark\DrawingBackendBenchmark.csproj", "{B5CF5531-624F-9F49-46BD-937FF4062F54}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebGPUExternalSurfaceDemo", "samples\WebGPUExternalSurfaceDemo\WebGPUExternalSurfaceDemo.csproj", "{97619DEB-226C-EB67-140D-0C7E67B2F7D0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebGPUWindowDemo", "samples\WebGPUWindowDemo\WebGPUWindowDemo.csproj", "{4570F31D-D00F-6A80-DB7C-74826CBD15AE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.WebGPU", "src\ImageSharp.Drawing.WebGPU\ImageSharp.Drawing.WebGPU.csproj", "{638FC2AE-7FC1-6527-E051-C6B34803F08D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.Tests", "tests\ImageSharp.Drawing.Tests\ImageSharp.Drawing.Tests.csproj", "{D73D1783-E5F5-5094-6D3E-D77B45A51B4A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.Benchmarks", "tests\ImageSharp.Drawing.Benchmarks\ImageSharp.Drawing.Benchmarks.csproj", "{78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.ManualBenchmarks", "tests\ImageSharp.Drawing.ManualBenchmarks\ImageSharp.Drawing.ManualBenchmarks.csproj", "{62B1D30A-935F-819E-A227-A7FFF2E12CC1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.Renderers.Avalonia", "src\ImageSharp.Drawing.Renderers.Avalonia\ImageSharp.Drawing.Renderers.Avalonia.csproj", "{7DE3802B-CCA4-48C7-9822-007393C1C4BC}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -367,42 +371,6 @@ Global
{2E33181E-6E28-4662-A801-E2E7DC206029}.Release|x64.Build.0 = Release|Any CPU
{2E33181E-6E28-4662-A801-E2E7DC206029}.Release|x86.ActiveCfg = Release|Any CPU
{2E33181E-6E28-4662-A801-E2E7DC206029}.Release|x86.Build.0 = Release|Any CPU
- {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|x64.ActiveCfg = Debug|Any CPU
- {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|x64.Build.0 = Debug|Any CPU
- {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|x86.ActiveCfg = Debug|Any CPU
- {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|x86.Build.0 = Debug|Any CPU
- {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.Build.0 = Release|Any CPU
- {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|x64.ActiveCfg = Release|Any CPU
- {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|x64.Build.0 = Release|Any CPU
- {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|x86.ActiveCfg = Release|Any CPU
- {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|x86.Build.0 = Release|Any CPU
- {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|x64.ActiveCfg = Debug|Any CPU
- {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|x64.Build.0 = Debug|Any CPU
- {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|x86.ActiveCfg = Debug|Any CPU
- {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|x86.Build.0 = Debug|Any CPU
- {59804113-1DD4-4F80-8D06-35FF71652508}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {59804113-1DD4-4F80-8D06-35FF71652508}.Release|Any CPU.Build.0 = Release|Any CPU
- {59804113-1DD4-4F80-8D06-35FF71652508}.Release|x64.ActiveCfg = Release|Any CPU
- {59804113-1DD4-4F80-8D06-35FF71652508}.Release|x64.Build.0 = Release|Any CPU
- {59804113-1DD4-4F80-8D06-35FF71652508}.Release|x86.ActiveCfg = Release|Any CPU
- {59804113-1DD4-4F80-8D06-35FF71652508}.Release|x86.Build.0 = Release|Any CPU
- {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|x64.ActiveCfg = Debug|Any CPU
- {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|x64.Build.0 = Debug|Any CPU
- {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|x86.ActiveCfg = Debug|Any CPU
- {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|x86.Build.0 = Debug|Any CPU
- {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|Any CPU.Build.0 = Release|Any CPU
- {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|x64.ActiveCfg = Release|Any CPU
- {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|x64.Build.0 = Release|Any CPU
- {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|x86.ActiveCfg = Release|Any CPU
- {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|x86.Build.0 = Release|Any CPU
{C7606104-5D58-4670-912C-3F336606B02D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C7606104-5D58-4670-912C-3F336606B02D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C7606104-5D58-4670-912C-3F336606B02D}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -415,30 +383,6 @@ Global
{C7606104-5D58-4670-912C-3F336606B02D}.Release|x64.Build.0 = Release|Any CPU
{C7606104-5D58-4670-912C-3F336606B02D}.Release|x86.ActiveCfg = Release|Any CPU
{C7606104-5D58-4670-912C-3F336606B02D}.Release|x86.Build.0 = Release|Any CPU
- {70193989-E587-451A-AF34-4C74FEE5DA5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {70193989-E587-451A-AF34-4C74FEE5DA5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {70193989-E587-451A-AF34-4C74FEE5DA5A}.Debug|x64.ActiveCfg = Debug|Any CPU
- {70193989-E587-451A-AF34-4C74FEE5DA5A}.Debug|x64.Build.0 = Debug|Any CPU
- {70193989-E587-451A-AF34-4C74FEE5DA5A}.Debug|x86.ActiveCfg = Debug|Any CPU
- {70193989-E587-451A-AF34-4C74FEE5DA5A}.Debug|x86.Build.0 = Debug|Any CPU
- {70193989-E587-451A-AF34-4C74FEE5DA5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {70193989-E587-451A-AF34-4C74FEE5DA5A}.Release|Any CPU.Build.0 = Release|Any CPU
- {70193989-E587-451A-AF34-4C74FEE5DA5A}.Release|x64.ActiveCfg = Release|Any CPU
- {70193989-E587-451A-AF34-4C74FEE5DA5A}.Release|x64.Build.0 = Release|Any CPU
- {70193989-E587-451A-AF34-4C74FEE5DA5A}.Release|x86.ActiveCfg = Release|Any CPU
- {70193989-E587-451A-AF34-4C74FEE5DA5A}.Release|x86.Build.0 = Release|Any CPU
- {999D90DE-6940-4693-9F80-701F2D5DB77A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {999D90DE-6940-4693-9F80-701F2D5DB77A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {999D90DE-6940-4693-9F80-701F2D5DB77A}.Debug|x64.ActiveCfg = Debug|Any CPU
- {999D90DE-6940-4693-9F80-701F2D5DB77A}.Debug|x64.Build.0 = Debug|Any CPU
- {999D90DE-6940-4693-9F80-701F2D5DB77A}.Debug|x86.ActiveCfg = Debug|Any CPU
- {999D90DE-6940-4693-9F80-701F2D5DB77A}.Debug|x86.Build.0 = Debug|Any CPU
- {999D90DE-6940-4693-9F80-701F2D5DB77A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {999D90DE-6940-4693-9F80-701F2D5DB77A}.Release|Any CPU.Build.0 = Release|Any CPU
- {999D90DE-6940-4693-9F80-701F2D5DB77A}.Release|x64.ActiveCfg = Release|Any CPU
- {999D90DE-6940-4693-9F80-701F2D5DB77A}.Release|x64.Build.0 = Release|Any CPU
- {999D90DE-6940-4693-9F80-701F2D5DB77A}.Release|x86.ActiveCfg = Release|Any CPU
- {999D90DE-6940-4693-9F80-701F2D5DB77A}.Release|x86.Build.0 = Release|Any CPU
{43A3CA46-2675-4AD4-88FC-DDC2E657F0B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{43A3CA46-2675-4AD4-88FC-DDC2E657F0B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{43A3CA46-2675-4AD4-88FC-DDC2E657F0B7}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -451,18 +395,18 @@ Global
{43A3CA46-2675-4AD4-88FC-DDC2E657F0B7}.Release|x64.Build.0 = Release|Any CPU
{43A3CA46-2675-4AD4-88FC-DDC2E657F0B7}.Release|x86.ActiveCfg = Release|Any CPU
{43A3CA46-2675-4AD4-88FC-DDC2E657F0B7}.Release|x86.Build.0 = Release|Any CPU
- {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Debug|x64.ActiveCfg = Debug|Any CPU
- {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Debug|x64.Build.0 = Debug|Any CPU
- {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Debug|x86.ActiveCfg = Debug|Any CPU
- {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Debug|x86.Build.0 = Debug|Any CPU
- {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Release|Any CPU.Build.0 = Release|Any CPU
- {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Release|x64.ActiveCfg = Release|Any CPU
- {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Release|x64.Build.0 = Release|Any CPU
- {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Release|x86.ActiveCfg = Release|Any CPU
- {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Release|x86.Build.0 = Release|Any CPU
+ {333F3D4F-D251-5601-4851-307FF0F13D23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {333F3D4F-D251-5601-4851-307FF0F13D23}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {333F3D4F-D251-5601-4851-307FF0F13D23}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {333F3D4F-D251-5601-4851-307FF0F13D23}.Debug|x64.Build.0 = Debug|Any CPU
+ {333F3D4F-D251-5601-4851-307FF0F13D23}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {333F3D4F-D251-5601-4851-307FF0F13D23}.Debug|x86.Build.0 = Debug|Any CPU
+ {333F3D4F-D251-5601-4851-307FF0F13D23}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {333F3D4F-D251-5601-4851-307FF0F13D23}.Release|Any CPU.Build.0 = Release|Any CPU
+ {333F3D4F-D251-5601-4851-307FF0F13D23}.Release|x64.ActiveCfg = Release|Any CPU
+ {333F3D4F-D251-5601-4851-307FF0F13D23}.Release|x64.Build.0 = Release|Any CPU
+ {333F3D4F-D251-5601-4851-307FF0F13D23}.Release|x86.ActiveCfg = Release|Any CPU
+ {333F3D4F-D251-5601-4851-307FF0F13D23}.Release|x86.Build.0 = Release|Any CPU
{B5CF5531-624F-9F49-46BD-937FF4062F54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B5CF5531-624F-9F49-46BD-937FF4062F54}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B5CF5531-624F-9F49-46BD-937FF4062F54}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -475,6 +419,90 @@ Global
{B5CF5531-624F-9F49-46BD-937FF4062F54}.Release|x64.Build.0 = Release|Any CPU
{B5CF5531-624F-9F49-46BD-937FF4062F54}.Release|x86.ActiveCfg = Release|Any CPU
{B5CF5531-624F-9F49-46BD-937FF4062F54}.Release|x86.Build.0 = Release|Any CPU
+ {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Debug|x64.Build.0 = Debug|Any CPU
+ {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Debug|x86.Build.0 = Debug|Any CPU
+ {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Release|x64.ActiveCfg = Release|Any CPU
+ {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Release|x64.Build.0 = Release|Any CPU
+ {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Release|x86.ActiveCfg = Release|Any CPU
+ {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Release|x86.Build.0 = Release|Any CPU
+ {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Debug|x64.Build.0 = Debug|Any CPU
+ {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Debug|x86.Build.0 = Debug|Any CPU
+ {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Release|x64.ActiveCfg = Release|Any CPU
+ {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Release|x64.Build.0 = Release|Any CPU
+ {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Release|x86.ActiveCfg = Release|Any CPU
+ {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Release|x86.Build.0 = Release|Any CPU
+ {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Debug|x64.Build.0 = Debug|Any CPU
+ {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Debug|x86.Build.0 = Debug|Any CPU
+ {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Release|x64.ActiveCfg = Release|Any CPU
+ {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Release|x64.Build.0 = Release|Any CPU
+ {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Release|x86.ActiveCfg = Release|Any CPU
+ {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Release|x86.Build.0 = Release|Any CPU
+ {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Debug|x64.ActiveCfg = Debug|x64
+ {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Debug|x64.Build.0 = Debug|x64
+ {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Debug|x86.ActiveCfg = Debug|x86
+ {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Debug|x86.Build.0 = Debug|x86
+ {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Release|x64.ActiveCfg = Release|x64
+ {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Release|x64.Build.0 = Release|x64
+ {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Release|x86.ActiveCfg = Release|x86
+ {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Release|x86.Build.0 = Release|x86
+ {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Debug|x64.Build.0 = Debug|Any CPU
+ {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Debug|x86.Build.0 = Debug|Any CPU
+ {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Release|x64.ActiveCfg = Release|Any CPU
+ {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Release|x64.Build.0 = Release|Any CPU
+ {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Release|x86.ActiveCfg = Release|Any CPU
+ {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Release|x86.Build.0 = Release|Any CPU
+ {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Debug|x64.Build.0 = Debug|Any CPU
+ {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Debug|x86.Build.0 = Debug|Any CPU
+ {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Release|x64.ActiveCfg = Release|Any CPU
+ {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Release|x64.Build.0 = Release|Any CPU
+ {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Release|x86.ActiveCfg = Release|Any CPU
+ {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Release|x86.Build.0 = Release|Any CPU
+ {7DE3802B-CCA4-48C7-9822-007393C1C4BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7DE3802B-CCA4-48C7-9822-007393C1C4BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7DE3802B-CCA4-48C7-9822-007393C1C4BC}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7DE3802B-CCA4-48C7-9822-007393C1C4BC}.Debug|x64.Build.0 = Debug|Any CPU
+ {7DE3802B-CCA4-48C7-9822-007393C1C4BC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7DE3802B-CCA4-48C7-9822-007393C1C4BC}.Debug|x86.Build.0 = Debug|Any CPU
+ {7DE3802B-CCA4-48C7-9822-007393C1C4BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7DE3802B-CCA4-48C7-9822-007393C1C4BC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7DE3802B-CCA4-48C7-9822-007393C1C4BC}.Release|x64.ActiveCfg = Release|Any CPU
+ {7DE3802B-CCA4-48C7-9822-007393C1C4BC}.Release|x64.Build.0 = Release|Any CPU
+ {7DE3802B-CCA4-48C7-9822-007393C1C4BC}.Release|x86.ActiveCfg = Release|Any CPU
+ {7DE3802B-CCA4-48C7-9822-007393C1C4BC}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -497,23 +525,26 @@ Global
{6458AFCB-A159-47D5-8F2B-50C95C0915E0} = {DB21FED7-E8CB-4B00-9EB2-9144D32A590A}
{39F5197B-CF6C-41A5-9739-7F97E78BB104} = {6458AFCB-A159-47D5-8F2B-50C95C0915E0}
{E1C42A6F-913B-4A7B-B1A8-2BB62843B254} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
- {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
- {59804113-1DD4-4F80-8D06-35FF71652508} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
{68A8CC40-6AED-4E96-B524-31B1158FDEEA} = {815C0625-CD3D-440F-9F80-2D83856AB7AE}
{23859314-5693-4E6C-BE5C-80A433439D2A} = {1799C43E-5C54-4A8F-8D64-B1475241DB0D}
- {061582C2-658F-40AE-A978-7D74A4EB2C0A} = {815C0625-CD3D-440F-9F80-2D83856AB7AE}
{C7606104-5D58-4670-912C-3F336606B02D} = {815C0625-CD3D-440F-9F80-2D83856AB7AE}
- {70193989-E587-451A-AF34-4C74FEE5DA5A} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
- {999D90DE-6940-4693-9F80-701F2D5DB77A} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070}
{43A3CA46-2675-4AD4-88FC-DDC2E657F0B7} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070}
- {F0AAE591-A44E-48A5-8C74-395BDF98F646} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070}
+ {333F3D4F-D251-5601-4851-307FF0F13D23} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070}
{B5CF5531-624F-9F49-46BD-937FF4062F54} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070}
+ {97619DEB-226C-EB67-140D-0C7E67B2F7D0} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070}
+ {4570F31D-D00F-6A80-DB7C-74826CBD15AE} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070}
+ {638FC2AE-7FC1-6527-E051-C6B34803F08D} = {815C0625-CD3D-440F-9F80-2D83856AB7AE}
+ {D73D1783-E5F5-5094-6D3E-D77B45A51B4A} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
+ {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
+ {62B1D30A-935F-819E-A227-A7FFF2E12CC1} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
+ {7DE3802B-CCA4-48C7-9822-007393C1C4BC} = {815C0625-CD3D-440F-9F80-2D83856AB7AE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795}
EndGlobalSection
GlobalSection(SharedMSBuildProjectFiles) = preSolution
shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{2e33181e-6e28-4662-a801-e2e7dc206029}*SharedItemsImports = 5
+ shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{638fc2ae-7fc1-6527-e051-c6b34803f08d}*SharedItemsImports = 5
shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13
EndGlobalSection
GlobalSection(Performance) = preSolution
diff --git a/samples/AvaloniaControlCatalog/App.xaml b/samples/AvaloniaControlCatalog/App.xaml
new file mode 100644
index 000000000..42f35c720
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/App.xaml
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ #33000000
+ #99000000
+ #FFE6E6E6
+ #FF000000
+
+
+ #33FFFFFF
+ #99FFFFFF
+ #FF1F1F1F
+ #FFFFFFFF
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/AvaloniaControlCatalog/App.xaml.cs b/samples/AvaloniaControlCatalog/App.xaml.cs
new file mode 100644
index 000000000..71e4113a3
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/App.xaml.cs
@@ -0,0 +1,164 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using Avalonia.Platform;
+using Avalonia.Styling;
+using Avalonia.Themes.Simple;
+using Avalonia.Themes.Fluent;
+using ControlCatalog.Models;
+using ControlCatalog.ViewModels;
+
+namespace ControlCatalog
+{
+ public class App : Application
+ {
+ private readonly Styles _themeStylesContainer = new();
+ private FluentTheme? _fluentTheme;
+ private SimpleTheme? _simpleTheme;
+ private IStyle? _colorPickerFluent, _colorPickerSimple;
+
+ public App()
+ {
+ DataContext = new ApplicationViewModel();
+ }
+
+ public override void Initialize()
+ {
+ Styles.Add(_themeStylesContainer);
+
+ AvaloniaXamlLoader.Load(this);
+
+ _fluentTheme = (FluentTheme)Resources["FluentTheme"]!;
+ _simpleTheme = (SimpleTheme)Resources["SimpleTheme"]!;
+ _colorPickerFluent = (IStyle)Resources["ColorPickerFluent"]!;
+ _colorPickerSimple = (IStyle)Resources["ColorPickerSimple"]!;
+
+ SetCatalogThemes(CatalogTheme.Fluent);
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
+ {
+ desktopLifetime.MainWindow = new MainWindow { DataContext = new MainWindowViewModel() };
+ }
+ else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime)
+ {
+ singleViewFactoryApplicationLifetime.MainViewFactory = () => new PageNavigationHost()
+ {
+ Page = new MainView { DataContext = new MainWindowViewModel() }
+ };
+ }
+ else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
+ {
+ singleViewLifetime.MainView = new PageNavigationHost()
+ {
+ Page = new MainView { DataContext = new MainWindowViewModel() }
+ };
+ }
+
+ if (this.TryGetFeature() is { } activatableApplicationLifetime)
+ {
+ activatableApplicationLifetime.Activated += (sender, args) =>
+ Console.WriteLine($"App activated: {args.Kind}");
+ activatableApplicationLifetime.Deactivated += (sender, args) =>
+ Console.WriteLine($"App deactivated: {args.Kind}");
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+
+ public void OnDockNewWindowClicked(object? sender, EventArgs e)
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime)
+ {
+ var window = new MainWindow();
+ window.Show();
+ }
+ }
+
+ public void OnDockShowMainWindowClicked(object? sender, EventArgs e)
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
+ {
+ desktopLifetime.MainWindow?.Activate();
+ }
+ }
+
+ private int _dockMenuItemCount;
+
+ public void OnDockAddItemClicked(object? sender, EventArgs e)
+ {
+ var dockMenu = NativeDock.GetMenu(this);
+ if (dockMenu is not null)
+ {
+ _dockMenuItemCount++;
+ var item = new NativeMenuItem($"New item {_dockMenuItemCount}");
+ item.Click += (_, _) =>
+ {
+ dockMenu.Items.Remove(item);
+ };
+ dockMenu.Items.Insert(0, item);
+ }
+ }
+
+ private CatalogTheme _prevTheme;
+ public static CatalogTheme CurrentTheme => ((App)Current!)._prevTheme;
+ public static void SetCatalogThemes(CatalogTheme theme)
+ {
+ var app = (App)Current!;
+ var prevTheme = app._prevTheme;
+ app._prevTheme = theme;
+ var shouldReopenWindow = prevTheme != theme;
+
+ if (app._themeStylesContainer.Count == 0)
+ {
+ app._themeStylesContainer.Add(new Style());
+ app._themeStylesContainer.Add(new Style());
+ app._themeStylesContainer.Add(new Style());
+ }
+
+ if (theme == CatalogTheme.Fluent)
+ {
+ app._themeStylesContainer[0] = app._fluentTheme!;
+ app._themeStylesContainer[1] = app._colorPickerFluent!;
+ }
+ else if (theme == CatalogTheme.Simple)
+ {
+ app._themeStylesContainer[0] = app._simpleTheme!;
+ app._themeStylesContainer[1] = app._colorPickerSimple!;
+ }
+
+ if (shouldReopenWindow)
+ {
+ if (app.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
+ {
+ var oldWindow = desktopLifetime.MainWindow;
+ var newWindow = new MainWindow()
+ {
+ DataContext = new MainWindowViewModel()
+ };
+ desktopLifetime.MainWindow = newWindow;
+ newWindow.Show();
+ oldWindow?.Close();
+ }
+ else if (app.ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime)
+ {
+ singleViewFactoryApplicationLifetime.MainViewFactory = () => new PageNavigationHost()
+ {
+ Page = new MainView { DataContext = new MainWindowViewModel() }
+ };
+ }
+ else if (app.ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
+ {
+ singleViewLifetime.MainView = new PageNavigationHost()
+ {
+ Page = new MainView { DataContext = new MainWindowViewModel() }
+ };
+ }
+ }
+ }
+ }
+}
diff --git a/samples/AvaloniaControlCatalog/Assets/CurvedHeader/avatar.jpg b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/avatar.jpg
new file mode 100644
index 000000000..ddcf43956
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/avatar.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:72bc7dc39313ed42adb7ccc430b4083e2d4c91b4433a7eca0c620fdb64892316
+size 270846
diff --git a/samples/AvaloniaControlCatalog/Assets/CurvedHeader/featured.jpg b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/featured.jpg
new file mode 100644
index 000000000..bfc31e4bb
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/featured.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7b86c986b58e751da873fb022a9c3fbc8608c5884db65dca711866f3b63b59a3
+size 342297
diff --git a/samples/AvaloniaControlCatalog/Assets/CurvedHeader/product1.jpg b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/product1.jpg
new file mode 100644
index 000000000..4842769e5
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/product1.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a3b4ab4d1fe41bc08f45170c393f9c7bf0dab08e3f9334ec781a48a25f6926e0
+size 392467
diff --git a/samples/AvaloniaControlCatalog/Assets/CurvedHeader/product2.jpg b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/product2.jpg
new file mode 100644
index 000000000..0fab31114
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/product2.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:328cf4e51f1aa7c85263d7f643f949fbdf026a02267d09a9549a9b724ce260cb
+size 311991
diff --git a/samples/AvaloniaControlCatalog/Assets/CurvedHeader/product3.jpg b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/product3.jpg
new file mode 100644
index 000000000..ef7100e67
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/product3.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:041d41538d29d7b8dc60c4f004eb6c2214a50367bfe00ef65255ce8209d61dd2
+size 314528
diff --git a/samples/AvaloniaControlCatalog/Assets/CurvedHeader/update1.jpg b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/update1.jpg
new file mode 100644
index 000000000..fbb14a94c
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/update1.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b91f15899decfc3ee76c1a81a3046690fd6e311d99967ec8a4cc3f3de71eda22
+size 201420
diff --git a/samples/AvaloniaControlCatalog/Assets/CurvedHeader/update2.jpg b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/update2.jpg
new file mode 100644
index 000000000..e66023e5f
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/update2.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c1705f35eb2edd8b7a8e244a44488670f54288910277e8b48671837aa02e4a18
+size 202337
diff --git a/samples/AvaloniaControlCatalog/Assets/CurvedHeader/update3.jpg b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/update3.jpg
new file mode 100644
index 000000000..cf1c02779
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/update3.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0540dc8d5d03e5043ff4c976a4946efbcb01a4f7c3e0c4f6005dbf87630be478
+size 219094
diff --git a/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-Bold.ttf b/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-Bold.ttf
new file mode 100644
index 000000000..0a35698de
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-Bold.ttf
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:da4f442e66843990825ed4757e27ad3442cad83f9844cc503e8ece85e00f77f2
+size 290916
diff --git a/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-BoldItalic.ttf b/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-BoldItalic.ttf
new file mode 100644
index 000000000..1dcbaab57
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-BoldItalic.ttf
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4413cbf92a508e6a6eb0a089ee9bcf1b94f0b366ea755c5c64b6d4755f160c40
+size 103200
diff --git a/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-Italic.ttf b/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-Italic.ttf
new file mode 100644
index 000000000..5f1badfb8
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-Italic.ttf
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dac7846e443ec58c0802cd6fa55836060a62d8a7448c0bccff5242bac9e71d0e
+size 103828
diff --git a/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-Regular.ttf b/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-Regular.ttf
new file mode 100644
index 000000000..68410ff1e
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-Regular.ttf
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:71d10a86b4c54a5a9c0c8b467e53ac67d79edb96c956e4e9f65a7074dfb9992a
+size 293516
diff --git a/samples/AvaloniaControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf b/samples/AvaloniaControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf
new file mode 100644
index 000000000..d70ce0515
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5e2297609126d20a5c3f35eb0ed8ac8bc7e4a2290a820f4055c0703d2c598912
+size 4505016
diff --git a/samples/AvaloniaControlCatalog/Assets/Icon.png b/samples/AvaloniaControlCatalog/Assets/Icon.png
new file mode 100644
index 000000000..26522b1ff
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Icon.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1821e7bd48a6f407bddcbc1fd789132a3d5b6a5b787951114e3360b4bd650c96
+size 14349
diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/avatar.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/avatar.jpg
new file mode 100644
index 000000000..baeae257c
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/avatar.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c48b24b7af0a316393b9492b04f26c3716efb385f222cb1f3098a3225e5fd3d4
+size 22590
diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/dest_alps.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/dest_alps.jpg
new file mode 100644
index 000000000..db24b89be
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/dest_alps.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:942940764a67ff522ab558510d0fc28d8c6d8139d36ad12f0150ca80061e5911
+size 40061
diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/dest_forest.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/dest_forest.jpg
new file mode 100644
index 000000000..3034a1fa4
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/dest_forest.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:13210c53e74791192cd5f0a08e525acbb84a4fe01634af2a37c26d4440c13335
+size 57483
diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/dest_norway.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/dest_norway.jpg
new file mode 100644
index 000000000..d6e6bde5a
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/dest_norway.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d35bf25fec4fe7e043daf0a2db45fd60022d6ac30f822de04476e9b75a1613b6
+size 26764
diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/exp_angkor.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/exp_angkor.jpg
new file mode 100644
index 000000000..33d774d8a
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/exp_angkor.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:67d40972a68c877c5caa83ede7e9f9f740262b2f66f643c61f37bff5455e7ffd
+size 78618
diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/exp_tokyo.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/exp_tokyo.jpg
new file mode 100644
index 000000000..271b47c2e
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/exp_tokyo.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:386207dd4f978585f3eab30b99e26edd7d809c739a65d6e6b21c6044d36c1899
+size 104781
diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_alpine.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_alpine.jpg
new file mode 100644
index 000000000..6a5363ec2
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_alpine.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8a952107d9fcdc461dd40dd185e661813766c6f03dee7dfa1727e85973e15b0c
+size 39021
diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_bay.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_bay.jpg
new file mode 100644
index 000000000..e96394b45
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_bay.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ae12c0a8c2580d98ed4ed4fea5b156f7849081199e1b2716f3c8abb4e986de0f
+size 35928
diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_city.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_city.jpg
new file mode 100644
index 000000000..a7b42af88
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_city.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a9d399a312edfc37e9b485c02b11cb3cd5c8eae45c6638966a7a65fe7f8c5218
+size 38189
diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_paris.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_paris.jpg
new file mode 100644
index 000000000..a0a719738
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_paris.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ea917b0df0e1dfb0125f782dd86942eec48f81da011cf0c69719efbf050dac75
+size 16808
diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_tropical.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_tropical.jpg
new file mode 100644
index 000000000..c1af3469a
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_tropical.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2885affa5a5400330e5ddbf2a88b4a5ffedc38e7cd7a5d3c4988c84869d8b481
+size 23530
diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_venice.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_venice.jpg
new file mode 100644
index 000000000..8331f302c
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_venice.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d80c97e65cf610c562ebda65357356aa6d4d47c32e36e7a77212449aeb9a457c
+size 32633
diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/story1.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/story1.jpg
new file mode 100644
index 000000000..024fa15bc
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/story1.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:04fe12f3211d8413bcf576839355460357081c5c29b3ee3c8767d9008a00aff9
+size 13068
diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/story2.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/story2.jpg
new file mode 100644
index 000000000..ce3d2d406
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/story2.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6c81284b88d9cc65af2e35d286dbff0c72da487672358e332be8499c2a174ffe
+size 10385
diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/story3.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/story3.jpg
new file mode 100644
index 000000000..a491aa122
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/story3.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b5fec94c831245620f3bfc82a34d62482668359418dd4c72ba8b0f16671ae521
+size 9262
diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/cast1.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/cast1.jpg
new file mode 100644
index 000000000..a82fd4de6
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Movies/cast1.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f085e47346df78b2d8d7fc3202c94f0a05f51c94669842f1deaca61edf3513a0
+size 383729
diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/cast2.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/cast2.jpg
new file mode 100644
index 000000000..52efa377e
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Movies/cast2.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:97f9c47d312a7a20d6f816c1f1aa12553d889cdcc812895c6de133e5c4163d3f
+size 384405
diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/continue1.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/continue1.jpg
new file mode 100644
index 000000000..28e76e590
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Movies/continue1.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8864dd8af0a41fecdcd10c9f3dd7f46fd40835eaceec84d9c3bdb469b908c1de
+size 243330
diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/hero.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/hero.jpg
new file mode 100644
index 000000000..d19b19d29
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Movies/hero.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d1d19998f473a10f475fb09dbc9333846e33926df134cae53fbb26ef12dfa065
+size 365476
diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/morelike1.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/morelike1.jpg
new file mode 100644
index 000000000..5a74f00e9
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Movies/morelike1.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:232f551a9920a0df27e3f63e6c1bb0284b32654873bbdb6e0fd01d2b03fa289e
+size 386942
diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/search1.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/search1.jpg
new file mode 100644
index 000000000..fe0ac989c
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Movies/search1.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5e49187f3a3d58250c0340654ef62986c5bcf71cc2bc7ccf8e9f464d06d29843
+size 365280
diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/toprated1.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/toprated1.jpg
new file mode 100644
index 000000000..9c0594bf7
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Movies/toprated1.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:379bed75f5e9fdcf81e0be3b8ce244097b3f941945c64bd4f793501062b92ab8
+size 323000
diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/toprated2.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/toprated2.jpg
new file mode 100644
index 000000000..bc2ac63a9
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Movies/toprated2.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6027435c1abaf0a84f7badf3b496589c2eb12211b86506273c374a3da6fc4328
+size 193691
diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/toprated3.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/toprated3.jpg
new file mode 100644
index 000000000..fc9092277
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Movies/toprated3.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:81bd4874b15875b3297f59958b67e76fe0cc36c84a3c3e55f1ec72466ef0de01
+size 276190
diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/toprated4.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/toprated4.jpg
new file mode 100644
index 000000000..428d18379
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Movies/toprated4.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b64cedeb1b6e382eb48c4ac46667fccf8fcdca4de7a754b6b6e9afed5bac0c17
+size 338325
diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/trending1.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/trending1.jpg
new file mode 100644
index 000000000..275d20959
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Movies/trending1.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8f48ab966356c739a134dfdf518572722d655a1c8ff19d16349f93dd4c5da574
+size 354912
diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/trending2.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/trending2.jpg
new file mode 100644
index 000000000..0714cc902
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Movies/trending2.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5c4ae07bf3716d1131c59766518d74e50e0a6150f9a018c74fde92b39b786470
+size 360307
diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/cat_hiit.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/cat_hiit.jpg
new file mode 100644
index 000000000..6cb7b1dcf
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Pulse/cat_hiit.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9d2255faa3104c9dbe66b1d5a0a27b9eecac836b322fdf30f43d84d798bc0904
+size 363012
diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/cat_strength.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/cat_strength.jpg
new file mode 100644
index 000000000..7619e5609
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Pulse/cat_strength.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:98eff7032787cf75e5c01968070ca3a18b4167d8124e7dde37ab5ee883e9206f
+size 328458
diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/cat_yoga.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/cat_yoga.jpg
new file mode 100644
index 000000000..99d8a4424
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Pulse/cat_yoga.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:273226cb81451cfed6405f3f337dfd5a1a90f806052c1c5b849fcc31c9ed2bb9
+size 220062
diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/ex_bench.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/ex_bench.jpg
new file mode 100644
index 000000000..dff9c6388
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Pulse/ex_bench.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3b6c1e389d2e8a9b3bf8638629ccc6be5dda989fa5aeeb7fbac3b15c347aaf3e
+size 348183
diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/ex_deadlifts.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/ex_deadlifts.jpg
new file mode 100644
index 000000000..1bf715e1c
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Pulse/ex_deadlifts.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:729e1e7ab3cad488f8c15055bd2bc46dbf4df378235d09b17b6faf345535dcae
+size 350168
diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/ex_overhead.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/ex_overhead.jpg
new file mode 100644
index 000000000..ba6c56f2b
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Pulse/ex_overhead.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6d05add74876aa9913d0668ba14814def56385cc6d5a71ef32e2947aa8cf35e4
+size 318869
diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/ex_pullups.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/ex_pullups.jpg
new file mode 100644
index 000000000..d73d27140
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Pulse/ex_pullups.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c61f8680ec76d0a6dce17194f90960bd071f3e0eb9f4e5c1fa44ad42e08d5fee
+size 259525
diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/ex_squats.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/ex_squats.jpg
new file mode 100644
index 000000000..db4673a60
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Pulse/ex_squats.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e4c0ba6f7dfecb0f725199f27771fa143626fd4af82c2267a3e6a4e033d4c2bc
+size 349045
diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/profile_avatar.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/profile_avatar.jpg
new file mode 100644
index 000000000..e878e2845
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Pulse/profile_avatar.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:099c45bf78acddb5e3706c08c78570b2216bc287bee4c9551331ef7755985a87
+size 234819
diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/rec_fullbody.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/rec_fullbody.jpg
new file mode 100644
index 000000000..1edfdab75
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Pulse/rec_fullbody.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4f74e1316bc51221749ebb06bdfbd6e7a291a3cd2bf27482ff8927a1fe7722ff
+size 299718
diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/rec_mobility.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/rec_mobility.jpg
new file mode 100644
index 000000000..8a197c6ff
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Pulse/rec_mobility.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:80ea995c7241abeda718538e4d39e99c905c561c6b646a536c75cac3968b942a
+size 378365
diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/rec_powercore.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/rec_powercore.jpg
new file mode 100644
index 000000000..35621156b
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Pulse/rec_powercore.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f4c4fbf95b31719a0c1aa149635e17ae1165e93a4bae19774c0dd628025bdc5f
+size 333866
diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/workout_hero.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/workout_hero.jpg
new file mode 100644
index 000000000..6736801e6
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Pulse/workout_hero.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e9cce5e1a6dba98d1517ecba88a22b14c9974a775f83190ecfd700b1a09e2996
+size 357270
diff --git a/samples/AvaloniaControlCatalog/Assets/Restaurant/dish1.jpg b/samples/AvaloniaControlCatalog/Assets/Restaurant/dish1.jpg
new file mode 100644
index 000000000..47c7c3e47
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Restaurant/dish1.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1e53d7c95d20decc8fd69432d737f49955d9aad6c4b5e97dbfa8f7d14034e3b2
+size 63977
diff --git a/samples/AvaloniaControlCatalog/Assets/Restaurant/dish2.jpg b/samples/AvaloniaControlCatalog/Assets/Restaurant/dish2.jpg
new file mode 100644
index 000000000..7da1fd389
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Restaurant/dish2.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c0e26e64efacd609c7c47b5f46f2238f5eeb69a94374c1d57b2bc773962f9596
+size 19720
diff --git a/samples/AvaloniaControlCatalog/Assets/Restaurant/dish3.jpg b/samples/AvaloniaControlCatalog/Assets/Restaurant/dish3.jpg
new file mode 100644
index 000000000..0e33613ed
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Restaurant/dish3.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5fdc8a579038c204a04c44e6828f89e3c18026537258283d46a416fcadc4256a
+size 56364
diff --git a/samples/AvaloniaControlCatalog/Assets/Restaurant/dish4.jpg b/samples/AvaloniaControlCatalog/Assets/Restaurant/dish4.jpg
new file mode 100644
index 000000000..42b17c210
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Restaurant/dish4.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9854911823e06db37e4ab09de2946c46ffadd035735a9e4ac907b0ed1a6ec92c
+size 46057
diff --git a/samples/AvaloniaControlCatalog/Assets/Restaurant/featured_dish.jpg b/samples/AvaloniaControlCatalog/Assets/Restaurant/featured_dish.jpg
new file mode 100644
index 000000000..3756a3442
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Restaurant/featured_dish.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c73b185ddc65fef06e932526f145e2d94ce3b2421b3e75ecca24d54568274e9f
+size 68998
diff --git a/samples/AvaloniaControlCatalog/Assets/Restaurant/user_avatar.jpg b/samples/AvaloniaControlCatalog/Assets/Restaurant/user_avatar.jpg
new file mode 100644
index 000000000..ce3d2d406
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Restaurant/user_avatar.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6c81284b88d9cc65af2e35d286dbff0c72da487672358e332be8499c2a174ffe
+size 10385
diff --git a/samples/AvaloniaControlCatalog/Assets/RetroGaming/cyber_city.jpg b/samples/AvaloniaControlCatalog/Assets/RetroGaming/cyber_city.jpg
new file mode 100644
index 000000000..e931dc471
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/RetroGaming/cyber_city.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:87d6f01e3c0216f1b87b165fd30a45bd58ca01efd38e779eacf09e9acfd771f6
+size 354653
diff --git a/samples/AvaloniaControlCatalog/Assets/RetroGaming/dungeon_bit.jpg b/samples/AvaloniaControlCatalog/Assets/RetroGaming/dungeon_bit.jpg
new file mode 100644
index 000000000..4c2c30162
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/RetroGaming/dungeon_bit.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1879f78ca91253fd12e66e9f10b1fe5943471f1c84561086dd4ebb930e26ef20
+size 468907
diff --git a/samples/AvaloniaControlCatalog/Assets/RetroGaming/forest_spirit.jpg b/samples/AvaloniaControlCatalog/Assets/RetroGaming/forest_spirit.jpg
new file mode 100644
index 000000000..36c5957d0
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/RetroGaming/forest_spirit.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e03ea60a9425aa35d8daea7336eb8ba6eb5a6388bc7c4b918480a1e8153f66ad
+size 426054
diff --git a/samples/AvaloniaControlCatalog/Assets/RetroGaming/hero.jpg b/samples/AvaloniaControlCatalog/Assets/RetroGaming/hero.jpg
new file mode 100644
index 000000000..deeb19590
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/RetroGaming/hero.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a12f0998ac595661b6eb9b0db10d2c8b0dc94036a9ac8979aabef0907bde8517
+size 220826
diff --git a/samples/AvaloniaControlCatalog/Assets/RetroGaming/neon_ninja.jpg b/samples/AvaloniaControlCatalog/Assets/RetroGaming/neon_ninja.jpg
new file mode 100644
index 000000000..240ae5ea2
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/RetroGaming/neon_ninja.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dbd118a09032ee0e8e095199f65a8230ffadb301f02799308d8dc9221f617f38
+size 303206
diff --git a/samples/AvaloniaControlCatalog/Assets/RetroGaming/neon_racer.jpg b/samples/AvaloniaControlCatalog/Assets/RetroGaming/neon_racer.jpg
new file mode 100644
index 000000000..787236868
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/RetroGaming/neon_racer.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:eae91ec8b75d3910700ec06ba95da8dbef9e6390414db02466e1e8c8c12bf359
+size 398687
diff --git a/samples/AvaloniaControlCatalog/Assets/RetroGaming/pixel_quest.jpg b/samples/AvaloniaControlCatalog/Assets/RetroGaming/pixel_quest.jpg
new file mode 100644
index 000000000..ea0352cb8
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/RetroGaming/pixel_quest.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c14d6c36f708f5182d97ef13f5f93ab38d976ab69700d89936eede18f5041ea4
+size 318451
diff --git a/samples/AvaloniaControlCatalog/Assets/RetroGaming/space_voids.jpg b/samples/AvaloniaControlCatalog/Assets/RetroGaming/space_voids.jpg
new file mode 100644
index 000000000..9eaa32bd7
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/RetroGaming/space_voids.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:04404c6cb7b46ac40b5e5d939328c00d81952ccd29d8f766f038f7bb5b15e47b
+size 346663
diff --git a/samples/AvaloniaControlCatalog/Assets/Sanctuary/city_bg.jpg b/samples/AvaloniaControlCatalog/Assets/Sanctuary/city_bg.jpg
new file mode 100644
index 000000000..840946873
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Sanctuary/city_bg.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a7289d08dfd44e1f9b1b9165492924d34600e14330086c3bf311c0d3ccaff429
+size 416179
diff --git a/samples/AvaloniaControlCatalog/Assets/Sanctuary/forest_bg.jpg b/samples/AvaloniaControlCatalog/Assets/Sanctuary/forest_bg.jpg
new file mode 100644
index 000000000..5cc52e300
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Sanctuary/forest_bg.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:84201ad5c3993927ff95093312f194401f85359298bb8dcc0061d9a6d9576c2b
+size 460289
diff --git a/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_arctic_silence.jpg b/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_arctic_silence.jpg
new file mode 100644
index 000000000..11ced44ea
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_arctic_silence.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5507dcbeb97f469087eb4b66f4e9de9dff988628cab77c9f74922ea39d2d14fd
+size 468065
diff --git a/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_deep_forest.jpg b/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_deep_forest.jpg
new file mode 100644
index 000000000..944c463aa
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_deep_forest.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a2d5c7b9f823005cd5245ede7f3587cc00d6cf1ccb113f5aece0aebf4b017711
+size 537150
diff --git a/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_desert_sands.jpg b/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_desert_sands.jpg
new file mode 100644
index 000000000..eea7a888a
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_desert_sands.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:eb636651ef45fc72bb98c324f556cc89589527d0f277e405b192f09c7b833c1b
+size 331737
diff --git a/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_hero.jpg b/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_hero.jpg
new file mode 100644
index 000000000..72e22e63c
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_hero.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:49646d05d73283e485c1cff68bab10090815e65b30798d51b376c57c8042dd76
+size 466219
diff --git a/samples/AvaloniaControlCatalog/Assets/Sanctuary/mountain_bg.jpg b/samples/AvaloniaControlCatalog/Assets/Sanctuary/mountain_bg.jpg
new file mode 100644
index 000000000..d61797adc
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/Sanctuary/mountain_bg.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:605e9595791622ee51d12f84411c8d6ccae372d134b6fe8d9d3276bbde5c0ae0
+size 416532
diff --git a/samples/AvaloniaControlCatalog/Assets/avalonia-32.png b/samples/AvaloniaControlCatalog/Assets/avalonia-32.png
new file mode 100644
index 000000000..f896a9f2b
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/avalonia-32.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a6d9f3f015b19fd537e192fdfc0e8069ea5dcd983ddd2cdf7213f3638bff82d0
+size 1322
diff --git a/samples/AvaloniaControlCatalog/Assets/delicate-arch-896885_640.jpg b/samples/AvaloniaControlCatalog/Assets/delicate-arch-896885_640.jpg
new file mode 100644
index 000000000..e1f825cf6
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/delicate-arch-896885_640.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5f015d396bc046aab2849a62d5c30842be3423ddc4ff58d0b2b07d7f9898b0dc
+size 95812
diff --git a/samples/AvaloniaControlCatalog/Assets/github_icon.png b/samples/AvaloniaControlCatalog/Assets/github_icon.png
new file mode 100644
index 000000000..41e457653
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/github_icon.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e3c7853d2d976abc126cb5f533d0080ea03436bd05c6f0bfcd57c1efdff69683
+size 17223
diff --git a/samples/AvaloniaControlCatalog/Assets/hirsch-899118_640.jpg b/samples/AvaloniaControlCatalog/Assets/hirsch-899118_640.jpg
new file mode 100644
index 000000000..c3254eb41
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/hirsch-899118_640.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:37545dd5cf7e85f58cc55dc0e271cabaf15cb4a8ccdadb352122095615b74c63
+size 216662
diff --git a/samples/AvaloniaControlCatalog/Assets/image1.jpg b/samples/AvaloniaControlCatalog/Assets/image1.jpg
new file mode 100644
index 000000000..29ab7de1c
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/image1.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:89852956751255d12625b89737fbc97faa9b4cc3c8cf40f28983491ad0ef8e17
+size 71581
diff --git a/samples/AvaloniaControlCatalog/Assets/image2.jpg b/samples/AvaloniaControlCatalog/Assets/image2.jpg
new file mode 100644
index 000000000..75c97f009
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/image2.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e0f13d195f3daf0d313cb408b3895bf6fd95324a4fdc4523c718bbe15b4bfa50
+size 53850
diff --git a/samples/AvaloniaControlCatalog/Assets/image3.jpg b/samples/AvaloniaControlCatalog/Assets/image3.jpg
new file mode 100644
index 000000000..6b70c394e
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/image3.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:64f5c61afcaca7e4a5d2b32967acce91f8a6976c2eeb1848efb5bd6556412b51
+size 54199
diff --git a/samples/AvaloniaControlCatalog/Assets/image4.jpg b/samples/AvaloniaControlCatalog/Assets/image4.jpg
new file mode 100644
index 000000000..d7bd96d1f
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/image4.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a2c4c6245efa1bcade1d6819be4fc932fd5a78076e009faa54008d8f91a5071a
+size 97298
diff --git a/samples/AvaloniaControlCatalog/Assets/image5.jpg b/samples/AvaloniaControlCatalog/Assets/image5.jpg
new file mode 100644
index 000000000..3bfd6f62d
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/image5.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fcaecb19b5907e37e3728f935390f8744c739df7d0233d7d5688db2d33b5809c
+size 69100
diff --git a/samples/AvaloniaControlCatalog/Assets/image6.jpg b/samples/AvaloniaControlCatalog/Assets/image6.jpg
new file mode 100644
index 000000000..3d7239a9f
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/image6.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c81db4378d3cf4d5f871c59824763014cc8dc88e9adedbd734ae35cecc8e03da
+size 109665
diff --git a/samples/AvaloniaControlCatalog/Assets/image7.jpg b/samples/AvaloniaControlCatalog/Assets/image7.jpg
new file mode 100644
index 000000000..818793eea
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/image7.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:137a9d41c4e59c420711cfd49090621cf3c0f300e7c3429046a5c783083a7ebc
+size 61519
diff --git a/samples/AvaloniaControlCatalog/Assets/maple-leaf-888807_640.jpg b/samples/AvaloniaControlCatalog/Assets/maple-leaf-888807_640.jpg
new file mode 100644
index 000000000..b892ddfc3
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/maple-leaf-888807_640.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1b9f04697c2afc982323df5fcc3804e9a832da24bc213057f7258b0f8915c2f9
+size 113622
diff --git a/samples/AvaloniaControlCatalog/Assets/test_icon.ico b/samples/AvaloniaControlCatalog/Assets/test_icon.ico
new file mode 100644
index 000000000..b023aa246
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Assets/test_icon.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:df9262e2176d830bfbef5b5e1dfa12f91c89e8ecb764eee366c3352df371d665
+size 176111
diff --git a/samples/AvaloniaControlCatalog/AvaloniaControlCatalog.csproj b/samples/AvaloniaControlCatalog/AvaloniaControlCatalog.csproj
new file mode 100644
index 000000000..ae7007d9f
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/AvaloniaControlCatalog.csproj
@@ -0,0 +1,80 @@
+
+
+ net10.0-windows
+ WinExe
+ true
+
+ default
+ enable
+ ControlCatalog
+ AvaloniaControlCatalog
+ true
+ true
+ false
+ $(NoWarn);AVA3001
+ $(MSBuildProjectDirectory)\NuGet.Config
+ 12.2.999-cibuild0068621-alpha
+ https://nuget-feed-all.avaloniaui.net/v3/index.json;$(RestoreAdditionalProjectSources)
+ true
+ false
+ true
+ true
+ app.manifest
+
+
+
+
+
+
+ %(Filename)
+
+
+ Designer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MSBuild:Compile
+
+
+
+
diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/AnonymousObserverNonGenericHelper.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/AnonymousObserverNonGenericHelper.cs
new file mode 100644
index 000000000..134c67b28
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/AnonymousObserverNonGenericHelper.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Avalonia.Reactive;
+
+///
+/// Contains fields for that aren't using generic arguments.
+/// Separated to avoid unnecessary generic instantiations.
+///
+internal static class AnonymousObserverNonGenericHelper
+{
+ public static readonly Action ThrowsOnError = ex => throw ex;
+ public static readonly Action NoOpCompleted = () => { };
+}
diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/AvaloniaPropertyBindingObservable.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/AvaloniaPropertyBindingObservable.cs
new file mode 100644
index 000000000..fd68381d5
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/AvaloniaPropertyBindingObservable.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Data;
+
+namespace Avalonia.Reactive
+{
+ internal class AvaloniaPropertyBindingObservable : LightweightObservableBase>, IDescription
+ {
+ private readonly WeakReference _target;
+ private readonly AvaloniaProperty _property;
+ private readonly Func? _converter;
+ private BindingValue _value = BindingValue.Unset;
+
+ public AvaloniaPropertyBindingObservable(
+ AvaloniaObject target,
+ AvaloniaProperty property,
+ Func? converter = null)
+ {
+ _target = new WeakReference(target);
+ _property = property;
+ _converter = converter;
+ }
+
+ public string Description => $"{_target.GetType().Name}.{_property.Name}";
+
+ protected override void Initialize()
+ {
+ if (_target.TryGetTarget(out var target))
+ {
+ if (_converter is { } converter)
+ {
+ var unconvertedValue = (TSource)target.GetValue(_property)!;
+ _value = converter(unconvertedValue);
+ target.PropertyChanged += PropertyChanged_WithConversion;
+ }
+ else
+ {
+ _value = (TResult)target.GetValue(_property)!;
+ target.PropertyChanged += PropertyChanged;
+ }
+ }
+ }
+
+ protected override void Deinitialize()
+ {
+ if (_target.TryGetTarget(out var target))
+ {
+ if (_converter is not null)
+ {
+ target.PropertyChanged -= PropertyChanged_WithConversion;
+ }
+ else
+ {
+ target.PropertyChanged -= PropertyChanged;
+ }
+ }
+ }
+
+ protected override void Subscribed(IObserver> observer, bool first)
+ {
+ if (_value.Type != BindingValueType.UnsetValue)
+ {
+ observer.OnNext(_value);
+ }
+ }
+
+ private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Property == _property)
+ {
+ if (e is AvaloniaPropertyChangedEventArgs typedArgs)
+ {
+ PublishValue(e.Sender.GetValue(typedArgs.Property));
+ }
+ else
+ {
+ PublishUntypedValue(e.Sender.GetValue(e.Property));
+ }
+ }
+ }
+
+ private void PropertyChanged_WithConversion(object? sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Property == _property)
+ {
+ if (e is AvaloniaPropertyChangedEventArgs typedArgs)
+ {
+ var newValueRaw = e.Sender.GetValue(typedArgs.Property);
+
+ var newValue = _converter!(newValueRaw);
+
+ PublishValue(newValue);
+ }
+ else
+ {
+ var newValue = e.Sender.GetValue(e.Property);
+
+ if (newValue is TSource source)
+ {
+ newValue = _converter!(source);
+ }
+
+ PublishUntypedValue(newValue);
+ }
+ }
+ }
+
+ private void PublishValue(TResult newValue)
+ {
+ if (!_value.HasValue || !EqualityComparer.Default.Equals(newValue, _value.Value))
+ {
+ _value = newValue;
+ PublishNext(_value);
+ }
+ }
+
+ private void PublishUntypedValue(object? newValue)
+ {
+ if (!Equals(newValue, _value))
+ {
+ _value = (TResult)newValue!;
+ PublishNext(_value);
+ }
+ }
+ }
+}
diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/AvaloniaPropertyChangedObservable.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/AvaloniaPropertyChangedObservable.cs
new file mode 100644
index 000000000..fa876a890
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/AvaloniaPropertyChangedObservable.cs
@@ -0,0 +1,46 @@
+using System;
+
+namespace Avalonia.Reactive
+{
+ internal class AvaloniaPropertyChangedObservable :
+ LightweightObservableBase,
+ IDescription
+ {
+ private readonly WeakReference _target;
+ private readonly AvaloniaProperty _property;
+
+ public AvaloniaPropertyChangedObservable(
+ AvaloniaObject target,
+ AvaloniaProperty property)
+ {
+ _target = new WeakReference(target);
+ _property = property;
+ }
+
+ public string Description => $"{_target.GetType().Name}.{_property.Name}";
+
+ protected override void Initialize()
+ {
+ if (_target.TryGetTarget(out var target))
+ {
+ target.PropertyChanged += PropertyChanged;
+ }
+ }
+
+ protected override void Deinitialize()
+ {
+ if (_target.TryGetTarget(out var target))
+ {
+ target.PropertyChanged -= PropertyChanged;
+ }
+ }
+
+ private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Property == _property)
+ {
+ PublishNext(e);
+ }
+ }
+ }
+}
diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/AvaloniaPropertyObservable.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/AvaloniaPropertyObservable.cs
new file mode 100644
index 000000000..0d40fa96e
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/AvaloniaPropertyObservable.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Data;
+
+namespace Avalonia.Reactive
+{
+ internal class AvaloniaPropertyObservable : LightweightObservableBase, IDescription
+ {
+ private readonly WeakReference _target;
+ private readonly AvaloniaProperty _property;
+ private readonly Func? _converter;
+ private Optional _value;
+
+ public AvaloniaPropertyObservable(
+ AvaloniaObject target,
+ AvaloniaProperty property,
+ Func? converter = null)
+ {
+ _target = new WeakReference(target);
+ _property = property;
+ _converter = converter;
+ }
+
+ public string Description => $"{_target.GetType().Name}.{_property.Name}";
+
+ protected override void Initialize()
+ {
+ if (_target.TryGetTarget(out var target))
+ {
+ if (_converter is { } converter)
+ {
+ var unconvertedValue = (TSource)target.GetValue(_property)!;
+ _value = converter(unconvertedValue);
+ target.PropertyChanged += PropertyChanged_WithConversion;
+ }
+ else
+ {
+ _value = (TResult)target.GetValue(_property)!;
+ target.PropertyChanged += PropertyChanged;
+ }
+ }
+ }
+
+ protected override void Deinitialize()
+ {
+ if (_target.TryGetTarget(out var target))
+ {
+ if (_converter is not null)
+ {
+ target.PropertyChanged -= PropertyChanged_WithConversion;
+ }
+ else
+ {
+ target.PropertyChanged -= PropertyChanged;
+ }
+ }
+
+ _value = default;
+ }
+
+ protected override void Subscribed(IObserver observer, bool first)
+ {
+ if (_value.HasValue)
+ observer.OnNext(_value.Value);
+ }
+
+ private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Property == _property)
+ {
+ TResult newValue;
+
+ if (e is AvaloniaPropertyChangedEventArgs typed)
+ {
+ newValue = AvaloniaObjectExtensions.GetValue(e.Sender, typed.Property);
+ }
+ else
+ {
+ newValue = (TResult)e.Sender.GetValue(e.Property)!;
+ }
+
+ PublishNewValue(newValue);
+ }
+ }
+
+ private void PropertyChanged_WithConversion(object? sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Property == _property)
+ {
+ TSource newValueRaw;
+
+ if (e is AvaloniaPropertyChangedEventArgs typed)
+ {
+ newValueRaw = AvaloniaObjectExtensions.GetValue(e.Sender, typed.Property);
+ }
+ else
+ {
+ newValueRaw = (TSource)e.Sender.GetValue(e.Property)!;
+ }
+
+ var newValue = _converter!(newValueRaw);
+
+ PublishNewValue(newValue);
+ }
+ }
+
+ private void PublishNewValue(TResult newValue)
+ {
+ if (!_value.HasValue ||
+ !EqualityComparer.Default.Equals(newValue, _value.Value))
+ {
+ _value = newValue;
+ PublishNext(_value.Value!);
+ }
+ }
+ }
+}
diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/CombinedSubject.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/CombinedSubject.cs
new file mode 100644
index 000000000..dc6153133
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/CombinedSubject.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace Avalonia.Reactive;
+
+internal class CombinedSubject : IAvaloniaSubject
+{
+ private readonly IObserver _observer;
+ private readonly IObservable _observable;
+
+ public CombinedSubject(IObserver observer, IObservable observable)
+ {
+ _observer = observer;
+ _observable = observable;
+ }
+
+ public void OnCompleted() => _observer.OnCompleted();
+
+ public void OnError(Exception error) => _observer.OnError(error);
+
+ public void OnNext(T value) => _observer.OnNext(value);
+
+ public IDisposable Subscribe(IObserver observer) => _observable.Subscribe(observer);
+}
diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/CompositeDisposable.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/CompositeDisposable.cs
new file mode 100644
index 000000000..4952686ad
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/CompositeDisposable.cs
@@ -0,0 +1,427 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Threading;
+
+namespace Avalonia.Reactive;
+
+internal sealed class CompositeDisposable : ICollection, IDisposable
+{
+ private readonly object _gate = new object();
+ private bool _disposed;
+ private List _disposables;
+ private int _count;
+ private const int ShrinkThreshold = 64;
+
+ ///
+ /// Initializes a new instance of the class with the specified number of disposables.
+ ///
+ /// The number of disposables that the new CompositeDisposable can initially store.
+ /// is less than zero.
+ public CompositeDisposable(int capacity)
+ {
+ if (capacity < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(capacity));
+ }
+
+ _disposables = new List(capacity);
+ }
+
+ ///
+ /// Initializes a new instance of the class from a group of disposables.
+ ///
+ /// Disposables that will be disposed together.
+ /// is null.
+ /// Any of the disposables in the collection is null.
+ public CompositeDisposable(params IDisposable[] disposables)
+ {
+ if (disposables == null)
+ {
+ throw new ArgumentNullException(nameof(disposables));
+ }
+
+ _disposables = ToList(disposables);
+
+ // _count can be read by other threads and thus should be properly visible
+ // also releases the _disposables contents so it becomes thread-safe
+ Volatile.Write(ref _count, _disposables.Count);
+ }
+
+ ///
+ /// Initializes a new instance of the class from a group of disposables.
+ ///
+ /// Disposables that will be disposed together.
+ /// is null.
+ /// Any of the disposables in the collection is null.
+ public CompositeDisposable(IList disposables)
+ {
+ if (disposables == null)
+ {
+ throw new ArgumentNullException(nameof(disposables));
+ }
+
+ _disposables = ToList(disposables);
+
+ // _count can be read by other threads and thus should be properly visible
+ // also releases the _disposables contents so it becomes thread-safe
+ Volatile.Write(ref _count, _disposables.Count);
+ }
+
+ private static List ToList(IEnumerable disposables)
+ {
+ var capacity = disposables switch
+ {
+ IDisposable[] a => a.Length,
+ ICollection c => c.Count,
+ _ => 12
+ };
+
+ var list = new List(capacity);
+
+ // do the copy and null-check in one step to avoid a
+ // second loop for just checking for null items
+ foreach (var d in disposables)
+ {
+ if (d == null)
+ {
+ throw new ArgumentException("Disposables can't contain null", nameof(disposables));
+ }
+
+ list.Add(d);
+ }
+
+ return list;
+ }
+
+ ///
+ /// Gets the number of disposables contained in the .
+ ///
+ public int Count => Volatile.Read(ref _count);
+
+ ///
+ /// Adds a disposable to the or disposes the disposable if the is disposed.
+ ///
+ /// Disposable to add.
+ /// is null.
+ public void Add(IDisposable item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException(nameof(item));
+ }
+
+ lock (_gate)
+ {
+ if (!_disposed)
+ {
+ _disposables.Add(item);
+
+ // If read atomically outside the lock, it should be written atomically inside
+ // the plain read on _count is fine here because manipulation always happens
+ // from inside a lock.
+ Volatile.Write(ref _count, _count + 1);
+ return;
+ }
+ }
+
+ item.Dispose();
+ }
+
+ ///
+ /// Removes and disposes the first occurrence of a disposable from the .
+ ///
+ /// Disposable to remove.
+ /// true if found; false otherwise.
+ /// is null.
+ public bool Remove(IDisposable item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException(nameof(item));
+ }
+
+ lock (_gate)
+ {
+ // this composite was already disposed and if the item was in there
+ // it has been already removed/disposed
+ if (_disposed)
+ {
+ return false;
+ }
+
+ //
+ // List doesn't shrink the size of the underlying array but does collapse the array
+ // by copying the tail one position to the left of the removal index. We don't need
+ // index-based lookup but only ordering for sequential disposal. So, instead of spending
+ // cycles on the Array.Copy imposed by Remove, we use a null sentinel value. We also
+ // do manual Swiss cheese detection to shrink the list if there's a lot of holes in it.
+ //
+
+ // read fields as infrequently as possible
+ var current = _disposables;
+
+ var i = current.IndexOf(item);
+ if (i < 0)
+ {
+ // not found, just return
+ return false;
+ }
+
+ current[i] = null;
+
+ if (current.Capacity > ShrinkThreshold && _count < current.Capacity / 2)
+ {
+ var fresh = new List(current.Capacity / 2);
+
+ foreach (var d in current)
+ {
+ if (d != null)
+ {
+ fresh.Add(d);
+ }
+ }
+
+ _disposables = fresh;
+ }
+
+ // make sure the Count property sees an atomic update
+ Volatile.Write(ref _count, _count - 1);
+ }
+
+ // if we get here, the item was found and removed from the list
+ // just dispose it and report success
+
+ item.Dispose();
+
+ return true;
+ }
+
+ ///
+ /// Disposes all disposables in the group and removes them from the group.
+ ///
+ public void Dispose()
+ {
+ List? currentDisposables = null;
+
+ lock (_gate)
+ {
+ if (!_disposed)
+ {
+ currentDisposables = _disposables;
+
+ // nulling out the reference is faster no risk to
+ // future Add/Remove because _disposed will be true
+ // and thus _disposables won't be touched again.
+ _disposables = null!; // NB: All accesses are guarded by _disposed checks.
+
+ Volatile.Write(ref _count, 0);
+ Volatile.Write(ref _disposed, true);
+ }
+ }
+
+ if (currentDisposables != null)
+ {
+ foreach (var d in currentDisposables)
+ {
+ d?.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// Removes and disposes all disposables from the , but does not dispose the .
+ ///
+ public void Clear()
+ {
+ IDisposable?[] previousDisposables;
+
+ lock (_gate)
+ {
+ // disposed composites are always clear
+ if (_disposed)
+ {
+ return;
+ }
+
+ var current = _disposables;
+
+ previousDisposables = current.ToArray();
+ current.Clear();
+
+ Volatile.Write(ref _count, 0);
+ }
+
+ foreach (var d in previousDisposables)
+ {
+ d?.Dispose();
+ }
+ }
+
+ ///
+ /// Determines whether the contains a specific disposable.
+ ///
+ /// Disposable to search for.
+ /// true if the disposable was found; otherwise, false.
+ /// is null.
+ public bool Contains(IDisposable item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException(nameof(item));
+ }
+
+ lock (_gate)
+ {
+ if (_disposed)
+ {
+ return false;
+ }
+
+ return _disposables.Contains(item);
+ }
+ }
+
+ ///
+ /// Copies the disposables contained in the to an array, starting at a particular array index.
+ ///
+ /// Array to copy the contained disposables to.
+ /// Target index at which to copy the first disposable of the group.
+ /// is null.
+ /// is less than zero. -or - is larger than or equal to the array length.
+ public void CopyTo(IDisposable[] array, int arrayIndex)
+ {
+ if (array == null)
+ {
+ throw new ArgumentNullException(nameof(array));
+ }
+
+ if (arrayIndex < 0 || arrayIndex >= array.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(arrayIndex));
+ }
+
+ lock (_gate)
+ {
+ // disposed composites are always empty
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (arrayIndex + _count > array.Length)
+ {
+ // there is not enough space beyond arrayIndex
+ // to accommodate all _count disposables in this composite
+ throw new ArgumentOutOfRangeException(nameof(arrayIndex));
+ }
+
+ var i = arrayIndex;
+
+ foreach (var d in _disposables)
+ {
+ if (d != null)
+ {
+ array[i++] = d;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Always returns false.
+ ///
+ public bool IsReadOnly => false;
+
+ ///
+ /// Returns an enumerator that iterates through the .
+ ///
+ /// An enumerator to iterate over the disposables.
+ public IEnumerator GetEnumerator()
+ {
+ lock (_gate)
+ {
+ if (_disposed || _count == 0)
+ {
+ return EmptyEnumerator;
+ }
+
+ // the copy is unavoidable but the creation
+ // of an outer IEnumerable is avoidable
+ return new CompositeEnumerator(_disposables.ToArray());
+ }
+ }
+
+ ///
+ /// Returns an enumerator that iterates through the .
+ ///
+ /// An enumerator to iterate over the disposables.
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ ///
+ /// Gets a value that indicates whether the object is disposed.
+ ///
+ public bool IsDisposed => Volatile.Read(ref _disposed);
+
+ ///
+ /// An empty enumerator for the
+ /// method to avoid allocation on disposed or empty composites.
+ ///
+ private static readonly CompositeEnumerator EmptyEnumerator =
+ new CompositeEnumerator(Array.Empty());
+
+ ///
+ /// An enumerator for an array of disposables.
+ ///
+ private sealed class CompositeEnumerator : IEnumerator
+ {
+ private readonly IDisposable?[] _disposables;
+ private int _index;
+
+ public CompositeEnumerator(IDisposable?[] disposables)
+ {
+ _disposables = disposables;
+ _index = -1;
+ }
+
+ public IDisposable Current => _disposables[_index]!; // NB: _index is only advanced to non-null positions.
+
+ object IEnumerator.Current => _disposables[_index]!;
+
+ public void Dispose()
+ {
+ // Avoid retention of the referenced disposables
+ // beyond the lifecycle of the enumerator.
+ // Not sure if this happens by default to
+ // generic array enumerators though.
+ var disposables = _disposables;
+ Array.Clear(disposables, 0, disposables.Length);
+ }
+
+ public bool MoveNext()
+ {
+ var disposables = _disposables;
+
+ for (;;)
+ {
+ var idx = ++_index;
+
+ if (idx >= disposables.Length)
+ {
+ return false;
+ }
+
+ // inlined that filter for null elements
+ if (disposables[idx] != null)
+ {
+ return true;
+ }
+ }
+ }
+
+ public void Reset()
+ {
+ _index = -1;
+ }
+ }
+}
diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/Disposable.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/Disposable.cs
new file mode 100644
index 000000000..17b0d01db
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/Disposable.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Threading;
+
+namespace Avalonia.Reactive;
+
+///
+/// Provides a set of static methods for creating objects.
+///
+internal static class Disposable
+{
+ ///
+ /// Represents a disposable that does nothing on disposal.
+ ///
+ private sealed class EmptyDisposable : IDisposable
+ {
+ public static readonly EmptyDisposable Instance = new();
+
+ private EmptyDisposable()
+ {
+ }
+
+ public void Dispose()
+ {
+ // no op
+ }
+ }
+
+ internal sealed class AnonymousDisposable : IDisposable
+ {
+ private volatile Action? _dispose;
+ public AnonymousDisposable(Action dispose)
+ {
+ _dispose = dispose;
+ }
+ public bool IsDisposed => _dispose == null;
+ public void Dispose()
+ {
+ Interlocked.Exchange(ref _dispose, null)?.Invoke();
+ }
+ }
+
+ internal sealed class AnonymousDisposable : IDisposable
+ {
+ private TState _state;
+ private volatile Action? _dispose;
+
+ public AnonymousDisposable(TState state, Action dispose)
+ {
+ _state = state;
+ _dispose = dispose;
+ }
+
+ public bool IsDisposed => _dispose == null;
+ public void Dispose()
+ {
+ Interlocked.Exchange(ref _dispose, null)?.Invoke(_state);
+ _state = default!;
+ }
+ }
+
+ ///
+ /// Gets the disposable that does nothing when disposed.
+ ///
+ public static IDisposable Empty => EmptyDisposable.Instance;
+
+ ///
+ /// Creates a disposable object that invokes the specified action when disposed.
+ ///
+ /// Action to run during the first call to . The action is guaranteed to be run at most once.
+ /// The disposable object that runs the given action upon disposal.
+ /// is null.
+ public static IDisposable Create(Action dispose)
+ {
+ if (dispose == null)
+ {
+ throw new ArgumentNullException(nameof(dispose));
+ }
+
+ return new AnonymousDisposable(dispose);
+ }
+
+ ///
+ /// Creates a disposable object that invokes the specified action when disposed.
+ ///
+ /// The state to be passed to the action.
+ /// Action to run during the first call to . The action is guaranteed to be run at most once.
+ /// The disposable object that runs the given action upon disposal.
+ /// is null.
+ public static IDisposable Create(TState state, Action dispose)
+ {
+ if (dispose == null)
+ {
+ throw new ArgumentNullException(nameof(dispose));
+ }
+
+ return new AnonymousDisposable(state, dispose);
+ }
+}
diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/DisposableMixin.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/DisposableMixin.cs
new file mode 100644
index 000000000..478312fa4
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/DisposableMixin.cs
@@ -0,0 +1,37 @@
+using System;
+using Avalonia.Reactive;
+
+namespace Avalonia.Reactive;
+
+///
+/// Extension methods associated with the IDisposable interface.
+///
+internal static class DisposableMixin
+{
+ ///
+ /// Ensures the provided disposable is disposed with the specified .
+ ///
+ ///
+ /// The type of the disposable.
+ ///
+ ///
+ /// The disposable we are going to want to be disposed by the CompositeDisposable.
+ ///
+ ///
+ /// The to which will be added.
+ ///
+ ///
+ /// The disposable.
+ ///
+ public static T DisposeWith(this T item, CompositeDisposable compositeDisposable)
+ where T : IDisposable
+ {
+ if (compositeDisposable is null)
+ {
+ throw new ArgumentNullException(nameof(compositeDisposable));
+ }
+
+ compositeDisposable.Add(item);
+ return item;
+ }
+}
diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/IAvaloniaSubject.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/IAvaloniaSubject.cs
new file mode 100644
index 000000000..272e2190b
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/IAvaloniaSubject.cs
@@ -0,0 +1,8 @@
+using System;
+
+namespace Avalonia.Reactive;
+
+internal interface IAvaloniaSubject : IObserver, IObservable /*, ISubject */
+{
+
+}
diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/LightweightObservableBase.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/LightweightObservableBase.cs
new file mode 100644
index 000000000..0688a4edb
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/LightweightObservableBase.cs
@@ -0,0 +1,237 @@
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Threading;
+
+namespace Avalonia.Reactive
+{
+ ///
+ /// Lightweight base class for observable implementations.
+ ///
+ /// The observable type.
+ ///
+ /// ObservableBase{T} is rather heavyweight in terms of allocations and memory
+ /// usage. This class provides a more lightweight base for some internal observable types
+ /// in the Avalonia framework.
+ ///
+ internal abstract class LightweightObservableBase : IObservable
+ {
+ private Exception? _error;
+ private List>? _observers = new List>();
+
+ public bool HasObservers => _observers?.Count > 0;
+
+ public IDisposable Subscribe(IObserver observer)
+ {
+ _ = observer ?? throw new ArgumentNullException(nameof(observer));
+
+ //Dispatcher.UIThread.VerifyAccess();
+
+ var first = false;
+
+ for (; ; )
+ {
+ if (Volatile.Read(ref _observers) == null)
+ {
+ if (_error != null)
+ {
+ observer.OnError(_error);
+ }
+ else
+ {
+ observer.OnCompleted();
+ }
+
+ return Disposable.Empty;
+ }
+
+ lock (this)
+ {
+ if (_observers == null)
+ {
+ continue;
+ }
+
+ first = _observers.Count == 0;
+ _observers.Add(observer);
+ break;
+ }
+ }
+
+ if (first)
+ {
+ Initialize();
+ }
+
+ Subscribed(observer, first);
+
+ return new RemoveObserver(this, observer);
+ }
+
+ void Remove(IObserver observer)
+ {
+ if (Volatile.Read(ref _observers) != null)
+ {
+ lock (this)
+ {
+ var observers = _observers;
+
+ if (observers != null)
+ {
+ observers.Remove(observer);
+
+ if (observers.Count == 0)
+ {
+ observers.TrimExcess();
+ Deinitialize();
+ }
+ }
+ }
+ }
+ }
+
+ sealed class RemoveObserver : IDisposable
+ {
+ LightweightObservableBase? _parent;
+
+ IObserver? _observer;
+
+ public RemoveObserver(LightweightObservableBase parent, IObserver observer)
+ {
+ _parent = parent;
+ Volatile.Write(ref _observer, observer);
+ }
+
+ public void Dispose()
+ {
+ var observer = _observer;
+ Interlocked.Exchange(ref _parent, null)?.Remove(observer!);
+ _observer = null;
+ }
+ }
+
+ protected abstract void Initialize();
+ protected abstract void Deinitialize();
+
+ protected void PublishNext(T value)
+ {
+ if (Volatile.Read(ref _observers) != null)
+ {
+ IObserver[]? observers = null;
+ int count = 0;
+
+ // Optimize for the common case of 1/2/3 observers.
+ IObserver? observer0 = null;
+ IObserver? observer1 = null;
+ IObserver? observer2 = null;
+ lock (this)
+ {
+ if (_observers == null)
+ {
+ return;
+ }
+
+ count = _observers.Count;
+ switch (count)
+ {
+ case 3:
+ observer0 = _observers[0];
+ observer1 = _observers[1];
+ observer2 = _observers[2];
+ break;
+ case 2:
+ observer0 = _observers[0];
+ observer1 = _observers[1];
+ break;
+ case 1:
+ observer0 = _observers[0];
+ break;
+ case 0:
+ return;
+ default:
+ {
+ observers = ArrayPool>.Shared.Rent(count);
+ _observers.CopyTo(observers);
+ break;
+ }
+ }
+ }
+
+ if (observer0 != null)
+ {
+ observer0.OnNext(value);
+ observer1?.OnNext(value);
+ observer2?.OnNext(value);
+ }
+ else if (observers != null)
+ {
+ for(int i = 0; i < count; i++)
+ {
+ observers[i].OnNext(value);
+ // Avoid memory leak by clearing the reference.
+ observers[i] = null!;
+ }
+
+ ArrayPool>.Shared.Return(observers);
+ }
+ }
+ }
+
+ protected void PublishCompleted()
+ {
+ if (Volatile.Read(ref _observers) != null)
+ {
+ IObserver[] observers;
+
+ lock (this)
+ {
+ if (_observers == null)
+ {
+ return;
+ }
+ observers = _observers.ToArray();
+ Volatile.Write(ref _observers, null);
+ }
+
+ foreach (var observer in observers)
+ {
+ observer.OnCompleted();
+ }
+
+ Deinitialize();
+ }
+ }
+
+ protected void PublishError(Exception error)
+ {
+ if (Volatile.Read(ref _observers) != null)
+ {
+
+ IObserver[] observers;
+
+ lock (this)
+ {
+ if (_observers == null)
+ {
+ return;
+ }
+
+ _error = error;
+ observers = _observers.ToArray();
+ Volatile.Write(ref _observers, null);
+ }
+
+ foreach (var observer in observers)
+ {
+ observer.OnError(error);
+ }
+
+ Deinitialize();
+ }
+ }
+
+ protected virtual void Subscribed(IObserver observer, bool first)
+ {
+ }
+ }
+}
diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/LightweightSubject.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/LightweightSubject.cs
new file mode 100644
index 000000000..5663fd66d
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/LightweightSubject.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Threading;
+using Avalonia.Threading;
+
+namespace Avalonia.Reactive;
+
+internal class LightweightSubject : LightweightObservableBase, IAvaloniaSubject
+{
+ public void OnCompleted()
+ {
+ PublishCompleted();
+ }
+
+ public void OnError(Exception error)
+ {
+ PublishError(error);
+ }
+
+ public void OnNext(T value)
+ {
+ PublishNext(value);
+ }
+
+ protected override void Initialize() { }
+
+ protected override void Deinitialize() { }
+}
diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/Observable.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/Observable.cs
new file mode 100644
index 000000000..4cffdaa68
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/Observable.cs
@@ -0,0 +1,259 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Reactive.Operators;
+using Avalonia.Threading;
+
+namespace Avalonia.Reactive;
+
+///
+/// Provides common observable methods as a replacement for the Rx framework.
+///
+internal static class Observable
+{
+ public static IObservable Create(Func, IDisposable> subscribe)
+ {
+ return new CreateWithDisposableObservable(subscribe);
+ }
+
+ public static IDisposable Subscribe(this IObservable source, Action action)
+ {
+ return source.Subscribe(new AnonymousObserver(action));
+ }
+
+ public static IObservable Select(this IObservable source, Func selector)
+ {
+ return Create(obs =>
+ {
+ return source.Subscribe(new AnonymousObserver(
+ input =>
+ {
+ TResult value;
+ try
+ {
+ value = selector(input);
+ }
+ catch (Exception ex)
+ {
+ obs.OnError(ex);
+ return;
+ }
+
+ obs.OnNext(value);
+ }, obs.OnError, obs.OnCompleted));
+ });
+ }
+
+ public static IObservable StartWith(this IObservable source, TSource value)
+ {
+ return Create(obs =>
+ {
+ obs.OnNext(value);
+ return source.Subscribe(obs);
+ });
+ }
+
+ public static IObservable Where(this IObservable source, Func predicate)
+ {
+ return Create(obs =>
+ {
+ return source.Subscribe(new AnonymousObserver(
+ input =>
+ {
+ bool shouldRun;
+ try
+ {
+ shouldRun = predicate(input);
+ }
+ catch (Exception ex)
+ {
+ obs.OnError(ex);
+ return;
+ }
+ if (shouldRun)
+ {
+ obs.OnNext(input);
+ }
+ }, obs.OnError, obs.OnCompleted));
+ });
+ }
+
+ public static IObservable Switch(
+ this IObservable> sources)
+ {
+ return new Switch(sources);
+ }
+
+ public static IObservable CombineLatest(
+ this IObservable first, IObservable second,
+ Func resultSelector)
+ {
+ return new CombineLatest(first, second, resultSelector);
+ }
+
+ public static IObservable CombineLatest(
+ this IEnumerable> inputs)
+ {
+ return new CombineLatest(inputs, items => items);
+ }
+
+ public static IObservable Skip(this IObservable source, int skipCount)
+ {
+ if (skipCount <= 0)
+ {
+ throw new ArgumentException("Skip count must be bigger than zero", nameof(skipCount));
+ }
+
+ return Create(obs =>
+ {
+ var remaining = skipCount;
+ return source.Subscribe(new AnonymousObserver(
+ input =>
+ {
+ if (remaining <= 0)
+ {
+ obs.OnNext(input);
+ }
+ else
+ {
+ remaining--;
+ }
+ }, obs.OnError, obs.OnCompleted));
+ });
+ }
+
+ public static IObservable Take(this IObservable source, int takeCount)
+ {
+ if (takeCount <= 0)
+ {
+ return Empty();
+ }
+
+ return Create(obs =>
+ {
+ var remaining = takeCount;
+ IDisposable? sub = null;
+ sub = source.Subscribe(new AnonymousObserver(
+ input =>
+ {
+ if (remaining > 0)
+ {
+ --remaining;
+ obs.OnNext(input);
+
+ if (remaining == 0)
+ {
+ sub?.Dispose();
+ obs.OnCompleted();
+ }
+ }
+ }, obs.OnError, obs.OnCompleted));
+ return sub;
+ });
+ }
+
+ public static IObservable FromEventPattern(Action addHandler, Action removeHandler)
+ {
+ return Create(observer =>
+ {
+ var handler = new Action(observer.OnNext);
+ var converted = new EventHandler((_, args) => handler(args));
+ addHandler(converted);
+
+ return Disposable.Create(() => removeHandler(converted));
+ });
+ }
+
+ public static IObservable FromEventPattern(Action> addHandler, Action> removeHandler) where T : EventArgs
+ {
+ return Create(observer =>
+ {
+ var handler = new Action(observer.OnNext);
+ var converted = new EventHandler((_, args) => handler(args));
+ addHandler(converted);
+
+ return Disposable.Create(() => removeHandler(converted));
+ });
+ }
+
+ public static IObservable Return(T value)
+ {
+ return new ReturnImpl(value);
+ }
+
+ public static IObservable Empty()
+ {
+ return EmptyImpl.Instance;
+ }
+
+ ///
+ /// Returns an observable that fires once with the specified value and never completes.
+ ///
+ /// The type of the value.
+ /// The value.
+ /// The observable.
+ public static IObservable SingleValue(T value)
+ {
+ return new SingleValueImpl(value);
+ }
+
+ private sealed class SingleValueImpl : IObservable
+ {
+ private readonly T _value;
+
+ public SingleValueImpl(T value)
+ {
+ _value = value;
+ }
+ public IDisposable Subscribe(IObserver observer)
+ {
+ observer.OnNext(_value);
+ return Disposable.Empty;
+ }
+ }
+
+ private sealed class ReturnImpl : IObservable
+ {
+ private readonly T _value;
+
+ public ReturnImpl(T value)
+ {
+ _value = value;
+ }
+ public IDisposable Subscribe(IObserver observer)
+ {
+ observer.OnNext(_value);
+ observer.OnCompleted();
+ return Disposable.Empty;
+ }
+ }
+
+ internal sealed class EmptyImpl : IObservable
+ {
+ internal static readonly IObservable Instance = new EmptyImpl();
+
+ private EmptyImpl() { }
+
+ public IDisposable Subscribe(IObserver observer)
+ {
+ observer.OnCompleted();
+ return Disposable.Empty;
+ }
+ }
+
+ private sealed class CreateWithDisposableObservable : IObservable
+ {
+ private readonly Func, IDisposable> _subscribe;
+
+ public CreateWithDisposableObservable(Func, IDisposable> subscribe)
+ {
+ _subscribe = subscribe;
+ }
+
+ public IDisposable Subscribe(IObserver observer)
+ {
+ return _subscribe(observer);
+ }
+ }
+}
diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/Operators/CombineLatest.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/Operators/CombineLatest.cs
new file mode 100644
index 000000000..aa95ceec3
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/Operators/CombineLatest.cs
@@ -0,0 +1,374 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+
+namespace Avalonia.Reactive.Operators;
+
+// Code based on https://github.com/dotnet/reactive/blob/main/Rx.NET/Source/src/System.Reactive/Linq/Observable/CombineLatest.cs
+
+internal sealed class CombineLatest : IObservable
+{
+ private readonly IObservable _first;
+ private readonly IObservable _second;
+ private readonly Func _resultSelector;
+
+ public CombineLatest(IObservable first, IObservable second,
+ Func resultSelector)
+ {
+ _first = first;
+ _second = second;
+ _resultSelector = resultSelector;
+ }
+
+ public IDisposable Subscribe(IObserver observer)
+ {
+ var sink = new _(_resultSelector, observer);
+ sink.Run(_first, _second);
+ return sink;
+ }
+
+ internal sealed class _ : IdentitySink
+ {
+ private readonly Func _resultSelector;
+ private readonly object _gate = new object();
+
+ public _(Func resultSelector, IObserver observer)
+ : base(observer)
+ {
+ _resultSelector = resultSelector;
+ _firstDisposable = null!;
+ _secondDisposable = null!;
+ }
+
+ private IDisposable _firstDisposable;
+ private IDisposable _secondDisposable;
+
+ public void Run(IObservable first, IObservable second)
+ {
+ var fstO = new FirstObserver(this);
+ var sndO = new SecondObserver(this);
+
+ fstO.SetOther(sndO);
+ sndO.SetOther(fstO);
+
+ _firstDisposable = first.Subscribe(fstO);
+ _secondDisposable = second.Subscribe(sndO);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _firstDisposable.Dispose();
+ _secondDisposable.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ private sealed class FirstObserver : IObserver
+ {
+ private readonly _ _parent;
+ private SecondObserver _other;
+
+ public FirstObserver(_ parent)
+ {
+ _parent = parent;
+ _other = default!; // NB: Will be set by SetOther.
+ }
+
+ public void SetOther(SecondObserver other) { _other = other; }
+
+ public bool HasValue { get; private set; }
+ public TFirst? Value { get; private set; }
+ public bool Done { get; private set; }
+
+ public void OnNext(TFirst value)
+ {
+ lock (_parent._gate)
+ {
+ HasValue = true;
+ Value = value;
+
+ if (_other.HasValue)
+ {
+ TResult res;
+ try
+ {
+ res = _parent._resultSelector(value, _other.Value!);
+ }
+ catch (Exception ex)
+ {
+ _parent.ForwardOnError(ex);
+ return;
+ }
+
+ _parent.ForwardOnNext(res);
+ }
+ else if (_other.Done)
+ {
+ _parent.ForwardOnCompleted();
+ }
+ }
+ }
+
+ public void OnError(Exception error)
+ {
+ lock (_parent._gate)
+ {
+ _parent.ForwardOnError(error);
+ }
+ }
+
+ public void OnCompleted()
+ {
+ lock (_parent._gate)
+ {
+ Done = true;
+
+ if (_other.Done)
+ {
+ _parent.ForwardOnCompleted();
+ }
+ else
+ {
+ _parent._firstDisposable.Dispose();
+ }
+ }
+ }
+ }
+
+ private sealed class SecondObserver : IObserver
+ {
+ private readonly _ _parent;
+ private FirstObserver _other;
+
+ public SecondObserver(_ parent)
+ {
+ _parent = parent;
+ _other = default!; // NB: Will be set by SetOther.
+ }
+
+ public void SetOther(FirstObserver other) { _other = other; }
+
+ public bool HasValue { get; private set; }
+ public TSecond? Value { get; private set; }
+ public bool Done { get; private set; }
+
+ public void OnNext(TSecond value)
+ {
+ lock (_parent._gate)
+ {
+ HasValue = true;
+ Value = value;
+
+ if (_other.HasValue)
+ {
+ TResult res;
+ try
+ {
+ res = _parent._resultSelector(_other.Value!, value);
+ }
+ catch (Exception ex)
+ {
+ _parent.ForwardOnError(ex);
+ return;
+ }
+
+ _parent.ForwardOnNext(res);
+ }
+ else if (_other.Done)
+ {
+ _parent.ForwardOnCompleted();
+ }
+ }
+ }
+
+ public void OnError(Exception error)
+ {
+ lock (_parent._gate)
+ {
+ _parent.ForwardOnError(error);
+ }
+ }
+
+ public void OnCompleted()
+ {
+ lock (_parent._gate)
+ {
+ Done = true;
+
+ if (_other.Done)
+ {
+ _parent.ForwardOnCompleted();
+ }
+ else
+ {
+ _parent._secondDisposable.Dispose();
+ }
+ }
+ }
+ }
+ }
+}
+
+internal sealed class CombineLatest : IObservable
+{
+ private readonly IEnumerable> _sources;
+ private readonly Func _resultSelector;
+
+ public CombineLatest(IEnumerable> sources, Func resultSelector)
+ {
+ _sources = sources;
+ _resultSelector = resultSelector;
+ }
+
+ public IDisposable Subscribe(IObserver observer)
+ {
+ var sink = new _(_resultSelector, observer);
+ sink.Run(_sources);
+ return sink;
+ }
+
+ internal sealed class _ : IdentitySink
+ {
+ private readonly object _gate = new object();
+ private readonly Func _resultSelector;
+
+ public _(Func resultSelector, IObserver observer)
+ : base(observer)
+ {
+ _resultSelector = resultSelector;
+
+ // NB: These will be set in Run before getting used.
+ _hasValue = null!;
+ _values = null!;
+ _isDone = null!;
+ _subscriptions = null!;
+ }
+
+ private bool[] _hasValue;
+ private bool _hasValueAll;
+ private TSource[] _values;
+ private bool[] _isDone;
+ private IDisposable[] _subscriptions;
+
+ public void Run(IEnumerable> sources)
+ {
+ var srcs = sources.ToArray();
+
+ var N = srcs.Length;
+
+ _hasValue = new bool[N];
+ _hasValueAll = false;
+
+ _values = new TSource[N];
+
+ _isDone = new bool[N];
+
+ _subscriptions = new IDisposable[N];
+
+ for (var i = 0; i < N; i++)
+ {
+ var j = i;
+
+ var o = new SourceObserver(this, j);
+ _subscriptions[j] = o;
+
+ o.Disposable = srcs[j].Subscribe(o);
+ }
+
+ SetUpstream(new CompositeDisposable(_subscriptions));
+ }
+
+ private void OnNext(int index, TSource value)
+ {
+ lock (_gate)
+ {
+ _values[index] = value;
+
+ _hasValue[index] = true;
+
+ if (_hasValueAll || (_hasValueAll = _hasValue.All(v => v)))
+ {
+ TResult res;
+ try
+ {
+ res = _resultSelector(_values);
+ }
+ catch (Exception ex)
+ {
+ ForwardOnError(ex);
+ return;
+ }
+
+ ForwardOnNext(res);
+ }
+ else if (_isDone.Where((_, i) => i != index).All(d => d))
+ {
+ ForwardOnCompleted();
+ }
+ }
+ }
+
+ private new void OnError(Exception error)
+ {
+ lock (_gate)
+ {
+ ForwardOnError(error);
+ }
+ }
+
+ private void OnCompleted(int index)
+ {
+ lock (_gate)
+ {
+ _isDone[index] = true;
+
+ if (_isDone.All(d => d))
+ {
+ ForwardOnCompleted();
+ }
+ else
+ {
+ _subscriptions[index].Dispose();
+ }
+ }
+ }
+
+ private sealed class SourceObserver : IObserver, IDisposable
+ {
+ private readonly _ _parent;
+ private readonly int _index;
+
+ public SourceObserver(_ parent, int index)
+ {
+ _parent = parent;
+ _index = index;
+ }
+
+ public IDisposable? Disposable { get; set; }
+
+ public void OnNext(TSource value)
+ {
+ _parent.OnNext(_index, value);
+ }
+
+ public void OnError(Exception error)
+ {
+ _parent.OnError(error);
+ }
+
+ public void OnCompleted()
+ {
+ _parent.OnCompleted(_index);
+ }
+
+ public void Dispose()
+ {
+ Disposable?.Dispose();
+ }
+ }
+ }
+}
diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/Operators/Sink.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/Operators/Sink.cs
new file mode 100644
index 000000000..7acb5833d
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/Operators/Sink.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Threading;
+
+namespace Avalonia.Reactive.Operators;
+
+// Code based on https://github.com/dotnet/reactive/blob/main/Rx.NET/Source/src/System.Reactive/Internal/Sink.cs
+
+internal abstract class Sink : IDisposable
+{
+ private IDisposable? _upstream;
+ private readonly IObserver _observer;
+
+ protected Sink(IObserver observer)
+ {
+ _observer = observer;
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ ///
+ /// Override this method to dispose additional resources.
+ /// The method is guaranteed to be called at most once.
+ ///
+ /// If true, the method was called from .
+ protected virtual void Dispose(bool disposing)
+ {
+ //Calling base.Dispose(true) is not a proper disposal, so we can omit the assignment here.
+ //Sink is internal so this can pretty much be enforced.
+ //_observer = NopObserver.Instance;
+
+ _upstream?.Dispose();
+ }
+
+ public void ForwardOnNext(TTarget value)
+ {
+ _observer.OnNext(value);
+ }
+
+ public void ForwardOnCompleted()
+ {
+ _observer.OnCompleted();
+ Dispose();
+ }
+
+ public void ForwardOnError(Exception error)
+ {
+ _observer.OnError(error);
+ Dispose();
+ }
+
+ protected void SetUpstream(IDisposable upstream)
+ {
+ _upstream = upstream;
+ }
+
+ protected void DisposeUpstream()
+ {
+ _upstream?.Dispose();
+ }
+}
+
+internal abstract class Sink : Sink, IObserver
+{
+ protected Sink(IObserver observer) : base(observer)
+ {
+ }
+
+ public virtual void Run(IObservable source)
+ {
+ SetUpstream(source.Subscribe(this));
+ }
+
+ public abstract void OnNext(TSource value);
+
+ public virtual void OnError(Exception error) => ForwardOnError(error);
+
+ public virtual void OnCompleted() => ForwardOnCompleted();
+
+ public IObserver GetForwarder() => new _(this);
+
+ private sealed class _ : IObserver
+ {
+ private readonly Sink _forward;
+
+ public _(Sink forward)
+ {
+ _forward = forward;
+ }
+
+ public void OnNext(TTarget value) => _forward.ForwardOnNext(value);
+
+ public void OnError(Exception error) => _forward.ForwardOnError(error);
+
+ public void OnCompleted() => _forward.ForwardOnCompleted();
+ }
+}
+
+internal abstract class IdentitySink : Sink
+{
+ protected IdentitySink(IObserver observer) : base(observer)
+ {
+ }
+
+ public override void OnNext(T value)
+ {
+ ForwardOnNext(value);
+ }
+}
diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/Operators/Switch.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/Operators/Switch.cs
new file mode 100644
index 000000000..f0ca9e3ba
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/Operators/Switch.cs
@@ -0,0 +1,145 @@
+using System;
+
+namespace Avalonia.Reactive.Operators;
+
+// Code based on https://github.com/dotnet/reactive/blob/main/Rx.NET/Source/src/System.Reactive/Linq/Observable/Switch.cs
+
+internal sealed class Switch : IObservable
+{
+ private readonly IObservable> _sources;
+
+ public Switch(IObservable> sources)
+ {
+ _sources = sources;
+ }
+
+ public IDisposable Subscribe(IObserver observer)
+ {
+ return _sources.Subscribe(new _(observer));
+ }
+
+ internal sealed class _ : Sink, TSource>
+ {
+ private readonly object _gate = new object();
+
+ public _(IObserver observer)
+ : base(observer)
+ {
+ }
+
+ private IDisposable? _innerSerialDisposable;
+ private bool _isStopped;
+ private ulong _latest;
+ private bool _hasLatest;
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _innerSerialDisposable?.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ public override void OnNext(IObservable value)
+ {
+ ulong id;
+
+ lock (_gate)
+ {
+ id = unchecked(++_latest);
+ _hasLatest = true;
+ }
+
+ var innerObserver = new InnerObserver(this, id);
+
+ _innerSerialDisposable?.Dispose();
+ _innerSerialDisposable = innerObserver;
+ innerObserver.Disposable = value.Subscribe(innerObserver);
+ }
+
+ public override void OnError(Exception error)
+ {
+ lock (_gate)
+ {
+ ForwardOnError(error);
+ }
+ }
+
+ public override void OnCompleted()
+ {
+ lock (_gate)
+ {
+ DisposeUpstream();
+
+ _isStopped = true;
+ if (!_hasLatest)
+ {
+ ForwardOnCompleted();
+ }
+ }
+ }
+
+ private sealed class InnerObserver : IObserver, IDisposable
+ {
+ private readonly _ _parent;
+ private readonly ulong _id;
+
+ public InnerObserver(_ parent, ulong id)
+ {
+ _parent = parent;
+ _id = id;
+ }
+
+ public IDisposable? Disposable { get; set; }
+
+ public void OnNext(TSource value)
+ {
+ lock (_parent._gate)
+ {
+ if (_parent._latest == _id)
+ {
+ _parent.ForwardOnNext(value);
+ }
+ }
+ }
+
+ public void OnError(Exception error)
+ {
+ lock (_parent._gate)
+ {
+ Dispose();
+
+ if (_parent._latest == _id)
+ {
+ _parent.ForwardOnError(error);
+ }
+ }
+ }
+
+ public void OnCompleted()
+ {
+ lock (_parent._gate)
+ {
+ Dispose();
+
+ if (_parent._latest == _id)
+ {
+ _parent._hasLatest = false;
+
+ if (_parent._isStopped)
+ {
+ _parent.ForwardOnCompleted();
+ }
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ Disposable?.Dispose();
+ }
+ }
+ }
+}
diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/SingleSubscriberObservableBase.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/SingleSubscriberObservableBase.cs
new file mode 100644
index 000000000..10d42214c
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/SingleSubscriberObservableBase.cs
@@ -0,0 +1,78 @@
+using System;
+using Avalonia.Threading;
+
+namespace Avalonia.Reactive
+{
+ internal abstract class SingleSubscriberObservableBase : IObservable, IDisposable
+ {
+ private Exception? _error;
+ private IObserver? _observer;
+ private bool _completed;
+
+ public IDisposable Subscribe(IObserver observer)
+ {
+ _ = observer ?? throw new ArgumentNullException(nameof(observer));
+ Dispatcher.UIThread.VerifyAccess();
+
+ if (_observer != null)
+ {
+ throw new InvalidOperationException("The observable can only be subscribed once.");
+ }
+
+ if (_error != null)
+ {
+ observer.OnError(_error);
+ }
+ else if (_completed)
+ {
+ observer.OnCompleted();
+ }
+ else
+ {
+ _observer = observer;
+ Subscribed();
+ }
+
+ return this;
+ }
+
+ public virtual void Dispose()
+ {
+ Unsubscribed();
+ _observer = null;
+ }
+
+ protected abstract void Unsubscribed();
+
+ protected void PublishNext(T value)
+ {
+ _observer?.OnNext(value);
+ }
+
+ protected void PublishCompleted()
+ {
+ _completed = true;
+
+ if (_observer != null)
+ {
+ _observer.OnCompleted();
+ Unsubscribed();
+ _observer = null;
+ }
+ }
+
+ protected void PublishError(Exception error)
+ {
+ _error = error;
+
+ if (_observer != null)
+ {
+ _observer.OnError(error);
+ Unsubscribed();
+ _observer = null;
+ }
+ }
+
+ protected abstract void Subscribed();
+ }
+}
diff --git a/samples/AvaloniaControlCatalog/Converter/DegToRadConverter.cs b/samples/AvaloniaControlCatalog/Converter/DegToRadConverter.cs
new file mode 100644
index 000000000..b062bcb64
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Converter/DegToRadConverter.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+using Avalonia.Data.Converters;
+
+namespace ControlCatalog.Converter
+{
+ public class DegToRadConverter : IValueConverter
+ {
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is double rad)
+ {
+ return rad * 180.0d / Math.PI;
+ }
+ return 0.0d;
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is double deg)
+ {
+ return deg / 180.0d * Math.PI;
+ }
+ return 0.0d;
+ }
+ }
+}
diff --git a/samples/AvaloniaControlCatalog/Converter/GdpToBrushConverter.cs b/samples/AvaloniaControlCatalog/Converter/GdpToBrushConverter.cs
new file mode 100644
index 000000000..d5d6f5cd0
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Converter/GdpToBrushConverter.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Globalization;
+using Avalonia;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+using Avalonia.Media.Immutable;
+
+namespace ControlCatalog.Converter;
+
+public sealed class GdpToBrushConverter : IValueConverter
+{
+ private readonly ImmutableSolidColorBrush _orangeBrush = new(Colors.Orange, 0.6);
+ private readonly ImmutableSolidColorBrush _yellowBrush = new(Colors.Yellow, 0.6);
+ private readonly ImmutableSolidColorBrush _greenBrush = new(Colors.LightGreen, 0.6);
+
+ public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is not int gdp)
+ return AvaloniaProperty.UnsetValue;
+
+ return gdp switch
+ {
+ <= 5000 => _orangeBrush,
+ <= 10000 => _yellowBrush,
+ _ => _greenBrush
+ };
+ }
+
+ object IValueConverter.ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ => throw new NotSupportedException();
+}
diff --git a/samples/AvaloniaControlCatalog/Converter/HexConverter.cs b/samples/AvaloniaControlCatalog/Converter/HexConverter.cs
new file mode 100644
index 000000000..31cce5ba6
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Converter/HexConverter.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Globalization;
+using Avalonia;
+using Avalonia.Data.Converters;
+
+namespace ControlCatalog.Converter;
+
+public class HexConverter : IValueConverter
+{
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ var str = value?.ToString();
+ if (str == null)
+ return AvaloniaProperty.UnsetValue;
+ if (int.TryParse(str, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int x))
+ return (decimal)x;
+ return AvaloniaProperty.UnsetValue;
+
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ try
+ {
+ if (value is decimal d)
+ return ((int)d).ToString("X8");
+ return AvaloniaProperty.UnsetValue;
+ }
+ catch
+ {
+ return AvaloniaProperty.UnsetValue;
+ }
+ }
+}
diff --git a/samples/AvaloniaControlCatalog/Converter/MathSubtractConverter.cs b/samples/AvaloniaControlCatalog/Converter/MathSubtractConverter.cs
new file mode 100644
index 000000000..6f3c99abe
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Converter/MathSubtractConverter.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace ControlCatalog.Converter;
+
+public class MathSubtractConverter : IValueConverter
+{
+ public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is double dv && parameter is double dp)
+ {
+ return dv - dp;
+ }
+ return double.NaN;
+ }
+
+ public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotSupportedException();
+ }
+}
diff --git a/samples/AvaloniaControlCatalog/Converter/TableViewColumnWidthConverter.cs b/samples/AvaloniaControlCatalog/Converter/TableViewColumnWidthConverter.cs
new file mode 100644
index 000000000..e9f976f6f
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Converter/TableViewColumnWidthConverter.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Globalization;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Data.Converters;
+
+namespace ControlCatalog.Converter;
+
+public sealed class TableViewColumnWidthConverter : IValueConverter
+{
+ public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is bool useStarSize &&
+ parameter is string stringParameter &&
+ double.TryParse(stringParameter, NumberStyles.Number, CultureInfo.InvariantCulture, out var baseWidth))
+ {
+ return useStarSize ?
+ new GridLength(baseWidth, GridUnitType.Star) :
+ new GridLength(baseWidth * 100, GridUnitType.Pixel);
+ }
+
+ return AvaloniaProperty.UnsetValue;
+ }
+
+ object? IValueConverter.ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ => throw new NotSupportedException();
+}
diff --git a/samples/AvaloniaControlCatalog/DecoratedWindow.xaml b/samples/AvaloniaControlCatalog/DecoratedWindow.xaml
new file mode 100644
index 000000000..804eebfb4
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/DecoratedWindow.xaml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+
+
+
+
+
+
+ Hello world!
+
+
+
+ None
+ BorderOnly
+ Full
+
+
+ CanResize
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/AvaloniaControlCatalog/DecoratedWindow.xaml.cs b/samples/AvaloniaControlCatalog/DecoratedWindow.xaml.cs
new file mode 100644
index 000000000..bb87d982b
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/DecoratedWindow.xaml.cs
@@ -0,0 +1,45 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+
+namespace ControlCatalog
+{
+ public partial class DecoratedWindow : Window
+ {
+ public DecoratedWindow()
+ {
+ InitializeComponent();
+ TitleBar.PointerPressed += (i, e) =>
+ {
+ BeginMoveDrag(e);
+ };
+ SetupSide(Left, StandardCursorType.LeftSide, WindowEdge.West);
+ SetupSide(Right, StandardCursorType.RightSide, WindowEdge.East);
+ SetupSide(Top, StandardCursorType.TopSide, WindowEdge.North);
+ SetupSide(Bottom, StandardCursorType.BottomSide, WindowEdge.South);
+ SetupSide(TopLeft, StandardCursorType.TopLeftCorner, WindowEdge.NorthWest);
+ SetupSide(TopRight, StandardCursorType.TopRightCorner, WindowEdge.NorthEast);
+ SetupSide(BottomLeft, StandardCursorType.BottomLeftCorner, WindowEdge.SouthWest);
+ SetupSide(BottomRight, StandardCursorType.BottomRightCorner, WindowEdge.SouthEast);
+ MinimizeButton.Click += delegate
+ { this.WindowState = WindowState.Minimized; };
+ MaximizeButton.Click += delegate
+ {
+ WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
+ };
+ CloseButton.Click += delegate
+ {
+ Close();
+ };
+ }
+
+ private void SetupSide(Control ctl, StandardCursorType cursor, WindowEdge edge)
+ {
+ ctl.Cursor = new Cursor(cursor);
+ ctl.PointerPressed += (i, e) =>
+ {
+ if (WindowState == WindowState.Normal)
+ BeginResizeDrag(edge, e);
+ };
+ }
+ }
+}
diff --git a/samples/AvaloniaControlCatalog/Icons.cs b/samples/AvaloniaControlCatalog/Icons.cs
new file mode 100644
index 000000000..58d0e0b7d
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/Icons.cs
@@ -0,0 +1,194 @@
+namespace ControlCatalog
+{
+ internal static class Icons
+ {
+ // Layers/Composition
+ public const string Layers = "M12,16L19.36,10.27L21,9L12,2L3,9L4.63,10.27M12,18.54L4.62,12.81L3,14.07L12,21.07L21,14.07L19.37,12.8";
+
+ // Keyboard
+ public const string Keyboard = "M19,10H17V8H19M19,13H17V11H19M16,10H14V8H16M16,13H14V11H16M16,17H8V15H16M7,10H5V8H7M7,13H5V11H7M8,11H10V13H8M8,8H10V10H8M11,11H13V13H11M11,8H13V10H11M20,5H4C2.89,5 2,5.89 2,7V17A2,2 0 0,0 4,19H20A2,2 0 0,0 22,17V7C22,5.89 21.1,5 20,5Z";
+
+ // Blur/Acrylic
+ public const string Blur = "M14.5,10C16.43,10 18,8.43 18,6.5C18,4.57 16.43,3 14.5,3C12.57,3 11,4.57 11,6.5C11,8.43 12.57,10 14.5,10M14.5,4A2.5,2.5 0 0,1 17,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,4M8.5,14C10.43,14 12,12.43 12,10.5C12,8.57 10.43,7 8.5,7C6.57,7 5,8.57 5,10.5C5,12.43 6.57,14 8.5,14M8.5,8A2.5,2.5 0 0,1 11,10.5A2.5,2.5 0 0,1 8.5,13A2.5,2.5 0 0,1 6,10.5A2.5,2.5 0 0,1 8.5,8M14.5,16A4.5,4.5 0 0,0 10,20.5A4.5,4.5 0 0,0 14.5,25A4.5,4.5 0 0,0 19,20.5A4.5,4.5 0 0,0 14.5,16M14.5,24A3.5,3.5 0 0,1 11,20.5A3.5,3.5 0 0,1 14.5,17A3.5,3.5 0 0,1 18,20.5A3.5,3.5 0 0,1 14.5,24Z";
+
+ // Adorner/Sparkle
+ public const string Sparkle = "M12,1L9,9L1,12L9,15L12,23L15,15L23,12L15,9";
+
+ // Text/AutoComplete
+ public const string TextInput = "M20,21H4V3H20V21M6,9H14V7H6V9M6,13H18V11H6V13M6,17H14V15H6V17Z";
+
+ // Border/Square
+ public const string Border = "M4,4H20V20H4V4M6,8V6H8V8H6M6,12V10H8V12H6M6,16V14H8V16H6M6,20V18H8V20H6M10,20V18H12V20H10M14,20V18H16V20H14M18,20V18H20V20H18M18,16V14H20V16H18M18,12V10H20V12H18M18,8V6H20V8H18M14,8V6H16V8H14M10,8V6H12V8H10Z";
+
+ // Lightning/Cache
+ public const string Lightning = "M11,21H7L13,3H17L14,10H20L11,21Z";
+
+ // Cursor Click/Button
+ public const string CursorClick = "M10.76,8.69A0.76,0.76 0 0,0 10,9.45V20.9C10,21.32 10.34,21.66 10.76,21.66C10.95,21.66 11.11,21.6 11.24,21.5L13.15,19.95L14.81,23.57C14.94,23.84 15.21,24 15.5,24C15.62,24 15.74,23.97 15.85,23.92L17.28,23.26C17.69,23.07 17.86,22.59 17.66,22.17L16,18.54L18.44,18.19C18.87,18.13 19.1,17.73 18.91,17.37L12.1,8.89C11.95,8.76 11.79,8.69 10.76,8.69M15,10V8H20V10H15M13.83,4.76L16.66,1.93L18.07,3.34L15.24,6.17L13.83,4.76M10,0V5H8V0H10M3.93,14.66L6.76,11.83L8.17,13.24L5.34,16.07L3.93,14.66M3.93,3.34L5.34,1.93L8.17,4.76L6.76,6.17L3.93,3.34M7,10H2V8H7V10Z";
+
+ // Spinner/ButtonSpinner
+ public const string Spinner = "M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z";
+
+ // Calendar
+ public const string Calendar = "M19,19H5V8H19M16,1V3H8V1H6V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3H18V1M9,15H7V13H9V15M13,15H11V13H13V15M17,15H15V13H17V15M9,11H7V9H9V11M13,11H11V9H13V11M17,11H15V9H17V11Z";
+
+ // Canvas/Artboard
+ public const string Canvas = "M17.5,12A1.5,1.5 0 0,1 16,10.5A1.5,1.5 0 0,1 17.5,9A1.5,1.5 0 0,1 19,10.5A1.5,1.5 0 0,1 17.5,12M14.5,8A1.5,1.5 0 0,1 13,6.5A1.5,1.5 0 0,1 14.5,5A1.5,1.5 0 0,1 16,6.5A1.5,1.5 0 0,1 14.5,8M9.5,8A1.5,1.5 0 0,1 8,6.5A1.5,1.5 0 0,1 9.5,5A1.5,1.5 0 0,1 11,6.5A1.5,1.5 0 0,1 9.5,8M6.5,12A1.5,1.5 0 0,1 5,10.5A1.5,1.5 0 0,1 6.5,9A1.5,1.5 0 0,1 8,10.5A1.5,1.5 0 0,1 6.5,12M12,3A9,9 0 0,0 3,12A9,9 0 0,0 12,21A1.5,1.5 0 0,0 13.5,19.5C13.5,19.11 13.35,18.76 13.11,18.5C12.88,18.23 12.73,17.88 12.73,17.5A1.5,1.5 0 0,1 14.23,16H16A5,5 0 0,0 21,11C21,6.58 16.97,3 12,3Z";
+
+ // Command/Terminal
+ public const string Terminal = "M20,19V7H4V19H20M20,3A2,2 0 0,1 22,5V19A2,2 0 0,1 20,21H4A2,2 0 0,1 2,19V5C2,3.89 2.9,3 4,3H20M13,17V15H18V17H13M9.58,13L5.57,9H8.4L11.7,12.3C12.09,12.69 12.09,13.33 11.7,13.72L8.42,17H5.59L9.58,13Z";
+
+ // Slides/Carousel
+ public const string Slides = "M2,6H6V18H2V6M7,6H17V18H7V6M18,6H22V18H18V6Z";
+
+ // Checkbox
+ public const string Checkbox = "M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M10,17L5,12L6.41,10.58L10,14.17L17.59,6.58L19,8";
+
+ // Clipboard
+ public const string Clipboard = "M19,3H14.82C14.4,1.84 13.3,1 12,1C10.7,1 9.6,1.84 9.18,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M12,3A1,1 0 0,1 13,4A1,1 0 0,1 12,5A1,1 0 0,1 11,4A1,1 0 0,1 12,3";
+
+ // Palette/Color
+ public const string Palette = "M12,3A9,9 0 0,0 3,12A9,9 0 0,0 12,21A1.5,1.5 0 0,0 13.5,19.5C13.5,19.11 13.35,18.76 13.11,18.5C12.88,18.23 12.73,17.88 12.73,17.5A1.5,1.5 0 0,1 14.23,16H16A5,5 0 0,0 21,11C21,6.58 16.97,3 12,3M6.5,12A1.5,1.5 0 0,1 5,10.5A1.5,1.5 0 0,1 6.5,9A1.5,1.5 0 0,1 8,10.5A1.5,1.5 0 0,1 6.5,12M9.5,8A1.5,1.5 0 0,1 8,6.5A1.5,1.5 0 0,1 9.5,5A1.5,1.5 0 0,1 11,6.5A1.5,1.5 0 0,1 9.5,8M14.5,8A1.5,1.5 0 0,1 13,6.5A1.5,1.5 0 0,1 14.5,5A1.5,1.5 0 0,1 16,6.5A1.5,1.5 0 0,1 14.5,8M17.5,12A1.5,1.5 0 0,1 16,10.5A1.5,1.5 0 0,1 17.5,9A1.5,1.5 0 0,1 19,10.5A1.5,1.5 0 0,1 17.5,12Z";
+
+ // Dropdown/ComboBox
+ public const string Dropdown = "M7,10L12,15L17,10H7Z";
+
+ // Container/Box
+ public const string Container = "M2,2H8V4H16V2H22V8H20V16H22V22H16V20H8V22H2V16H4V8H2V2M16,8V6H8V8H6V16H8V18H16V16H18V8H16M4,4V6H6V4H4M18,4V6H20V4H18M4,18V20H6V18H4M18,18V20H20V18H18Z";
+
+ // Document/Page
+ public const string Document = "M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z";
+
+ // Menu/Context
+ public const string Menu = "M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z";
+
+ // Cursor/Pointer
+ public const string Cursor = "M13.64,21.97C13.14,22.21 12.54,22 12.31,21.5L10.13,16.76L7.62,18.78C7.45,18.92 7.24,19 7,19A1,1 0 0,1 6,18V3A1,1 0 0,1 7,2C7.24,2 7.47,2.09 7.64,2.23L7.65,2.22L19.14,11.86C19.57,12.22 19.39,12.92 18.83,13.03L14.83,13.78L17,18.5C17.24,19 17,19.59 16.5,19.83L13.64,21.97Z";
+
+ // Brush/Drawing
+ public const string Brush = "M20.71,4.63L19.37,3.29C19,2.9 18.35,2.9 17.96,3.29L9,12.25L11.75,15L20.71,6.04C21.1,5.65 21.1,5 20.71,4.63M7,14A3,3 0 0,0 4,17C4,18.31 2.84,19 2,19C2.92,20.22 4.5,21 6,21A4,4 0 0,0 10,17A3,3 0 0,0 7,14Z";
+
+ // Grid/Table
+ public const string Grid = "M10,4V8H14V4H10M16,4V8H20V4H16M16,10V14H20V10H16M16,16V20H20V16H16M14,20V16H10V20H14M8,20V16H4V20H8M8,14V10H4V14H8M8,8V4H4V8H8M10,14H14V10H10V14Z";
+
+ // Shield/Validation
+ public const string Shield = "M10,17L6,13L7.41,11.59L10,14.17L16.59,7.58L18,9M12,1L3,5V11C3,16.55 6.84,21.74 12,23C17.16,21.74 21,16.55 21,11V5L12,1Z";
+
+ // Clock
+ public const string Clock = "M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M16.2,16.2L11,13V7H12.5V12.2L17,14.9L16.2,16.2Z";
+
+ // Dialog/Message
+ public const string Dialog = "M20,2H4A2,2 0 0,0 2,4V22L6,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2Z";
+
+ // DragDrop/Move
+ public const string DragDrop = "M13,6V11H18V7.75L22.25,12L18,16.25V13H13V18H16.25L12,22.25L7.75,18H11V13H6V16.25L1.75,12L6,7.75V11H11V6H7.75L12,1.75L16.25,6H13Z";
+
+ // Sidebar/Drawer
+ public const string Drawer = "M3,2H21A1,1 0 0,1 22,3V21A1,1 0 0,1 21,22H3A1,1 0 0,1 2,21V3A1,1 0 0,1 3,2M10,4V20H20V4H10M4,4V20H8V4H4Z";
+
+ // Expand
+ public const string Expand = "M10,21V19H6.41L10.91,14.5L9.5,13.09L5,17.59V14H3V21H10M14.5,10.91L19,6.41V10H21V3H14V5H17.59L13.09,9.5L14.5,10.91Z";
+
+ // Flyout/Popup
+ public const string Flyout = "M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M7,9H10V7H7V9M7,13H17V11H7V13M7,17H14V15H7V17Z";
+
+ // Target/Focus
+ public const string Target = "M12,8A4,4 0 0,0 8,12A4,4 0 0,0 12,16A4,4 0 0,0 16,12A4,4 0 0,0 12,8M3.05,13H1V11H3.05C3.5,6.83 6.83,3.5 11,3.05V1H13V3.05C17.17,3.5 20.5,6.83 20.95,11H23V13H20.95C20.5,17.17 17.17,20.5 13,20.95V23H11V20.95C6.83,20.5 3.5,17.17 3.05,13M12,5A7,7 0 0,0 5,12A7,7 0 0,0 12,19A7,7 0 0,0 19,12A7,7 0 0,0 12,5Z";
+
+ // Gesture/Touch
+ public const string Gesture = "M5,15A2,2 0 0,0 3,17V19H7V17A2,2 0 0,0 5,15M5,11A6,6 0 0,0 -1,17H1A4,4 0 0,1 5,13V11M5,7C0.03,7 -4,11.03 -4,16V17H-2V16C-2,12.13 1.13,9 5,9V7M20.5,9.5L12,18L8.5,14.5L7.08,15.91L12,20.84L21.92,10.91L20.5,9.5Z";
+
+ // Image/Photo
+ public const string Image = "M8.5,13.5L11,16.5L14.5,12L19,18H5M21,19V5C21,3.89 20.1,3 19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19Z";
+
+ // Tag/Label
+ public const string Tag = "M5.5,7A1.5,1.5 0 0,1 4,5.5A1.5,1.5 0 0,1 5.5,4A1.5,1.5 0 0,1 7,5.5A1.5,1.5 0 0,1 5.5,7M21.41,11.58L12.41,2.58C12.05,2.22 11.55,2 11,2H4C2.89,2 2,2.89 2,4V11C2,11.55 2.22,12.05 2.59,12.41L11.58,21.41C11.95,21.77 12.45,22 13,22C13.55,22 14.05,21.77 14.41,21.41L21.41,14.41C21.78,14.05 22,13.55 22,13C22,12.45 21.77,11.95 21.41,11.58Z";
+
+ // Transform/Rotate
+ public const string Transform = "M16.89,15.5L18.31,16.89C19.21,15.73 19.76,14.39 19.93,13H17.91C17.77,13.87 17.43,14.72 16.89,15.5M13,17.9V19.92C14.39,19.75 15.74,19.21 16.9,18.31L15.46,16.87C14.71,17.41 13.87,17.76 13,17.9M19.93,11C19.76,9.61 19.21,8.27 18.31,7.11L16.89,8.53C17.43,9.28 17.77,10.13 17.91,11M15.55,5.55L11,1V5.07C7.06,5.56 4,8.92 4,12.95C4,17.18 7.32,20.56 11.46,20.93C11.64,20.95 11.82,20.97 12,20.97C12.18,20.97 12.36,20.95 12.54,20.93C12.69,20.92 12.83,20.89 13,20.87V18.9C12.82,18.92 12.65,18.95 12.47,18.95C12.32,18.96 12.16,18.97 12,18.97C8.42,18.97 5.53,16.12 5.53,12.58C5.53,9.36 7.88,6.67 11,6.15V10L15.55,5.55Z";
+
+ // List
+ public const string List = "M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z";
+
+ // Bell/Notification
+ public const string Bell = "M21,19V20H3V19L5,17V11C5,7.9 7.03,5.17 10,4.29C10,4.19 10,4.1 10,4A2,2 0 0,1 12,2A2,2 0 0,1 14,4C14,4.1 14,4.19 14,4.29C16.97,5.17 19,7.9 19,11V17L21,19M14,21A2,2 0 0,1 12,23A2,2 0 0,1 10,21";
+
+ // Number/Hash
+ public const string Number = "M5.41,21L6.12,17H2.12L2.47,15H6.47L7.53,9H3.53L3.88,7H7.88L8.59,3H10.59L9.88,7H15.88L16.59,3H18.59L17.88,7H21.88L21.53,9H17.53L16.47,15H20.47L20.12,17H16.12L15.41,21H13.41L14.12,17H8.12L7.41,21H5.41M9.47,9L8.41,15H14.41L15.47,9H9.47Z";
+
+ // Cube/3D
+ public const string Cube = "M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L5,8.09V15.91L12,19.85L19,15.91V8.09L12,4.15Z";
+
+ // Dots/Pagination
+ public const string Dots = "M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z";
+
+ // Info/Information
+ public const string Info = "M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z";
+
+ // Progress/Loading
+ public const string Progress = "M13,2.03V2.05L13,4.05C17.39,4.59 20.5,8.58 19.96,12.97C19.5,16.61 16.64,19.5 13,19.93V21.93C18.5,21.38 22.5,16.5 21.95,11C21.5,6.25 17.73,2.5 13,2.03M11,2.06C9.05,2.25 7.19,3 5.67,4.26L7.1,5.74C8.22,4.84 9.57,4.26 11,4.06V2.06M4.26,5.67C3,7.19 2.25,9.04 2.05,11H4.05C4.24,9.58 4.8,8.23 5.69,7.1L4.26,5.67M2.06,13C2.26,14.96 3.03,16.81 4.27,18.33L5.69,16.9C4.81,15.77 4.24,14.42 4.06,13H2.06M7.1,18.37L5.67,19.74C7.18,21 9.04,21.79 11,22V20C9.58,19.82 8.23,19.25 7.1,18.37Z";
+
+ // Radio
+ public const string Radio = "M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,7A5,5 0 0,0 7,12A5,5 0 0,0 12,17A5,5 0 0,0 17,12A5,5 0 0,0 12,7Z";
+
+ // Refresh
+ public const string Refresh = "M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z";
+
+ // Layout/Grid
+ public const string Layout = "M19,11H13V5H19M19,19H13V13H19M11,11H5V5H11M11,19H5V13H11M3,3V21H21V3";
+
+ // Scroll
+ public const string Scroll = "M15,20H9V12H4.16L12,4.16L19.84,12H15V20Z";
+
+ // Slider/Tune
+ public const string Slider = "M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z";
+
+ // Split/SplitView
+ public const string Split = "M18,4H6C4.89,4 4,4.89 4,6V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18V6C20,4.89 19.1,4 18,4M18,6V18H13V6H18Z";
+
+ // Tab
+ public const string Tab = "M21,3H3C1.89,3 1,3.89 1,5V19A2,2 0 0,0 3,21H21C22.1,21 23,20.1 23,19V5C23,3.89 22.1,3 21,3M21,19H3V5H13V9H21V19Z";
+
+ // Theme/Palette
+ public const string Theme = "M12,18V6A6,6 0 0,1 18,12A6,6 0 0,1 12,18M20,15.31L23.31,12L20,8.69V4H15.31L12,0.69L8.69,4H4V8.69L0.69,12L4,15.31V20H8.69L12,23.31L15.31,20H20V15.31Z";
+
+ // Toggle/Switch
+ public const string Toggle = "M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z";
+
+ // Tooltip/Info bubble
+ public const string Tooltip = "M4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4A2,2 0 0,1 4,2M11,15H13V13H11V15M11,11H13V5H11V11Z";
+
+ // Animation/Transition
+ public const string Transition = "M2,5.27L3.28,4L20,20.72L18.73,22L12.73,16H7V19L3,15L7,11V14H9.73L5,9.27V14H3V5.27M21,9L17,5V8H14V10H17V13L21,9M17,17V14H15V17H17Z";
+
+ // Tree/Hierarchy
+ public const string Tree = "M15,20A1,1 0 0,0 16,19V4H8V19A1,1 0 0,0 9,20H2V22H22V20H15Z";
+
+ // Viewbox/Resize
+ public const string Viewbox = "M5,15H3V19A2,2 0 0,0 5,21H9V19H5M5,5H9V3H5A2,2 0 0,0 3,5V9H5M19,3H15V5H19V9H21V5A2,2 0 0,0 19,3M19,19H15V21H19A2,2 0 0,0 21,19V15H19";
+
+ // Puzzle/Embed
+ public const string Puzzle = "M20.5,11H19V7C19,5.89 18.1,5 17,5H13V3.5A2.5,2.5 0 0,0 10.5,1A2.5,2.5 0 0,0 8,3.5V5H4A2,2 0 0,0 2,7V10.8H3.5C5.04,10.8 6.2,11.96 6.2,13.5C6.2,15.04 5.04,16.2 3.5,16.2H2V20A2,2 0 0,0 4,22H7.8V20.5C7.8,18.96 8.96,17.8 10.5,17.8C12.04,17.8 13.2,18.96 13.2,20.5V22H17A2,2 0 0,0 19,20V16H20.5A2.5,2.5 0 0,0 23,13.5A2.5,2.5 0 0,0 20.5,11Z";
+
+ // Window/Application
+ public const string Window = "M4,4H20V20H4V4M6,8V18H18V8H6Z";
+
+ // Header/Title
+ public const string Header = "M3,4H5V10H9V4H11V18H9V12H5V18H3V4M13,8H15.31L15.63,5H17.63L17.31,8H19.31L19.63,5H21.63L21.31,8H23V10H21.1L20.9,12H23V14H20.69L20.37,17H18.37L18.69,14H16.69L16.37,17H14.37L14.69,14H13V12H14.9L15.1,10H13V8M17.1,10L16.9,12H18.9L19.1,10H17.1Z";
+
+ // Monitor/Screen
+ public const string Monitor = "M21,16H3V4H21M21,2H3C1.89,2 1,2.89 1,4V16A2,2 0 0,0 3,18H10V20H8V22H16V20H14V18H21A2,2 0 0,0 23,16V4C23,2.89 22.1,2 21,2Z";
+
+ // Navigation/Compass
+ public const string Navigation = "M12,2L4.5,20.29L5.21,21L12,18L18.79,21L19.5,20.29L12,2Z";
+
+ // OpenGL/3D Box (same as Cube but outline variant)
+ public const string Cube3D = "M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L6.04,7.5L12,10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z";
+
+ // Horizontal dots/Pager
+ public const string HorizontalDots = "M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z";
+
+ // Slider tune/adjust
+ public const string Tune = "M3,17V19H9V17H3M3,5V7H13V5H3M13,21V19H21V17H13V15H11V21H13M7,9V11H3V13H7V15H9V9H7M21,13V11H11V13H21M15,9H17V7H21V5H17V3H15V9Z";
+ }
+}
diff --git a/samples/AvaloniaControlCatalog/MainView.xaml b/samples/AvaloniaControlCatalog/MainView.xaml
new file mode 100644
index 000000000..f7b2658b0
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/MainView.xaml
@@ -0,0 +1,161 @@
+
+
+
+
+
+
+ 20
+ 10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ None
+ BorderOnly
+ Full
+
+
+
+
+ Default
+ Light
+ Dark
+
+
+
+
+ Fluent
+ Simple
+
+
+
+
+ None
+ Transparent
+ Blur
+ AcrylicBlur
+ Mica
+
+
+
+
+ LeftToRight
+ RightToLeft
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/AvaloniaControlCatalog/MainView.xaml.cs b/samples/AvaloniaControlCatalog/MainView.xaml.cs
new file mode 100644
index 000000000..d4a45a127
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/MainView.xaml.cs
@@ -0,0 +1,203 @@
+using System;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Media.Immutable;
+using Avalonia.Styling;
+using ControlCatalog.Models;
+using ControlCatalog.ViewModels;
+
+namespace ControlCatalog
+{
+ public partial class MainView : DrawerPage
+ {
+ private readonly TransparentStyles _transparentStyles = new();
+
+ public MainView()
+ {
+ InitializeComponent();
+
+ Loaded += MainView_Loaded;
+ Unloaded += MainView_Unloaded;
+ }
+
+ private const double WideBreakpoint = 1008;
+ private const double NarrowBreakpoint = 640;
+
+ protected override Type StyleKeyOverride => typeof(MainView);
+
+ private void MainView_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
+ {
+ if (DataContext == null)
+ return;
+
+ SizeChanged += OnDrawerSizeChanged;
+ UpdateAdaptiveLayout();
+ }
+
+ private void MainView_Unloaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
+ {
+ SizeChanged -= OnDrawerSizeChanged;
+ _lastAppliedMode = null;
+ }
+
+ private SplitViewDisplayMode? _lastAppliedMode;
+ private bool _updatingLayout;
+
+ private void OnDrawerSizeChanged(object? sender, SizeChangedEventArgs e)
+ {
+ if (e.WidthChanged)
+ UpdateAdaptiveLayout();
+ }
+
+ private void UpdateAdaptiveLayout()
+ {
+ if (_updatingLayout || DataContext == null)
+ return;
+
+ var width = Bounds.Width;
+ if (width <= 0)
+ return;
+
+ SplitViewDisplayMode targetMode;
+ if (width >= WideBreakpoint)
+ targetMode = SplitViewDisplayMode.Inline;
+ else if (width >= NarrowBreakpoint)
+ targetMode = SplitViewDisplayMode.CompactInline;
+ else
+ targetMode = SplitViewDisplayMode.Overlay;
+
+ if (_lastAppliedMode == targetMode)
+ return;
+
+ _updatingLayout = true;
+ try
+ {
+ _lastAppliedMode = targetMode;
+ ViewModel.DisplayMode = targetMode;
+
+ if (targetMode == SplitViewDisplayMode.Inline)
+ ViewModel.IsDrawerOpened = true;
+ else if (targetMode == SplitViewDisplayMode.Overlay)
+ ViewModel.IsDrawerOpened = false;
+ }
+ finally
+ {
+ _updatingLayout = false;
+ }
+ }
+
+ private void Themes_SelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (e.AddedItems.Count > 0 && e.AddedItems[0] is CatalogTheme theme)
+ {
+ App.SetCatalogThemes(theme);
+ }
+ }
+
+ private void ThemeVariants_SelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (Application.Current is { } app && e.AddedItems.Count > 0 && e.AddedItems[0] is ThemeVariant themeVariant)
+ {
+ app.RequestedThemeVariant = themeVariant;
+ }
+ }
+
+ private void FlowDirection_SelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (TopLevel.GetTopLevel(this) is { } topLevel && e.AddedItems.Count > 0 && e.AddedItems[0] is FlowDirection flowDirection)
+ {
+ topLevel.FlowDirection = flowDirection;
+ }
+ }
+
+ private void Decorations_SelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (TopLevel.GetTopLevel(this) is Window window && e.AddedItems.Count > 0 && e.AddedItems[0] is WindowDecorations systemDecorations)
+ {
+ window.WindowDecorations = systemDecorations;
+ }
+ }
+
+ private void TransparencyLevels_SelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (TopLevel.GetTopLevel(this) is { } topLevel && e.AddedItems.Count > 0 && e.AddedItems[0] is WindowTransparencyLevel transparencyLevel)
+ {
+ topLevel.TransparencyLevelHint = [transparencyLevel];
+
+ if (topLevel.ActualTransparencyLevel != WindowTransparencyLevel.None &&
+ transparencyLevel != WindowTransparencyLevel.None)
+ {
+ topLevel.Background = new ImmutableSolidColorBrush(Colors.Gray, 0.2);
+ if (!topLevel.Styles.Contains(_transparentStyles))
+ topLevel.Styles.Add(_transparentStyles);
+ }
+ else
+ {
+ topLevel.Background = null;
+ topLevel.Styles.Remove(_transparentStyles);
+ }
+ }
+ }
+
+ protected override void OnDataContextChanged(EventArgs e)
+ {
+ base.OnDataContextChanged(e);
+
+ if (ViewModel != null)
+ {
+ ViewModel.Navigator = NavPage;
+ }
+ }
+
+ internal MainWindowViewModel ViewModel => (MainWindowViewModel)DataContext!;
+
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+
+ if (DataContext == null)
+ return;
+
+ UpdateAdaptiveLayout();
+
+ var topLevel = TopLevel.GetTopLevel(this)!;
+ if (topLevel is Window window)
+ ViewModel.SelectedDecorationIndex = (int)window.WindowDecorations;
+
+ var insets = topLevel.InsetsManager;
+ if (insets != null)
+ {
+ // In real life application these events should be unsubscribed to avoid memory leaks.
+ ViewModel.SafeAreaPadding = insets.SafeAreaPadding;
+ insets.SafeAreaChanged += (sender, args) =>
+ {
+ ViewModel.SafeAreaPadding = insets.SafeAreaPadding;
+ };
+
+ ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdgePreference;
+ ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true;
+
+ ViewModel.PropertyChanged += async (sender, args) =>
+ {
+ if (args.PropertyName == nameof(ViewModel.DisplayEdgeToEdge))
+ {
+ insets.DisplayEdgeToEdgePreference = ViewModel.DisplayEdgeToEdge;
+ }
+ else if (args.PropertyName == nameof(ViewModel.IsSystemBarVisible))
+ {
+ insets.IsSystemBarVisible = ViewModel.IsSystemBarVisible;
+ }
+
+ // Give the OS some time to apply new values and refresh the view model.
+ await Task.Delay(100);
+ ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdgePreference;
+ ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true;
+ };
+ }
+
+ ViewModel.SelectedPageIndex = 0;
+ }
+ }
+}
diff --git a/samples/AvaloniaControlCatalog/MainWindow.xaml b/samples/AvaloniaControlCatalog/MainWindow.xaml
new file mode 100644
index 000000000..308b29c57
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/MainWindow.xaml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/AvaloniaControlCatalog/MainWindow.xaml.cs b/samples/AvaloniaControlCatalog/MainWindow.xaml.cs
new file mode 100644
index 000000000..ca040df56
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/MainWindow.xaml.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Runtime.InteropServices;
+using Avalonia.Controls;
+using Avalonia.Input;
+
+namespace ControlCatalog
+{
+ public partial class MainWindow : Window
+ {
+ private NativeMenu? _recentMenu;
+
+ public MainWindow()
+ {
+ InitializeComponent();
+
+ _recentMenu = ((NativeMenu.GetMenu(this)?.Items[0] as NativeMenuItem)?.Menu?.Items[2] as NativeMenuItem)?.Menu;
+ }
+
+ public static string MenuQuitHeader => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Quit Avalonia" : "E_xit";
+
+ public static KeyGesture MenuQuitGesture => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ?
+ new KeyGesture(Key.Q, KeyModifiers.Meta) :
+ new KeyGesture(Key.F4, KeyModifiers.Alt);
+
+ public void OnOpenClicked(object sender, EventArgs args)
+ {
+ _recentMenu?.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1)));
+ }
+
+ public void OnCloseClicked(object sender, EventArgs args)
+ {
+ Close();
+ }
+ }
+}
diff --git a/samples/AvaloniaControlCatalog/MiniMvvm/MiniCommand.cs b/samples/AvaloniaControlCatalog/MiniMvvm/MiniCommand.cs
new file mode 100644
index 000000000..57f6d6b4d
--- /dev/null
+++ b/samples/AvaloniaControlCatalog/MiniMvvm/MiniCommand.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Threading.Tasks;
+using System.Windows.Input;
+
+namespace MiniMvvm
+{
+ public sealed class MiniCommand : MiniCommand, ICommand
+ {
+ private readonly Action? _cb;
+ private readonly Func? _acb;
+ private bool _busy;
+
+ public MiniCommand(Action cb)
+ {
+ _cb = cb;
+ }
+
+ public MiniCommand(Func cb)
+ {
+ _acb = cb;
+ }
+
+ private bool Busy
+ {
+ get => _busy;
+ set
+ {
+ _busy = value;
+ CanExecuteChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+
+ public override event EventHandler? CanExecuteChanged;
+
+ public override bool CanExecute(object? parameter) => !_busy;
+
+ public override async void Execute(object? parameter)
+ {
+ if(Busy)
+ return;
+ try
+ {
+ Busy = true;
+ if (_cb != null)
+ _cb((T)parameter!);
+ else
+ await _acb!((T)parameter!);
+ }
+ finally
+ {
+ Busy = false;
+ }
+ }
+ }
+
+ public abstract class MiniCommand : ICommand
+ {
+ public static MiniCommand Create(Action cb) => new MiniCommand