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(_ => cb()); + public static MiniCommand Create(Action cb) => new MiniCommand(cb); + public static MiniCommand CreateFromTask(Func cb) => new MiniCommand(_ => cb()); + + public abstract bool CanExecute(object? parameter); + public abstract void Execute(object? parameter); + public abstract event EventHandler? CanExecuteChanged; + } +} diff --git a/samples/AvaloniaControlCatalog/MiniMvvm/PropertyChangedExtensions.cs b/samples/AvaloniaControlCatalog/MiniMvvm/PropertyChangedExtensions.cs new file mode 100644 index 000000000..cc551d61d --- /dev/null +++ b/samples/AvaloniaControlCatalog/MiniMvvm/PropertyChangedExtensions.cs @@ -0,0 +1,110 @@ +using System; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; +using Avalonia.Reactive; + +namespace MiniMvvm +{ + public static class PropertyChangedExtensions + { + class PropertyObservable : IObservable + { + private readonly INotifyPropertyChanged _target; + private readonly PropertyInfo _info; + + public PropertyObservable(INotifyPropertyChanged target, PropertyInfo info) + { + _target = target; + _info = info; + } + + class Subscription : IDisposable + { + private readonly INotifyPropertyChanged _target; + private readonly PropertyInfo _info; + private readonly IObserver _observer; + + public Subscription(INotifyPropertyChanged target, PropertyInfo info, IObserver observer) + { + _target = target; + _info = info; + _observer = observer; + _target.PropertyChanged += OnPropertyChanged; + _observer.OnNext((T)_info.GetValue(_target)!); + } + + private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == _info.Name) + _observer.OnNext((T)_info.GetValue(_target)!); + } + + public void Dispose() + { + _target.PropertyChanged -= OnPropertyChanged; + _observer.OnCompleted(); + } + } + + public IDisposable Subscribe(IObserver observer) + { + return new Subscription(_target, _info, observer); + } + } + + public static IObservable WhenAnyValue(this TModel model, + Expression> expr) where TModel : INotifyPropertyChanged + { + var l = (LambdaExpression)expr; + var ma = (MemberExpression)l.Body; + var prop = (PropertyInfo)ma.Member; + return new PropertyObservable(model, prop); + } + + public static IObservable WhenAnyValue(this TModel model, + Expression> v1, + Func cb + ) where TModel : INotifyPropertyChanged + { + return model.WhenAnyValue(v1).Select(cb); + } + + public static IObservable WhenAnyValue(this TModel model, + Expression> v1, + Expression> v2, + Func cb + ) where TModel : INotifyPropertyChanged => + Observable.CombineLatest( + model.WhenAnyValue(v1), + model.WhenAnyValue(v2), + cb); + + public static IObservable> WhenAnyValue(this TModel model, + Expression> v1, + Expression> v2 + ) where TModel : INotifyPropertyChanged => + model.WhenAnyValue(v1, v2, (a1, a2) => (a1, a2)); + + public static IObservable WhenAnyValue(this TModel model, + Expression> v1, + Expression> v2, + Expression> v3, + Func cb + ) where TModel : INotifyPropertyChanged => + model.WhenAnyValue(v1) + .CombineLatest( + model.WhenAnyValue(v2), + (l, r) => (l, r)) + .CombineLatest( + model.WhenAnyValue(v3), + (t, r) => cb(t.l, t.r, r)); + + public static IObservable> WhenAnyValue(this TModel model, + Expression> v1, + Expression> v2, + Expression> v3 + ) where TModel : INotifyPropertyChanged => + model.WhenAnyValue(v1, v2, v3, (a1, a2, a3) => (a1, a2, a3)); + } +} diff --git a/samples/AvaloniaControlCatalog/MiniMvvm/ViewModelBase.cs b/samples/AvaloniaControlCatalog/MiniMvvm/ViewModelBase.cs new file mode 100644 index 000000000..1b2f3b2c1 --- /dev/null +++ b/samples/AvaloniaControlCatalog/MiniMvvm/ViewModelBase.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace MiniMvvm +{ + public class ViewModelBase : INotifyPropertyChanged + { + public event PropertyChangedEventHandler? PropertyChanged; + + protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName] string? propertyName = null) + { + if (!EqualityComparer.Default.Equals(field, value)) + { + field = value; + RaisePropertyChanged(propertyName); + return true; + } + return false; + } + + protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} diff --git a/samples/AvaloniaControlCatalog/Models/CatalogTheme.cs b/samples/AvaloniaControlCatalog/Models/CatalogTheme.cs new file mode 100644 index 000000000..79b3182d2 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Models/CatalogTheme.cs @@ -0,0 +1,8 @@ +namespace ControlCatalog.Models +{ + public enum CatalogTheme + { + Fluent, + Simple + } +} diff --git a/samples/AvaloniaControlCatalog/Models/Person.cs b/samples/AvaloniaControlCatalog/Models/Person.cs new file mode 100644 index 000000000..99bc50250 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Models/Person.cs @@ -0,0 +1,125 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using Avalonia.Media; + +namespace ControlCatalog.Models +{ + public class Person : INotifyDataErrorInfo, INotifyPropertyChanged + { + string _firstName = string.Empty; + string _lastName = string.Empty; + bool _isBanned; + private int _age; + + public string FirstName + { + get => _firstName; + set + { + _firstName = value; + if (string.IsNullOrWhiteSpace(value)) + SetError(nameof(FirstName), "First Name Required"); + else + SetError(nameof(FirstName), null); + + OnPropertyChanged(nameof(FirstName)); + } + + } + + public string LastName + { + get => _lastName; + set + { + _lastName = value; + if (string.IsNullOrWhiteSpace(value)) + SetError(nameof(LastName), "Last Name Required"); + else + SetError(nameof(LastName), null); + + OnPropertyChanged(nameof(LastName)); + } + } + + public bool IsBanned + { + get => _isBanned; + set + { + _isBanned = value; + + OnPropertyChanged(nameof(_isBanned)); + } + } + + + /// + /// Gets or sets the age of the person + /// + public int Age + { + get => _age; + set + { + _age = value; + OnPropertyChanged(nameof(Age)); + } + } + + Dictionary> _errorLookup = new Dictionary>(); + + void SetError(string propertyName, string? error) + { + if (string.IsNullOrEmpty(error)) + { + if (_errorLookup.Remove(propertyName)) + OnErrorsChanged(propertyName); + } + else + { + if (_errorLookup.TryGetValue(propertyName, out var errorList)) + { + errorList.Clear(); + errorList.Add(error!); + } + else + { + var errors = new List { error! }; + _errorLookup.Add(propertyName, errors); + } + + OnErrorsChanged(propertyName); + } + } + + public bool HasErrors => _errorLookup.Count > 0; + + public event EventHandler? ErrorsChanged; + public event PropertyChangedEventHandler? PropertyChanged; + + void OnErrorsChanged(string propertyName) + { + ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); + } + void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public IEnumerable GetErrors(string? propertyName) + { + if (propertyName is { } && _errorLookup.TryGetValue(propertyName, out var errorList)) + return errorList; + else + return Array.Empty(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Models/StateData.cs b/samples/AvaloniaControlCatalog/Models/StateData.cs new file mode 100644 index 000000000..a7ea655cd --- /dev/null +++ b/samples/AvaloniaControlCatalog/Models/StateData.cs @@ -0,0 +1,20 @@ +namespace ControlCatalog.Models; + +public class StateData +{ + public string Name { get; private set; } + public string Abbreviation { get; private set; } + public string Capital { get; private set; } + + public StateData(string name, string abbreviation, string capital) + { + Name = name; + Abbreviation = abbreviation; + Capital = capital; + } + + public override string ToString() + { + return Name; + } +} diff --git a/samples/AvaloniaControlCatalog/NavHeaderItem.xaml b/samples/AvaloniaControlCatalog/NavHeaderItem.xaml new file mode 100644 index 000000000..6867661eb --- /dev/null +++ b/samples/AvaloniaControlCatalog/NavHeaderItem.xaml @@ -0,0 +1,101 @@ + + + + + + #99FFFFFF + #FF1F1F1F + #FF000000 + #FF171717 + + + #99000000 + #FFE6E6E6 + #FFFFFFFF + #FFF2F2F2 + + + 36 + 1 1 1 1 #2000, 0 0 1 1 #2fff + + + + Disabled + + Test + + Test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/NuGet.Config b/samples/AvaloniaControlCatalog/NuGet.Config new file mode 100644 index 000000000..320c436e0 --- /dev/null +++ b/samples/AvaloniaControlCatalog/NuGet.Config @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/AcceleratorPage.xaml b/samples/AvaloniaControlCatalog/Pages/AcceleratorPage.xaml new file mode 100644 index 000000000..62fcf0d74 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/AcceleratorPage.xaml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Accelerator Support + + + + + This is tab 1 content + + This is tab 1 content + + This is tab 1 content + + + + + This is tab 2 content + + + + + + This is tab 4 content + + + This is fab 5 content + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/AcceleratorPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/AcceleratorPage.xaml.cs new file mode 100644 index 000000000..40afacc05 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/AcceleratorPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class AcceleratorPage : ContentPage + { + public AcceleratorPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/AcrylicPage.xaml b/samples/AvaloniaControlCatalog/Pages/AcrylicPage.xaml new file mode 100644 index 000000000..4a68fbfaf --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/AcrylicPage.xaml @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/AcrylicPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/AcrylicPage.xaml.cs new file mode 100644 index 000000000..adb5b0570 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/AcrylicPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class AcrylicPage : ContentPage + { + public AcrylicPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/AdornerLayerPage.xaml b/samples/AvaloniaControlCatalog/Pages/AdornerLayerPage.xaml new file mode 100644 index 000000000..35907c7ef --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/AdornerLayerPage.xaml @@ -0,0 +1,76 @@ + + + + Rotation + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/AdornerLayerPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/AdornerLayerPage.xaml.cs new file mode 100644 index 000000000..5ee8d4447 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/AdornerLayerPage.xaml.cs @@ -0,0 +1,34 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class AdornerLayerPage : ContentPage + { + private Control? _adorner; + + public AdornerLayerPage() + { + InitializeComponent(); + } + + private void RemoveAdorner_OnClick(object? sender, RoutedEventArgs e) + { + var adorner = AdornerLayer.GetAdorner(AdornerButton); + if (adorner is { }) + { + _adorner = adorner; + } + AdornerLayer.SetAdorner(AdornerButton, null); + } + + private void AddAdorner_OnClick(object? sender, RoutedEventArgs e) + { + if (_adorner is { }) + { + AdornerLayer.SetAdorner(AdornerButton, _adorner); + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/AvaloniaControlCatalog/Pages/AutoCompleteBoxPage.xaml new file mode 100644 index 000000000..4edb00b7c --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/AutoCompleteBoxPage.xaml @@ -0,0 +1,83 @@ + + + + A control into which the user can input text + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs new file mode 100644 index 000000000..1624fc563 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.LogicalTree; +using ControlCatalog.Models; + +namespace ControlCatalog.Pages +{ + public partial class AutoCompleteBoxPage : ContentPage + { + private static StateData[] BuildAllStates() + { + return new StateData[] + { + new StateData("Alabama","AL","Montgomery"), + new StateData("Alaska","AK","Juneau"), + new StateData("Arizona","AZ","Phoenix"), + new StateData("Arkansas","AR","Little Rock"), + new StateData("California","CA","Sacramento"), + new StateData("Colorado","CO","Denver"), + new StateData("Connecticut","CT","Hartford"), + new StateData("Delaware","DE","Dover"), + new StateData("Florida","FL","Tallahassee"), + new StateData("Georgia","GA","Atlanta"), + new StateData("Hawaii","HI","Honolulu"), + new StateData("Idaho","ID","Boise"), + new StateData("Illinois","IL","Springfield"), + new StateData("Indiana","IN","Indianapolis"), + new StateData("Iowa","IA","Des Moines"), + new StateData("Kansas","KS","Topeka"), + new StateData("Kentucky","KY","Frankfort"), + new StateData("Louisiana","LA","Baton Rouge"), + new StateData("Maine","ME","Augusta"), + new StateData("Maryland","MD","Annapolis"), + new StateData("Massachusetts","MA","Boston"), + new StateData("Michigan","MI","Lansing"), + new StateData("Minnesota","MN","St. Paul"), + new StateData("Mississippi","MS","Jackson"), + new StateData("Missouri","MO","Jefferson City"), + new StateData("Montana","MT","Helena"), + new StateData("Nebraska","NE","Lincoln"), + new StateData("Nevada","NV","Carson City"), + new StateData("New Hampshire","NH","Concord"), + new StateData("New Jersey","NJ","Trenton"), + new StateData("New Mexico","NM","Santa Fe"), + new StateData("New York","NY","Albany"), + new StateData("North Carolina","NC","Raleigh"), + new StateData("North Dakota","ND","Bismarck"), + new StateData("Ohio","OH","Columbus"), + new StateData("Oklahoma","OK","Oklahoma City"), + new StateData("Oregon","OR","Salem"), + new StateData("Pennsylvania","PA","Harrisburg"), + new StateData("Rhode Island","RI","Providence"), + new StateData("South Carolina","SC","Columbia"), + new StateData("South Dakota","SD","Pierre"), + new StateData("Tennessee","TN","Nashville"), + new StateData("Texas","TX","Austin"), + new StateData("Utah","UT","Salt Lake City"), + new StateData("Vermont","VT","Montpelier"), + new StateData("Virginia","VA","Richmond"), + new StateData("Washington","WA","Olympia"), + new StateData("West Virginia","WV","Charleston"), + new StateData("Wisconsin","WI","Madison"), + new StateData("Wyoming","WY","Cheyenne"), + }; + } + public StateData[] States { get; private set; } + + private static LinkedList[] BuildAllSentences() + { + return new string[] + { + "Hello world", + "No this is Patrick", + "Never gonna give you up", + "How does one patch KDE2 under FreeBSD" + } + .Select(x => new LinkedList(x.Split(' '))) + .ToArray(); + } + public LinkedList[] Sentences { get; private set; } + + public AutoCompleteBoxPage() + { + InitializeComponent(); + + States = BuildAllStates(); + Sentences = BuildAllSentences(); + + foreach (AutoCompleteBox box in GetAllAutoCompleteBox().Where(x => x.Name != "CustomAutocompleteBox")) + { + box.ItemsSource = States; + } + + var converter = new FuncMultiValueConverter(parts => + { + return String.Format("{0} ({1})", parts.ToArray()); + }); + var binding = new MultiBinding { Converter = converter }; + binding.Bindings.Add(CompiledBinding.Create(s => s.Name)); + binding.Bindings.Add(CompiledBinding.Create(s => s.Abbreviation)); + + MultiBindingBox.ValueMemberBinding = binding; + + AsyncBox.AsyncPopulator = PopulateAsync; + + CustomAutocompleteBox.ItemsSource = Sentences.SelectMany(x => x); + CustomAutocompleteBox.TextFilter = LastWordContains; + CustomAutocompleteBox.TextSelector = AppendWord; + } + private IEnumerable GetAllAutoCompleteBox() + { + return + this.GetLogicalDescendants() + .OfType(); + } + + private static bool StringContains(string str, string? query) + { + if (query == null) + return false; + return str.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0; + } + private async Task> PopulateAsync(string? searchText, CancellationToken cancellationToken) + { + await Task.Delay(TimeSpan.FromSeconds(1.5), cancellationToken); + + return + States.Where(data => StringContains(data.Name, searchText) || StringContains(data.Capital, searchText)) + .ToList(); + } + + private bool LastWordContains(string? searchText, string? item) + { + var words = searchText?.Split(' ') ?? Array.Empty(); + var options = Sentences.Select(x => x.First) + .ToArray?>(); + for (var i = 0; i < words.Length; ++i) + { + var word = words[i]; + for (var j = 0; word is { } && j < options.Length; ++j) + { + if (options[i] is { } option) + { + if (i == words.Length - 1) + { + options[j] = option.Value.ToLower().Contains(word.ToLower()) ? option : null; + } + else + { + options[j] = option.Value.Equals(word, StringComparison.InvariantCultureIgnoreCase) ? option.Next : null; + } + } + } + } + + return options.Any(x => x != null && x.Value == item); + } + private string AppendWord(string? text, string? item) + { + if (item is { }) + { + string[] parts = text?.Split(' ') ?? Array.Empty(); + if (parts.Length == 0) + return item; + + parts[parts.Length - 1] = item; + return string.Join(" ", parts); + } + return string.Empty; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/BitmapCachePage.axaml b/samples/AvaloniaControlCatalog/Pages/BitmapCachePage.axaml new file mode 100644 index 000000000..4298f29e8 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/BitmapCachePage.axaml @@ -0,0 +1,49 @@ + + + + + + + + + + Render at scale + + Scale + + Enable clear type + + Snap to device pixels + Subpixel offset X + + + + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/BitmapCachePage.axaml.cs b/samples/AvaloniaControlCatalog/Pages/BitmapCachePage.axaml.cs new file mode 100644 index 000000000..be7cf7a7e --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/BitmapCachePage.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages; + +public partial class BitmapCachePage : ContentPage +{ + public BitmapCachePage() + { + InitializeComponent(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/BorderPage.xaml b/samples/AvaloniaControlCatalog/Pages/BorderPage.xaml new file mode 100644 index 000000000..297e3fcfb --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/BorderPage.xaml @@ -0,0 +1,65 @@ + + + + + + + A control which decorates a child with a border and background + + + + Border + + + Background And CenterBorder + + + Background And InnerBorderEdge + + + Background And OuterBorderEdge + + + Rounded Corners + + + Rounded Corners + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/BorderPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/BorderPage.xaml.cs new file mode 100644 index 000000000..71aebf24f --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/BorderPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class BorderPage : ContentPage + { + public BorderPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/ButtonSpinnerPage.xaml b/samples/AvaloniaControlCatalog/Pages/ButtonSpinnerPage.xaml new file mode 100644 index 000000000..c21a772c2 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ButtonSpinnerPage.xaml @@ -0,0 +1,34 @@ + + + + The ButtonSpinner control allows you to add button spinners to any element and then respond to the Spin event to manipulate that element. + + + AllowSpin + ShowButtonSpinner + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/ButtonSpinnerPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/ButtonSpinnerPage.xaml.cs new file mode 100644 index 000000000..28f4b2008 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ButtonSpinnerPage.xaml.cs @@ -0,0 +1,49 @@ +using System; +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class ButtonSpinnerPage : ContentPage + { + public ButtonSpinnerPage() + { + InitializeComponent(); + } + + public void OnSpin(object sender, SpinEventArgs e) + { + var spinner = (ButtonSpinner)sender; + + if (spinner.Content is TextBlock txtBox) + { + int value = Array.IndexOf(_mountains, txtBox.Text); + if (e.Direction == SpinDirection.Increase) + value++; + else + value--; + + if (value < 0) + value = _mountains.Length - 1; + else if (value >= _mountains.Length) + value = 0; + + txtBox.Text = _mountains[value]; + } + + } + + private readonly string[] _mountains = new[] + { + "Everest", + "K2 (Mount Godwin Austen)", + "Kangchenjunga", + "Lhotse", + "Makalu", + "Cho Oyu", + "Dhaulagiri", + "Manaslu", + "Nanga Parbat", + "Annapurna" + }; + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/ButtonsPage.xaml b/samples/AvaloniaControlCatalog/Pages/ButtonsPage.xaml new file mode 100644 index 000000000..695ae2851 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ButtonsPage.xaml @@ -0,0 +1,292 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A standard button control + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A button control with multiple states: checked, unchecked or indeterminate. + + + + + + + + + + + + + + A button control that raises its Click event repeatedly when it is pressed and held. + + + + + + + + + + + + + + + + A button control that functions as a navigateable hyperlink. + + + + + + + + + + + + + + + + + + + + + + A button with an added drop-down chevron to visually indicate it has a flyout with additional actions. + + + + + + + + + + + + Disabled + + + + + + + + + A button with primary and secondary parts that can each be pressed separately. The primary part behaves like a Button and the secondary part opens a flyout. + + + + + + + + + + + + + + + + + + + + + + + + A button with primary and secondary parts that can each be pressed separately. The primary part behaves like a ToggleButton with two states and the secondary part opens a flyout. + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/ButtonsPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/ButtonsPage.xaml.cs new file mode 100644 index 000000000..611fc7624 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ButtonsPage.xaml.cs @@ -0,0 +1,21 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class ButtonsPage : ContentPage + { + private int repeatButtonClickCount = 0; + + public ButtonsPage() + { + InitializeComponent(); + } + + public void OnRepeatButtonClick(object? sender, RoutedEventArgs args) + { + repeatButtonClickCount++; + RepeatButtonTextBlock.Text = $"Repeat Button: {repeatButtonClickCount}"; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/CalendarDatePickerPage.xaml b/samples/AvaloniaControlCatalog/Pages/CalendarDatePickerPage.xaml new file mode 100644 index 000000000..c00af6c80 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CalendarDatePickerPage.xaml @@ -0,0 +1,59 @@ + + + A control for selecting dates with a calendar drop-down + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CalendarDatePickerPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/CalendarDatePickerPage.xaml.cs new file mode 100644 index 000000000..0e4fcbd98 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CalendarDatePickerPage.xaml.cs @@ -0,0 +1,23 @@ +using System; +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class CalendarDatePickerPage : ContentPage + { + public CalendarDatePickerPage() + { + InitializeComponent(); + + DatePicker1.SelectedDate = DateTime.Today; + DatePicker2.SelectedDate = DateTime.Today.AddDays(10); + DatePicker3.SelectedDate = DateTime.Today.AddDays(20); + DatePicker5.SelectedDate = DateTime.Today; + + DatePicker4.TemplateApplied += (s, e) => + { + DatePicker4.BlackoutDates?.AddDatesInPast(); + }; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/CalendarPage.xaml b/samples/AvaloniaControlCatalog/Pages/CalendarPage.xaml new file mode 100644 index 000000000..44ab767de --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CalendarPage.xaml @@ -0,0 +1,64 @@ + + + A calendar control for selecting dates + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + # + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CalendarPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/CalendarPage.xaml.cs new file mode 100644 index 000000000..d9118326f --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CalendarPage.xaml.cs @@ -0,0 +1,20 @@ +using System; +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class CalendarPage : ContentPage + { + public CalendarPage() + { + InitializeComponent(); + + var today = DateTime.Today; + DisplayDatesCalendar.DisplayDateStart = today.AddDays(-25); + DisplayDatesCalendar.DisplayDateEnd = today.AddDays(25); + + BlackoutDatesCalendar.BlackoutDates.AddDatesInPast(); + BlackoutDatesCalendar.BlackoutDates.Add(new CalendarDateRange(today.AddDays(6))); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/CanvasPage.xaml b/samples/AvaloniaControlCatalog/Pages/CanvasPage.xaml new file mode 100644 index 000000000..a9e342181 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CanvasPage.xaml @@ -0,0 +1,38 @@ + + + A panel which lays out its children by explicit coordinates + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CanvasPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/CanvasPage.xaml.cs new file mode 100644 index 000000000..b94aabca9 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CanvasPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class CanvasPage : ContentPage + { + public CanvasPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/CarouselDemoPage.xaml b/samples/AvaloniaControlCatalog/Pages/CarouselDemoPage.xaml new file mode 100644 index 000000000..792441ddb --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CarouselDemoPage.xaml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CarouselDemoPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/CarouselDemoPage.xaml.cs new file mode 100644 index 000000000..0d59d296f --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CarouselDemoPage.xaml.cs @@ -0,0 +1,91 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class CarouselDemoPage : ContentPage + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = + { + // Overview + ("Overview", "First Look", + "Basic CarouselPage with three pages and page indicator.", + () => new CarouselPageFirstLookPage()), + + // Populate + ("Populate", "Data Templates", + "Bind CarouselPage to an ObservableCollection, add or remove pages at runtime, and switch the page template.", + () => new CarouselPageDataTemplatePage()), + + // Appearance + ("Appearance", "Customization", + "Switch slide direction between horizontal and vertical with PageSlide. Page indicator dots update on each selection.", + () => new CarouselPageCustomizationPage()), + + // Features + ("Features", "Page Transitions", + "Animate page switches with CrossFade or PageSlide.", + () => new CarouselPageTransitionsPage()), + ("Features", "Programmatic Selection", + "Jump to any page programmatically with SelectedIndex and respond to SelectionChanged events.", + () => new CarouselPageSelectionPage()), + ("Features", "Gesture & Keyboard", + "Swipe left/right to navigate pages. Toggle IsGestureEnabled and IsKeyboardNavigationEnabled.", + () => new CarouselPageGesturePage()), + ("Features", "Events", + "SelectionChanged, NavigatedTo, and NavigatedFrom events. Swipe or navigate to see the live event log.", + () => new CarouselPageEventsPage()), + + // Performance + ("Performance", "Performance Monitor", + "Track page count, live page instances, and managed heap size. Observe how GC reclaims memory after removing pages.", + () => new CarouselPagePerformancePage()), + + // Showcases + ("Showcases", "Sanctuary", + "Travel discovery app with 3 full-screen immersive pages. Each page has a real background photo, gradient overlay, and themed content. Built as a 1:1 replica of a Stitch design.", + () => new SanctuaryShowcasePage()), + ("Showcases", "Care Companion", + "Healthcare onboarding with CarouselPage (3 pages), then a TabbedPage patient dashboard. Skip or complete onboarding to navigate to the dashboard via RemovePage.", + () => new CareCompanionAppPage()), + + // Carousel (ItemsControl) demos + ("Carousel", "Getting Started", + "Basic Carousel with image items and previous/next navigation buttons.", + () => new CarouselGettingStartedPage()), + ("Carousel", "Transitions", + "Configure page transitions: PageSlide, CrossFade, 3D Rotation, or None.", + () => new CarouselTransitionsPage()), + ("Carousel", "Customization", + "Adjust orientation and transition type to tailor the carousel layout.", + () => new CarouselCustomizationPage()), + ("Carousel", "Gestures & Keyboard", + "Navigate items via swipe gesture and arrow keys. Toggle each input mode on and off.", + () => new CarouselGesturesPage()), + ("Carousel", "Vertical Orientation", + "Carousel with Orientation set to Vertical, navigated with Up/Down keys, swipe, or buttons.", + () => new CarouselVerticalPage()), + ("Carousel", "Multi-Item Peek", + "Adjust ViewportFraction to show multiple items simultaneously with adjacent cards peeking.", + () => new CarouselMultiItemPage()), + ("Carousel", "Data Binding", + "Bind Carousel to an ObservableCollection and add, remove, or shuffle items at runtime.", + () => new CarouselDataBindingPage()), + ("Carousel", "Curated Gallery", + "Editorial art gallery app with DrawerPage navigation, hero Carousel with PipsPager dots, and a horizontal peek carousel for collection highlights.", + () => new CarouselGalleryAppPage()), + }; + + public CarouselDemoPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/CarouselPage.xaml b/samples/AvaloniaControlCatalog/Pages/CarouselPage.xaml new file mode 100644 index 000000000..bdb40b903 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CarouselPage.xaml @@ -0,0 +1,119 @@ + + + A swipeable items control that can reveal adjacent pages with ViewportFraction. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Transition + + None + Page Slide + Cross Fade + Rotate 3D + Card Stack + Wave Reveal + Composite (Slide + Fade) + + + Orientation + + Horizontal + Vertical + + + Viewport Fraction + + + + 1.00 + + + + + + + + + Wrap Selection + Swipe Enabled + + + + + + + + Total Items: + 0 + + + Selected Index: + 0 + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CarouselPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/CarouselPage.xaml.cs new file mode 100644 index 000000000..c4a6ffa12 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CarouselPage.xaml.cs @@ -0,0 +1,149 @@ +using System; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using ControlCatalog.Pages.Transitions; + +namespace ControlCatalog.Pages +{ + public partial class CarouselPage : ContentPage + { + public CarouselPage() + { + InitializeComponent(); + + left.Click += (s, e) => carousel.Previous(); + right.Click += (s, e) => carousel.Next(); + transition.SelectionChanged += TransitionChanged; + orientation.SelectionChanged += TransitionChanged; + viewportFraction.ValueChanged += ViewportFractionChanged; + + wrapSelection.IsChecked = carousel.WrapSelection; + wrapSelection.IsCheckedChanged += (s, e) => + { + carousel.WrapSelection = wrapSelection.IsChecked ?? false; + UpdateButtonState(); + }; + + swipeEnabled.IsChecked = carousel.IsSwipeEnabled; + swipeEnabled.IsCheckedChanged += (s, e) => + { + carousel.IsSwipeEnabled = swipeEnabled.IsChecked ?? false; + }; + + carousel.PropertyChanged += (s, e) => + { + if (e.Property == SelectingItemsControl.SelectedIndexProperty) + { + UpdateButtonState(); + } + else if (e.Property == Carousel.ViewportFractionProperty) + { + UpdateViewportFractionDisplay(); + } + }; + + carousel.ViewportFraction = viewportFraction.Value; + UpdateButtonState(); + UpdateViewportFractionDisplay(); + } + + private void UpdateButtonState() + { + itemsCountIndicator.Text = carousel.ItemCount.ToString(); + selectedIndexIndicator.Text = carousel.SelectedIndex.ToString(); + + var wrap = carousel.WrapSelection; + left.IsEnabled = wrap || carousel.SelectedIndex > 0; + right.IsEnabled = wrap || carousel.SelectedIndex < carousel.ItemCount - 1; + } + + private void ViewportFractionChanged(object? sender, RangeBaseValueChangedEventArgs e) + { + carousel.ViewportFraction = Math.Round(e.NewValue, 2); + UpdateViewportFractionDisplay(); + } + + private void UpdateViewportFractionDisplay() + { + var value = carousel.ViewportFraction; + viewportFractionIndicator.Text = value.ToString("0.00"); + + var pagesInView = 1d / value; + viewportFractionHint.Text = value >= 1d + ? "1.00 shows a single full page." + : $"{pagesInView:0.##} pages fit in view. Try 0.80 for peeking or 0.33 for three full items."; + } + + private void TransitionChanged(object? sender, SelectionChangedEventArgs e) + { + var isVertical = orientation.SelectedIndex == 1; + var axis = isVertical ? PageSlide.SlideAxis.Vertical : PageSlide.SlideAxis.Horizontal; + + switch (transition.SelectedIndex) + { + case 0: + carousel.PageTransition = null; + break; + case 1: + carousel.PageTransition = new PageSlide(TimeSpan.FromSeconds(0.25), axis); + break; + case 2: + carousel.PageTransition = new CrossFade(TimeSpan.FromSeconds(0.25)); + break; + case 3: + carousel.PageTransition = new Rotate3DTransition(TimeSpan.FromSeconds(0.5), axis); + break; + case 4: + carousel.PageTransition = new CardStackPageTransition(TimeSpan.FromSeconds(0.5), axis); + break; + case 5: + carousel.PageTransition = new WaveRevealPageTransition(TimeSpan.FromSeconds(0.8), axis); + break; + case 6: + carousel.PageTransition = new CompositePageTransition + { + PageTransitions = + { + new PageSlide(TimeSpan.FromSeconds(0.25), axis), + new CrossFade(TimeSpan.FromSeconds(0.25)), + } + }; + break; + } + + UpdateLayoutForOrientation(isVertical); + } + + private void UpdateLayoutForOrientation(bool isVertical) + { + if (isVertical) + { + Grid.SetColumn(left, 1); + Grid.SetRow(left, 0); + Grid.SetColumn(right, 1); + Grid.SetRow(right, 2); + + left.Padding = new Thickness(20, 10); + right.Padding = new Thickness(20, 10); + + leftArrow.RenderTransform = new Avalonia.Media.RotateTransform(90); + rightArrow.RenderTransform = new Avalonia.Media.RotateTransform(90); + } + else + { + Grid.SetColumn(left, 0); + Grid.SetRow(left, 1); + Grid.SetColumn(right, 2); + Grid.SetRow(right, 1); + + left.Padding = new Thickness(10, 20); + right.Padding = new Thickness(10, 20); + + leftArrow.RenderTransform = null; + rightArrow.RenderTransform = null; + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml b/samples/AvaloniaControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml new file mode 100644 index 000000000..a11a1ea6b --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml.cs new file mode 100644 index 000000000..16f1ba0dd --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml.cs @@ -0,0 +1,1069 @@ +using System; +using System.Collections.ObjectModel; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Styling; +using AvaCarouselPage = Avalonia.Controls.CarouselPage; + +namespace ControlCatalog.Pages; + +public partial class CareCompanionAppPage : UserControl +{ + static readonly Color Primary = Color.Parse("#137fec"); + static readonly Color PrimaryDark = Color.Parse("#0a5bb5"); + static readonly Color PrimaryLight = Color.Parse("#e0f0ff"); + static readonly Color BgLight = Color.Parse("#f6f7f8"); + static readonly Color TextDark = Color.Parse("#111827"); + static readonly Color TextMuted = Color.Parse("#64748b"); + static readonly Color CardBg = Colors.White; + static readonly Color SuccessGreen = Color.Parse("#10b981"); + static readonly Color WarningAmber = Color.Parse("#f59e0b"); + + NavigationPage? _navPage; + AvaCarouselPage? _onboarding; + ScrollViewer? _infoPanel; + + public CareCompanionAppPage() + { + InitializeComponent(); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + _infoPanel = this.FindControl("InfoPanel"); + UpdateInfoVisibility(); + + _navPage = this.FindControl("NavPage"); + if (_navPage == null) return; + + _onboarding = BuildOnboardingCarousel(); + _ = _navPage.PushAsync(_onboarding); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == BoundsProperty) + UpdateInfoVisibility(); + } + + void UpdateInfoVisibility() + { + if (_infoPanel != null) + _infoPanel.IsVisible = Bounds.Width >= 650; + } + + static TextBlock Txt(string text, double size, FontWeight weight, Color color, + double opacity = 1, TextAlignment align = TextAlignment.Left, + TextWrapping wrap = TextWrapping.NoWrap) + => new TextBlock + { + Text = text, + FontSize = size, + FontWeight = weight, + Foreground = new SolidColorBrush(color), + Opacity = opacity, + TextAlignment = align, + TextWrapping = wrap, + }; + + static Button StyledButton(object content, IBrush bg, IBrush fg, double height, + CornerRadius radius, Thickness margin = default, double fontSize = 14, + FontWeight fontWeight = FontWeight.SemiBold, + IBrush? border = null, Thickness borderThick = default) + { + var btn = new Button + { + Content = content, + Background = bg, + Foreground = fg, + Height = height, + CornerRadius = radius, + Margin = margin, + Padding = new Thickness(16, 0), + HorizontalContentAlignment = HorizontalAlignment.Center, + VerticalContentAlignment = VerticalAlignment.Center, + BorderBrush = border, + BorderThickness = borderThick, + }; + + var over = new Style(x => x.OfType + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryMainPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryMainPage.xaml.cs new file mode 100644 index 000000000..05a4097a4 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryMainPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class SanctuaryMainPage : UserControl +{ + public SanctuaryMainPage() + { + InitializeComponent(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml b/samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml new file mode 100644 index 000000000..50864e5e5 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml @@ -0,0 +1,335 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml.cs new file mode 100644 index 000000000..be9137069 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml.cs @@ -0,0 +1,70 @@ +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages; + +public partial class SanctuaryShowcasePage : UserControl +{ + public SanctuaryShowcasePage() + { + InitializeComponent(); + } + + private void OnPage1CTA(object? sender, RoutedEventArgs e) + { + DemoCarousel.SelectedIndex = 1; + } + + private void OnPage2CTA(object? sender, RoutedEventArgs e) + { + DemoCarousel.SelectedIndex = 2; + } + + private async void OnPage3CTA(object? sender, RoutedEventArgs e) + { + var nav = this.FindAncestorOfType(); + if (nav == null) + return; + + var carouselWrapper = nav.NavigationStack.LastOrDefault(); + + var headerGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("*, Auto") }; + headerGrid.Children.Add(new TextBlock + { + Text = "Sanctuary", + VerticalAlignment = VerticalAlignment.Center + }); + var closeIcon = Geometry.Parse( + "M4.397 4.397a1 1 0 0 1 1.414 0L12 10.585l6.19-6.188a1 1 0 0 1 1.414 1.414L13.413 12l6.19 6.189a1 1 0 0 1-1.414 1.414L12 13.413l-6.189 6.19a1 1 0 0 1-1.414-1.414L10.585 12 4.397 5.811a1 1 0 0 1 0-1.414z"); + var closeBtn = new Button + { + Content = new PathIcon { Data = closeIcon }, + Background = Brushes.Transparent, + BorderThickness = new Thickness(0), + Padding = new Thickness(8, 4), + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(closeBtn, 1); + headerGrid.Children.Add(closeBtn); + closeBtn.Click += async (_, _) => await nav.PopAsync(null); + + var mainPage = new ContentPage + { + Header = headerGrid, + Content = new SanctuaryMainPage() + }; + NavigationPage.SetHasBackButton(mainPage, false); + + await nav.PushAsync(mainPage); + + if (carouselWrapper != null) + { + nav.RemovePage(carouselWrapper); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/CheckBoxPage.xaml b/samples/AvaloniaControlCatalog/Pages/CheckBoxPage.xaml new file mode 100644 index 000000000..4f9c59448 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CheckBoxPage.xaml @@ -0,0 +1,29 @@ + + + A check box control + + + + _Unchecked + _Checked + _Indeterminate + Disabled + + + Three State: Unchecked + Three State: Checked + Three State: Indeterminate + Three State: Disabled + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CheckBoxPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/CheckBoxPage.xaml.cs new file mode 100644 index 000000000..b63d7084d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CheckBoxPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class CheckBoxPage : ContentPage + { + public CheckBoxPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/ClipboardPage.xaml b/samples/AvaloniaControlCatalog/Pages/ClipboardPage.xaml new file mode 100644 index 000000000..86e99b930 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ClipboardPage.xaml @@ -0,0 +1,31 @@ + + + Example of ClipboardPage capabilities + + + + + + + + + + + + + Precise dirty rects + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hello world + + + Inner context flyout + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/ContextFlyoutPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/ContextFlyoutPage.xaml.cs new file mode 100644 index 000000000..f25b9341c --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ContextFlyoutPage.xaml.cs @@ -0,0 +1,59 @@ +using System; +using System.ComponentModel; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public partial class ContextFlyoutPage : ContentPage + { + public ContextFlyoutPage() + { + InitializeComponent(); + DataContext = new ContextPageViewModel(); + } + + private ContextPageViewModel? _model; + protected override void OnDataContextChanged(EventArgs e) + { + if (_model != null) + _model.View = null; + _model = DataContext as ContextPageViewModel; + if (_model != null) + _model.View = this; + + base.OnDataContextChanged(e); + } + + private void ContextFlyoutPage_Closing(object? sender, CancelEventArgs e) + { + e.Cancel = CancelCloseCheckBox?.IsChecked ?? false; + } + + private void ContextFlyoutPage_Opening(object? sender, EventArgs e) + { + if (e is CancelEventArgs cancelArgs) + { + cancelArgs.Cancel = CancelCloseCheckBox?.IsChecked ?? false; + } + } + + private void CloseFlyout(object? sender, RoutedEventArgs e) + { + TextBox.ContextFlyout?.Hide(); + } + + public void CustomContextRequested(object? sender, ContextRequestedEventArgs e) + { + if (sender is Border border && border.Child is TextBlock textBlock) + { + textBlock.Text = e.TryGetPosition(border, out var point) + ? $"Context was requested with pointer at: {point.X:N0}, {point.Y:N0}" + : "Context was requested without pointer"; + e.Handled = true; + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/ContextMenuPage.xaml b/samples/AvaloniaControlCatalog/Pages/ContextMenuPage.xaml new file mode 100644 index 000000000..7e2c1a221 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ContextMenuPage.xaml @@ -0,0 +1,100 @@ + + + A right click menu that can be applied to any control. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/ContextMenuPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/ContextMenuPage.xaml.cs new file mode 100644 index 000000000..86ce80712 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ContextMenuPage.xaml.cs @@ -0,0 +1,51 @@ +using System; +using System.ComponentModel; +using Avalonia.Controls; +using Avalonia.Input; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public partial class ContextMenuPage : ContentPage + { + public ContextMenuPage() + { + InitializeComponent(); + DataContext = new ContextPageViewModel(); + } + + private ContextPageViewModel? _model; + protected override void OnDataContextChanged(EventArgs e) + { + if (_model != null) + _model.View = null; + _model = DataContext as ContextPageViewModel; + if (_model != null) + _model.View = this; + + base.OnDataContextChanged(e); + } + + private void ContextFlyoutPage_Closing(object? sender, CancelEventArgs e) + { + e.Cancel = CancelCloseCheckBox?.IsChecked ?? false; + } + + private void ContextFlyoutPage_Opening(object? sender, CancelEventArgs e) + { + e.Cancel = CancelOpenCheckBox?.IsChecked ?? false; + } + + public void CustomContextRequested(object? sender, ContextRequestedEventArgs e) + { + if (sender is Border border && border.Child is TextBlock textBlock) + { + textBlock.Text = e.TryGetPosition(border, out var point) + ? $"Context was requested with pointer at: {point.X:N0}, {point.Y:N0}" + : "Context was requested without pointer"; + e.Handled = true; + } + + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/CursorPage.xaml b/samples/AvaloniaControlCatalog/Pages/CursorPage.xaml new file mode 100644 index 000000000..ae20193c9 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CursorPage.xaml @@ -0,0 +1,31 @@ + + + + Defines a cursor (mouse pointer) + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CursorPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/CursorPage.xaml.cs new file mode 100644 index 000000000..88699ac6f --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CursorPage.xaml.cs @@ -0,0 +1,14 @@ +using Avalonia.Controls; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public partial class CursorPage : ContentPage + { + public CursorPage() + { + InitializeComponent(); + DataContext = new CursorPageViewModel(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/CustomDrawing.xaml b/samples/AvaloniaControlCatalog/Pages/CustomDrawing.xaml new file mode 100644 index 000000000..f1cb2dab1 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CustomDrawing.xaml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DialogsPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DialogsPage.xaml.cs new file mode 100644 index 000000000..626f63101 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DialogsPage.xaml.cs @@ -0,0 +1,512 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Security; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Platform.Storage; + +namespace ControlCatalog.Pages +{ + public partial class DialogsPage : ContentPage + { + public DialogsPage() + { + InitializeComponent(); + + IStorageFolder? lastSelectedDirectory = null; + IStorageItem? lastSelectedItem = null; + bool ignoreTextChanged = false; + + var results = PickerLastResults; + var resultsVisible = PickerLastResultsVisible; + var bookmarkContainer = BookmarkContainer; + var openedFileContent = OpenedFileContent; + var openMultiple = OpenMultiple; + var currentFolderBox = CurrentFolderBox; + var useSuggestedFilter = UseSuggestedFilter; + var suggestedFilterSelector = SuggestedFilterSelector; + + currentFolderBox.TextChanged += async (sender, args) => + { + if (ignoreTextChanged) return; + + if (Enum.TryParse(currentFolderBox.Text, true, out var folderEnum)) + { + lastSelectedDirectory = await GetStorageProvider().TryGetWellKnownFolderAsync(folderEnum); + } + else if (!string.IsNullOrWhiteSpace(currentFolderBox.Text)) + { + if (!Uri.TryCreate(currentFolderBox.Text, UriKind.Absolute, out var folderLink)) + { + Uri.TryCreate("file://" + currentFolderBox.Text, UriKind.Absolute, out folderLink); + } + + if (folderLink is not null) + { + try + { + lastSelectedDirectory = await GetStorageProvider().TryGetFolderFromPathAsync(folderLink); + } + catch (SecurityException) + { + + } + } + } + }; + + + List? BuildFileTypes() + { + var selectedItem = (FilterSelector.SelectedItem as ComboBoxItem)?.Content + ?? "None"; + + var binLogType = new FilePickerFileType("Binary Log") + { + Patterns = new[] { "*.binlog", "*.buildlog" }, + MimeTypes = new[] { "application/binlog", "application/buildlog" }, + AppleUniformTypeIdentifiers = new[] { "public.data" } + }; + + return selectedItem switch + { + "All + TXT + BinLog" => new List + { + FilePickerFileTypes.All, FilePickerFileTypes.TextPlain, binLogType + }, + "Binlog" => new List { binLogType }, + "TXT extension only" => new List + { + new("TXT") { Patterns = FilePickerFileTypes.TextPlain.Patterns } + }, + "TXT mime only" => new List + { + new("TXT") { MimeTypes = FilePickerFileTypes.TextPlain.MimeTypes } + }, + "TXT apple type id only" => new List + { + new("TXT") + { + AppleUniformTypeIdentifiers = + FilePickerFileTypes.TextPlain.AppleUniformTypeIdentifiers + } + }, + _ => null + }; + } + + List? GetFileTypes() + { + var types = BuildFileTypes(); + UpdateSuggestedFilterSelector(types); + return types; + } + + void UpdateSuggestedFilterSelector(IReadOnlyList? types) + { + var previouslySelected = (suggestedFilterSelector.SelectedItem as ComboBoxItem)?.Tag as FilePickerFileType; + suggestedFilterSelector.Items.Clear(); + suggestedFilterSelector.Items.Add(new ComboBoxItem { Content = "First filter", Tag = null }); + + var desiredIndex = 0; + if (types is { Count: > 0 }) + { + for (var i = 0; i < types.Count; i++) + { + var type = types[i]; + var item = new ComboBoxItem { Content = type.Name, Tag = type }; + suggestedFilterSelector.Items.Add(item); + + if (previouslySelected is not null && ReferenceEquals(previouslySelected, type)) + { + desiredIndex = i + 1; + } + } + } + + suggestedFilterSelector.SelectedIndex = desiredIndex; + } + + FilePickerFileType? GetSuggestedFileType(IReadOnlyList? types) + { + if (useSuggestedFilter.IsChecked == true && types is { Count: > 0 }) + { + if (suggestedFilterSelector.SelectedItem is ComboBoxItem { Tag: FilePickerFileType selectedType } + && types.Any(t => ReferenceEquals(t, selectedType))) + { + return selectedType; + } + + return types.FirstOrDefault(); + } + + return null; + } + + void UpdateSuggestedFilterSelectorState() => + suggestedFilterSelector.IsEnabled = useSuggestedFilter.IsChecked == true; + + useSuggestedFilter.IsCheckedChanged += (_, _) => UpdateSuggestedFilterSelectorState(); + UpdateSuggestedFilterSelectorState(); + + FilterSelector.SelectionChanged += (_, _) => UpdateSuggestedFilterSelector(BuildFileTypes()); + UpdateSuggestedFilterSelector(BuildFileTypes()); + + DecoratedWindow.Click += delegate + { + new DecoratedWindow().Show(); + }; + DecoratedWindowDialog.Click += delegate + { + _ = new DecoratedWindow().ShowDialog(GetWindow()); + }; + Dialog.Click += delegate + { + var window = CreateSampleWindow(); + window.Height = 200; + _ = window.ShowDialog(GetWindow()); + }; + DialogNoTaskbar.Click += delegate + { + var window = CreateSampleWindow(); + window.Height = 200; + window.ShowInTaskbar = false; + _ = window.ShowDialog(GetWindow()); + }; + OwnedWindow.Click += delegate + { + var window = CreateSampleWindow(); + + window.Show(GetWindow()); + }; + + OwnedWindowNoTaskbar.Click += delegate + { + var window = CreateSampleWindow(); + + window.ShowInTaskbar = false; + + window.Show(GetWindow()); + }; + + OpenFilePicker.Click += async delegate + { + var fileTypes = GetFileTypes(); + var result = await GetStorageProvider().OpenFilePickerAsync(new FilePickerOpenOptions() + { + Title = "Open file", + FileTypeFilter = fileTypes, + SuggestedFileType = GetSuggestedFileType(fileTypes), + SuggestedFileName = "FileName", + SuggestedStartLocation = lastSelectedDirectory, + AllowMultiple = openMultiple.IsChecked == true + }); + + await SetPickerResult(result); + }; + SaveFilePicker.Click += async delegate + { + var fileTypes = GetFileTypes(); + var suggestedType = GetSuggestedFileType(fileTypes); + var file = await GetStorageProvider().SaveFilePickerAsync(new FilePickerSaveOptions() + { + Title = "Save file", + FileTypeChoices = fileTypes, + SuggestedFileType = suggestedType, + SuggestedStartLocation = lastSelectedDirectory, + SuggestedFileName = "FileName", + ShowOverwritePrompt = true + }); + + if (file is not null) + { + try + { + // Sync disposal of StreamWriter is not supported on WASM + await using var stream = await file.OpenWriteAsync(); + await using var writer = new System.IO.StreamWriter(stream); + await writer.WriteLineAsync(openedFileContent.Text); + + SetFolder(await file.GetParentAsync()); + } + catch (Exception ex) + { + openedFileContent.Text = ex.ToString(); + } + } + + await SetPickerResult(file is null ? null : new[] { file }); + }; + SaveFilePickerWithResult.Click += async delegate + { + var saveFileTypes = new[] { FilePickerFileTypes.Json, FilePickerFileTypes.Xml }; + var result = await GetStorageProvider().SaveFilePickerWithResultAsync(new FilePickerSaveOptions() + { + Title = "Save file", + FileTypeChoices = saveFileTypes, + SuggestedFileType = GetSuggestedFileType(saveFileTypes), + SuggestedStartLocation = lastSelectedDirectory, + SuggestedFileName = "FileName", + ShowOverwritePrompt = true + }); + + try + { + if (result.File is { } file) + { + // Sync disposal of StreamWriter is not supported on WASM + await using var stream = await file.OpenWriteAsync(); + await using var writer = new System.IO.StreamWriter(stream); + if (result.SelectedFileType == FilePickerFileTypes.Xml) + { + await writer.WriteLineAsync("Test"); + } + else + { + await writer.WriteLineAsync("""{ "sample": "Test" }"""); + } + + SetFolder(await result.File.GetParentAsync()); + } + } + catch (Exception ex) + { + openedFileContent.Text = ex.ToString(); + } + + await SetPickerResult(result.File is null ? null : new[] { result.File }, result.SelectedFileType); + }; + OpenFolderPicker.Click += async delegate + { + var folders = await GetStorageProvider().OpenFolderPickerAsync(new FolderPickerOpenOptions() + { + Title = "Folder file", + SuggestedStartLocation = lastSelectedDirectory, + SuggestedFileName = "FileName", + AllowMultiple = openMultiple.IsChecked == true + }); + + await SetPickerResult(folders); + }; + OpenFileFromBookmark.Click += async delegate + { + var file = bookmarkContainer.Text is not null + ? await GetStorageProvider().OpenFileBookmarkAsync(bookmarkContainer.Text) + : null; + + await SetPickerResult(file is null ? null : new[] { file }); + }; + OpenFolderFromBookmark.Click += async delegate + { + var folder = bookmarkContainer.Text is not null + ? await GetStorageProvider().OpenFolderBookmarkAsync(bookmarkContainer.Text) + : null; + + await SetPickerResult(folder is null ? null : new[] { folder }); + }; + + LaunchUri.Click += async delegate + { + var statusBlock = LaunchStatus; + if (Uri.TryCreate(UriToLaunch.Text, UriKind.Absolute, out var uri)) + { + var result = await TopLevel.GetTopLevel(this)!.Launcher.LaunchUriAsync(uri); + statusBlock.Text = "LaunchUriAsync returned " + result; + } + else + { + statusBlock.Text = "Can't parse the Uri"; + } + }; + + LaunchFile.Click += async delegate + { + var statusBlock = LaunchStatus; + if (lastSelectedItem is not null) + { + var result = await TopLevel.GetTopLevel(this)!.Launcher.LaunchFileAsync(lastSelectedItem); + statusBlock.Text = "LaunchFileAsync returned " + result; + } + else + { + statusBlock.Text = "Please select any file or folder first"; + } + }; + + void SetFolder(IStorageFolder? folder) + { + ignoreTextChanged = true; + lastSelectedDirectory = folder; + lastSelectedItem = folder; + currentFolderBox.Text = folder?.Path is { IsAbsoluteUri: true } abs ? abs.LocalPath : folder?.Path?.ToString(); + ignoreTextChanged = false; + } + async Task SetPickerResult(IReadOnlyCollection? items, FilePickerFileType? selectedType = null) + { + items ??= Array.Empty(); + bookmarkContainer.Text = items.FirstOrDefault(f => f.CanBookmark) is { } f ? await f.SaveBookmarkAsync() : "Can't bookmark"; + var mappedResults = new List(); + + string resultText = ""; + if (items.FirstOrDefault() is IStorageItem item) + { + resultText += item is IStorageFile ? "File:" : "Folder:"; + resultText += Environment.NewLine; + + var props = await item.GetBasicPropertiesAsync(); + resultText += @$"Size: {props.Size} + DateCreated: {props.DateCreated} + DateModified: {props.DateModified} + CanBookmark: {item.CanBookmark} + "; + if (item is IStorageFile file) + { + resultText += @$" + Content: + "; + + try + { + resultText += await ReadTextFromFile(file, 500); + } + catch (Exception ex) + { + resultText += ex.ToString(); + } + } + + if (item is IStorageFolder storageFolder) + { + SetFolder(storageFolder); + } + else + { + var parent = await item.GetParentAsync(); + SetFolder(parent); + if (parent is not null) + { + mappedResults.Add(FullPathOrName(parent)); + } + } + + foreach (var selectedItem in items) + { + mappedResults.Add("+> " + FullPathOrName(selectedItem)); + if (selectedItem is IStorageFolder folder) + { + await foreach (var innerItem in folder.GetItemsAsync()) + { + mappedResults.Add("++> " + FullPathOrName(innerItem)); + } + } + } + lastSelectedItem = item; + } + + if (selectedType is not null) + { + resultText += Environment.NewLine + "Selected type: " + selectedType.Name; + } + + openedFileContent.Text = resultText; + results.ItemsSource = mappedResults; + resultsVisible.IsVisible = mappedResults.Any(); + } + } + + internal static async Task ReadTextFromFile(IStorageFile file, int length) + { + await using var stream = await file.OpenReadAsync(); + using var reader = new System.IO.StreamReader(stream); + + // 4GB file test, shouldn't load more than 10000 chars into a memory. + var buffer = ArrayPool.Shared.Rent(length); + try + { + var charsRead = await reader.ReadAsync(buffer, 0, length); + return new string(buffer, 0, charsRead); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + var openedFileContent = OpenedFileContent; + try + { + var storageProvider = GetStorageProvider(); + openedFileContent.Text = $@"CanOpen: {storageProvider.CanOpen} +CanSave: {storageProvider.CanSave} +CanPickFolder: {storageProvider.CanPickFolder}"; + } + catch (Exception ex) + { + openedFileContent.Text = "Storage provider is not available: " + ex.Message; + } + } + + private Window CreateSampleWindow() + { + Button button; + Button dialogButton; + + var window = new Window + { + Height = 200, + Width = 200, + Content = new StackPanel + { + Spacing = 4, + Children = + { + new TextBlock { Text = "Hello world!" }, + (button = new Button + { + HorizontalAlignment = HorizontalAlignment.Center, + Content = "Click to close", + IsDefault = true + }), + (dialogButton = new Button + { + HorizontalAlignment = HorizontalAlignment.Center, + Content = "Dialog", + IsDefault = false + }) + } + }, + WindowStartupLocation = WindowStartupLocation.CenterOwner + }; + + button.Click += (_, __) => window.Close(); + dialogButton.Click += (_, __) => + { + var dialog = CreateSampleWindow(); + dialog.Height = 200; + dialog.ShowDialog(window); + }; + + return window; + } + + private IStorageProvider GetStorageProvider() => GetTopLevel().StorageProvider; + + private static string FullPathOrName(IStorageItem? item) + { + if (item is null) return "(null)"; + return item.Path is { IsAbsoluteUri: true } path ? path.ToString() : item.Name; + } + + Window GetWindow() => TopLevel.GetTopLevel(this) as Window ?? throw new NullReferenceException("Invalid Owner"); + TopLevel GetTopLevel() => TopLevel.GetTopLevel(this) ?? throw new NullReferenceException("Invalid Owner"); + } +#pragma warning restore CS0618 // Type or member is obsolete +} diff --git a/samples/AvaloniaControlCatalog/Pages/DragAndDropPage.xaml b/samples/AvaloniaControlCatalog/Pages/DragAndDropPage.xaml new file mode 100644 index 000000000..7300f8c00 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DragAndDropPage.xaml @@ -0,0 +1,61 @@ + + + + + + + + Example of Drag+Drop capabilities + + + + + Drag Me (text) + + + Drag Me (files) + + + Drag Me (bitmap) + + + Drag Me (custom) + + + + + + Drop some text, files, bitmap or custom format here (Copy) + + + Drop some text or custom format here (Move) + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DragAndDropPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DragAndDropPage.xaml.cs new file mode 100644 index 000000000..d9fe1af1d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DragAndDropPage.xaml.cs @@ -0,0 +1,174 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +using Avalonia.Platform.Storage; + +namespace ControlCatalog.Pages +{ + public partial class DragAndDropPage : ContentPage + { + private readonly DataFormat _customFormat = + DataFormat.CreateStringApplicationFormat("xxx-avalonia-controlcatalog-custom"); + + public DragAndDropPage() + { + InitializeComponent(); + + int textCount = 0; + + SetupDnd( + "Text", + d => d.Add(DataTransferItem.Create(DataFormat.Text, $"Text was dragged {++textCount} times")), + DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link); + + SetupDnd( + "Custom", + d => d.Add(DataTransferItem.Create(_customFormat, "Test123")), + DragDropEffects.Copy | DragDropEffects.Move); + + SetupDnd( + "Files", + async d => + { + var currentFile = Environment.ProcessPath; + + if (File.Exists(currentFile) && + TopLevel.GetTopLevel(this) is { } topLevel && + await topLevel.StorageProvider.TryGetFileFromPathAsync(currentFile) is { } storageFile) + { + d.Add(DataTransferItem.Create(DataFormat.File, storageFile)); + } + }, + DragDropEffects.Copy); + + SetupDnd( + "Bitmap", + d => d.Add(DataTransferItem.Create(DataFormat.Bitmap, new Bitmap(AssetLoader.Open(new Uri("avares://ControlCatalog/Assets/image1.jpg"))))), + DragDropEffects.Copy); + + AddHandlers(CopyTarget, DragDropEffects.Copy); + AddHandlers(MoveTarget, DragDropEffects.Move); + } + + private void SetupDnd(string suffix, Action factory, DragDropEffects effects) => + SetupDnd( + suffix, + o => + { + factory(o); + return Task.CompletedTask; + }, + effects); + + private void SetupDnd(string suffix, Func factory, DragDropEffects effects) + { + var dragMe = this.Get("DragMe" + suffix); + var dragState = this.Get("DragState" + suffix); + + async void DoDrag(object? sender, PointerPressedEventArgs e) + { + var dragData = new DataTransfer(); + await factory(dragData); + + var result = await DragDrop.DoDragDropAsync(e, dragData, effects); + switch (result) + { + case DragDropEffects.Move: + dragState.Text = "Data was moved"; + break; + case DragDropEffects.Copy: + dragState.Text = "Data was copied"; + break; + case DragDropEffects.Link: + dragState.Text = "Data was linked"; + break; + case DragDropEffects.None: + dragState.Text = "The drag operation was canceled"; + break; + default: + dragState.Text = "Unknown result"; + break; + } + } + + dragMe.PointerPressed += DoDrag; + } + + private void AddHandlers(Control target, DragDropEffects allowedEffects) + { + DragDrop.AddDragEnterHandler(target, DragOver); + DragDrop.AddDragOverHandler(target, DragOver); + DragDrop.AddDropHandler(target, Drop); + + void DragOver(object? sender, DragEventArgs e) + { + e.DragEffects &= allowedEffects; + + // Only allow if the dragged data contains text or filenames. + if (!e.DataTransfer.Contains(DataFormat.Text) + && !e.DataTransfer.Contains(DataFormat.File) + && !e.DataTransfer.Contains(DataFormat.Bitmap) + && !e.DataTransfer.Contains(_customFormat)) + e.DragEffects = DragDropEffects.None; + } + + async void Drop(object? sender, DragEventArgs e) + { + e.DragEffects &= allowedEffects; + + if (e.DragEffects == DragDropEffects.None) + return; + + if (e.DataTransfer.Contains(DataFormat.Text)) + { + DropState.Content = e.DataTransfer.TryGetText(); + } + else if (e.DataTransfer.Contains(DataFormat.File)) + { + var files = e.DataTransfer.TryGetFiles() ?? []; + var contentStr = ""; + + foreach (var item in files) + { + if (item is IStorageFile file) + { + var content = await DialogsPage.ReadTextFromFile(file, 500); + contentStr += $"File {item.Name}:{Environment.NewLine}{content}{Environment.NewLine}{Environment.NewLine}"; + } + else if (item is IStorageFolder folder) + { + var childrenCount = 0; + await foreach (var _ in folder.GetItemsAsync()) + { + childrenCount++; + } + contentStr += $"Folder {item.Name}: items {childrenCount}{Environment.NewLine}{Environment.NewLine}"; + } + } + DropState.Content = contentStr; + } + else if (e.DataTransfer.Contains(DataFormat.Bitmap)) + { + var bitmap = e.DataTransfer.TryGetValue(DataFormat.Bitmap); + DropState.Content = new Image + { + Source = bitmap, Width = 400, Height = 300, Stretch = Stretch.Uniform + }; + } + else if (e.DataTransfer.Contains(_customFormat)) + { + DropState.Content = "Custom: " + e.DataTransfer.TryGetValue(_customFormat); + } + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerDemoPage.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerDemoPage.xaml new file mode 100644 index 000000000..f1edd0bc6 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerDemoPage.xaml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerDemoPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerDemoPage.xaml.cs new file mode 100644 index 000000000..27eb93ff0 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerDemoPage.xaml.cs @@ -0,0 +1,76 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class DrawerDemoPage : ContentPage + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = + { + // Overview + ("Overview", "First Look", "Basic DrawerPage with a navigation drawer, menu items, and detail content.", + () => new DrawerPageFirstLookPage()), + + // Features + ("Features", "Navigation", + "Master-detail pattern: select a drawer menu item to navigate the detail via a NavigationPage with hamburger-to-back-button transition.", + () => new DrawerPageNavigationPage()), + ("Features", "Compact Rail", + "CompactOverlay and CompactInline layout modes: a narrow icon rail is always visible and expands on open. Adjust rail width and open pane width.", + () => new DrawerPageCompactPage()), + ("Features", "Breakpoint", + "DrawerBreakpointLength: below the threshold the drawer switches to Overlay mode automatically. Resize the window or adjust the slider to see the layout switch in real time.", + () => new DrawerPageBreakpointPage()), + ("Features", "Events", + "Opened, Closing, and Closed drawer events plus NavigatedTo and NavigatedFrom page lifecycle events. Enable 'Cancel next close' to prevent the drawer from closing.", + () => new DrawerPageEventsPage()), + ("Features", "RTL Layout", "Right-to-left layout: drawer opens from the right edge with mirrored gestures.", + () => new DrawerPageRtlPage()), + + // Appearance + ("Appearance", "Customization", + "Customize drawer behavior, layout mode, length, colors, header and footer.", + () => new DrawerPageCustomizationPage()), + ("Appearance", "Custom Flyout", + "Dark overlay menu with staggered item animations and CrossFade page transitions on the detail NavigationPage.", + () => new DrawerPageCustomFlyoutPage()), + ("Appearance", "Transitions", + "Configure the detail NavigationPage transition. Choose CrossFade, PageSlide, or CompositePageTransition to animate detail page changes.", + () => new DrawerPageTransitionsPage()), + + // Performance + ("Performance", "Performance Monitor", + "Track detail page swaps, live page instances, and managed heap size. Observe how GC reclaims memory after swapping pages.", + () => new DrawerPagePerformancePage()), + + // Showcases + ("Showcases", "AvaloniaFlix", + "Streaming app with DrawerPage wrapping NavigationPage. Hamburger auto-injected at root, back arrow on detail, and dark themed flyout menu.", + () => new AvaloniaFlixAppPage()), + ("Showcases", "L'Avenir Restaurant", + "Restaurant app with DrawerPage as the root container, NavigationPage for detail navigation, and TabbedPage bottom tabs for Menu, Reservations, and Profile.", + () => new LAvenirAppPage()), + ("Showcases", "EcoTracker", + "Sustainability tracker with CompactInline drawer, eco leaf hamburger icon, crossfade compact/open menu transitions, and green-themed Home, Stats, Habits, and Community pages.", + () => new EcoTrackerAppPage()), + ("Showcases", "ModernApp", + "Travel social app using a top-placement DrawerPage. A slide-down nav pane gives access to Discover, My Trips, Profile, and Settings. Features destination cards, story circles, an experience feed, a stats profile, and a travel gallery.", + () => new ModernAppPage()), + ("Showcases", "Controls Gallery App", + "Controls gallery app using DrawerPage CompactInline mode. Dark Fluent palette, accent pill selection indicator, search box that fades in when open, expandable category groups, and Settings pinned to the footer.", + () => new ControlsGalleryAppPage()), + }; + + public DrawerDemoPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/ControlsGalleryAppPage.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ControlsGalleryAppPage.xaml new file mode 100644 index 000000000..e0ce3ca03 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ControlsGalleryAppPage.xaml @@ -0,0 +1,400 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/ControlsGalleryAppPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ControlsGalleryAppPage.xaml.cs new file mode 100644 index 000000000..6c771f307 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ControlsGalleryAppPage.xaml.cs @@ -0,0 +1,464 @@ +using System; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages; + +public partial class ControlsGalleryAppPage : UserControl +{ + static readonly Color Accent = Color.Parse("#60CDFF"); + static readonly Color ContentBg = Color.Parse("#141414"); + static readonly Color CardBg = Color.Parse("#1F1F1F"); + static readonly Color BorderCol = Color.Parse("#2EFFFFFF"); + static readonly Color TextCol = Color.Parse("#FFFFFF"); + static readonly Color TextSec = Color.Parse("#C8FFFFFF"); + static readonly Color TextMuted = Color.Parse("#80FFFFFF"); + + DrawerPage? _drawer; + NavigationPage? _detailNav; + Button? _selectedBtn; + TextBox? _searchBox; + ContentPage? _preSearchPage; + bool _isSearching; + + public ControlsGalleryAppPage() + { + InitializeComponent(); + + _drawer = this.FindControl("NavDrawer"); + _detailNav = this.FindControl("DetailNav"); + _selectedBtn = this.FindControl + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageBreakpointPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageBreakpointPage.xaml.cs new file mode 100644 index 000000000..0da2eb2ed --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageBreakpointPage.xaml.cs @@ -0,0 +1,84 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageBreakpointPage : UserControl + { + private bool _isLoaded; + + public DrawerPageBreakpointPage() + { + InitializeComponent(); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + _isLoaded = true; + DemoDrawer.PropertyChanged += OnDrawerPropertyChanged; + UpdateStatus(); + } + + protected override void OnUnloaded(RoutedEventArgs e) + { + base.OnUnloaded(e); + DemoDrawer.PropertyChanged -= OnDrawerPropertyChanged; + } + + private void OnDrawerPropertyChanged(object? sender, Avalonia.AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == DrawerPage.BoundsProperty) + UpdateStatus(); + } + + private void OnBreakpointChanged(object? sender, RangeBaseValueChangedEventArgs e) + { + if (!_isLoaded) + return; + var value = (int)e.NewValue; + DemoDrawer.DrawerBreakpointLength = value; + BreakpointText.Text = value.ToString(); + UpdateStatus(); + } + + private void OnLayoutChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerLayoutBehavior = LayoutCombo.SelectedIndex switch + { + 0 => DrawerLayoutBehavior.Split, + 1 => DrawerLayoutBehavior.CompactInline, + 2 => DrawerLayoutBehavior.CompactOverlay, + _ => DrawerLayoutBehavior.Split + }; + UpdateStatus(); + } + + private void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + if (!_isLoaded || sender is not Button button) + return; + var item = button.Tag?.ToString() ?? "Home"; + DetailTitleText.Text = item; + DetailPage.Header = item; + if (DemoDrawer.DrawerLayoutBehavior != DrawerLayoutBehavior.Split) + DemoDrawer.IsOpen = false; + } + + private void UpdateStatus() + { + var isVertical = DemoDrawer.DrawerPlacement == DrawerPlacement.Top || + DemoDrawer.DrawerPlacement == DrawerPlacement.Bottom; + var length = isVertical ? DemoDrawer.Bounds.Height : DemoDrawer.Bounds.Width; + var breakpoint = DemoDrawer.DrawerBreakpointLength; + WidthText.Text = $"{(isVertical ? "Height" : "Width")}: {(int)length} px"; + var isOverlay = breakpoint > 0 && length > 0 && length < breakpoint; + ModeText.Text = isOverlay ? + "Mode: Overlay (below breakpoint)" : + $"Mode: {DemoDrawer.DrawerLayoutBehavior} (above breakpoint)"; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml new file mode 100644 index 000000000..fc3497263 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml.cs new file mode 100644 index 000000000..b1d84fb5f --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml.cs @@ -0,0 +1,78 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageCompactPage : UserControl + { + private bool _isLoaded; + + public DrawerPageCompactPage() + { + InitializeComponent(); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + _isLoaded = true; + DemoDrawer.Opened += OnDrawerStatusChanged; + DemoDrawer.Closed += OnDrawerStatusChanged; + } + + protected override void OnUnloaded(RoutedEventArgs e) + { + base.OnUnloaded(e); + DemoDrawer.Opened -= OnDrawerStatusChanged; + DemoDrawer.Closed -= OnDrawerStatusChanged; + } + + private void OnDrawerStatusChanged(object? sender, System.EventArgs e) => UpdateStatus(); + + private void OnLayoutChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerLayoutBehavior = LayoutCombo.SelectedIndex switch + { + 0 => DrawerLayoutBehavior.CompactOverlay, + 1 => DrawerLayoutBehavior.CompactInline, + _ => DrawerLayoutBehavior.CompactOverlay + }; + } + + private void OnCompactLengthChanged(object? sender, RangeBaseValueChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.CompactDrawerLength = e.NewValue; + CompactLengthText.Text = ((int)e.NewValue).ToString(); + } + + private void OnDrawerLengthChanged(object? sender, RangeBaseValueChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerLength = e.NewValue; + DrawerLengthText.Text = ((int)e.NewValue).ToString(); + } + + private void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + if (sender is not Button button) + return; + var item = button.Tag?.ToString() ?? "Home"; + DetailTitleText.Text = item; + DetailPage.Header = item; + DemoDrawer.IsOpen = false; + } + + private void UpdateStatus() + { + StatusText.Text = $"Drawer: {(DemoDrawer.IsOpen ? "Open" : "Closed")}"; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml new file mode 100644 index 000000000..11f584c39 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml @@ -0,0 +1,268 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml.cs new file mode 100644 index 000000000..c05d92ecf --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml.cs @@ -0,0 +1,146 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Threading; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageCustomFlyoutPage : UserControl + { + private Ellipse? _bubble1; + private Ellipse? _bubble2; + private DispatcherTimer? _bubbleTimer; + private double _bubblePhase; + + public DrawerPageCustomFlyoutPage() + { + InitializeComponent(); + + _bubble1 = this.FindControl("Bubble1"); + _bubble2 = this.FindControl("Bubble2"); + + DrawerPageControl.PropertyChanged += (_, args) => + { + if (args.Property == DrawerPage.IsOpenProperty) + OnDrawerOpenChanged((bool)args.NewValue!); + }; + + _ = DetailNav.PushAsync(BuildDetailPage("Home"), null); + } + + private Control[] MenuItems => + new Control[] { MenuItem1, MenuItem2, MenuItem3, MenuItem4, MenuItem5, FooterRow }; + + private void OnDrawerOpenChanged(bool isOpen) + { + if (isOpen) + { + StartBubbles(); + + foreach (var item in MenuItems) + { + item.Opacity = 1.0; + if (item.RenderTransform is TranslateTransform tt) + tt.Y = 0; + } + } + else + { + StopBubbles(); + + foreach (var item in MenuItems) + { + var savedItemT = item.Transitions; + item.Transitions = null; + item.Opacity = 0.0; + item.Transitions = savedItemT; + + if (item.RenderTransform is TranslateTransform tt) + { + var savedTT = tt.Transitions; + tt.Transitions = null; + tt.Y = 25; + tt.Transitions = savedTT; + } + } + } + } + + private void StartBubbles() + { + if (_bubbleTimer != null) return; + _bubblePhase = 0; + _bubbleTimer = new DispatcherTimer(DispatcherPriority.Render) + { + Interval = TimeSpan.FromMilliseconds(16) + }; + _bubbleTimer.Tick += OnBubbleTick; + _bubbleTimer.Start(); + } + + private void StopBubbles() + { + if (_bubbleTimer == null) return; + _bubbleTimer.Stop(); + _bubbleTimer.Tick -= OnBubbleTick; + _bubbleTimer = null; + + if (_bubble1 != null) _bubble1.RenderTransform = null; + if (_bubble2 != null) _bubble2.RenderTransform = null; + } + + private void OnBubbleTick(object? sender, EventArgs e) + { + _bubblePhase += 0.012; + + if (_bubble1 != null) + _bubble1.RenderTransform = new TranslateTransform( + x: Math.Sin(_bubblePhase * 0.65) * 10, + y: Math.Sin(_bubblePhase) * 14); + + if (_bubble2 != null) + _bubble2.RenderTransform = new TranslateTransform( + x: Math.Sin(_bubblePhase * 0.45 + 1.8) * 7, + y: Math.Cos(_bubblePhase * 0.85 + 0.6) * 10); + } + + private async void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + if (sender is not Button button) return; + var tag = button.Tag?.ToString() ?? "Home"; + + DrawerPageControl.IsOpen = false; + + await DetailNav.ReplaceAsync(BuildDetailPage(tag), null); + } + + private static ContentPage BuildDetailPage(string section) + { + var (iconPath, body) = section switch + { + "Home" => + ("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z", + "Welcome back! Here is your dashboard with recent activity, quick actions, and personalized content."), + "Explore" => + ("M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5M12,2A7,7 0 0,0 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9A7,7 0 0,0 12,2Z", + "Discover new places, trending topics, and recommended content tailored to your interests."), + "Messages" => + ("M20,8L12,13L4,8V6L12,11L20,6M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4Z", + "Your conversations and notifications. Stay connected with the people who matter."), + "Profile" => + ("M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z", + "View and edit your profile, manage privacy settings, and control your account preferences."), + "Settings" => + ("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.04 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.68 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.04 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z", + "Configure application preferences, notifications, and privacy options."), + _ => ("M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z", "") + }; + + var page = NavigationDemoHelper.MakeSectionPage(section, iconPath, section, body, 0); + NavigationPage.SetHasNavigationBar(page, false); + return page; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml new file mode 100644 index 000000000..3adffed5f --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs new file mode 100644 index 000000000..d94825238 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs @@ -0,0 +1,326 @@ +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Input.GestureRecognizers; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageCustomizationPage : UserControl + { + private bool _isLoaded; + + private static readonly string[] _iconPaths = + { + // 0 - 3 lines (default hamburger) + "M3 17h18a1 1 0 0 1 .117 1.993L21 19H3a1 1 0 0 1-.117-1.993L3 17h18H3Zm0-6 18-.002a1 1 0 0 1 .117 1.993l-.117.007L3 13a1 1 0 0 1-.117-1.993L3 11l18-.002L3 11Zm0-6h18a1 1 0 0 1 .117 1.993L21 7H3a1 1 0 0 1-.117-1.993L3 5h18H3Z", + // 1 - 2 lines + "M3,13H21V11H3M3,6V8H21V6", + // 2 - 4 squares + "M3,11H11V3H3M3,21H11V13H3M13,21H21V13H13M13,3V11H21V3", + }; + + public DrawerPageCustomizationPage() + { + InitializeComponent(); + EnableMouseSwipeGesture(DemoDrawer); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + _isLoaded = true; + } + + private void OnToggleDrawer(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.IsOpen = !DemoDrawer.IsOpen; + } + + private void OnBehaviorChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerBehavior = BehaviorCombo.SelectedIndex switch + { + 0 => DrawerBehavior.Auto, + 1 => DrawerBehavior.Flyout, + 2 => DrawerBehavior.Locked, + 3 => DrawerBehavior.Disabled, + _ => DrawerBehavior.Auto + }; + } + + private void OnLayoutChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerLayoutBehavior = LayoutCombo.SelectedIndex switch + { + 0 => DrawerLayoutBehavior.Overlay, + 1 => DrawerLayoutBehavior.Split, + 2 => DrawerLayoutBehavior.CompactOverlay, + 3 => DrawerLayoutBehavior.CompactInline, + _ => DrawerLayoutBehavior.Overlay + }; + } + + private void OnPlacementChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerPlacement = PlacementCombo.SelectedIndex switch + { + 1 => DrawerPlacement.Right, + 2 => DrawerPlacement.Top, + 3 => DrawerPlacement.Bottom, + _ => DrawerPlacement.Left + }; + } + + private void OnGestureToggled(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + if (sender is CheckBox check) + DemoDrawer.IsGestureEnabled = check.IsChecked == true; + } + + private void OnDrawerLengthChanged(object? sender, RangeBaseValueChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerLength = e.NewValue; + DrawerLengthText.Text = ((int)e.NewValue).ToString(); + } + + private void OnDrawerBgChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerBackground = DrawerBgCombo.SelectedIndex switch + { + 1 => new SolidColorBrush(Colors.SlateBlue), + 2 => new SolidColorBrush(Colors.DarkCyan), + 3 => new SolidColorBrush(Colors.DarkRed), + 4 => new SolidColorBrush(Colors.DarkGreen), + _ => null + }; + } + + private void OnHeaderBgChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerHeaderBackground = HeaderBgCombo.SelectedIndex switch + { + 1 => new SolidColorBrush(Colors.DodgerBlue), + 2 => new SolidColorBrush(Colors.Orange), + 3 => new SolidColorBrush(Colors.Teal), + 4 => new SolidColorBrush(Colors.Purple), + _ => null + }; + } + + private void OnFooterBgChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerFooterBackground = FooterBgCombo.SelectedIndex switch + { + 1 => new SolidColorBrush(Colors.DimGray), + 2 => new SolidColorBrush(Colors.DarkSlateBlue), + 3 => new SolidColorBrush(Colors.DarkOliveGreen), + 4 => new SolidColorBrush(Colors.Maroon), + _ => null + }; + } + + private void OnIconChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerIcon = new PathIcon { Data = Geometry.Parse(_iconPaths[IconCombo.SelectedIndex]) }; + } + + private void OnBackdropChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.BackdropBrush = BackdropCombo.SelectedIndex switch + { + 1 => new SolidColorBrush(Color.FromArgb(102, 0, 0, 0)), + 2 => new SolidColorBrush(Color.FromArgb(179, 0, 0, 0)), + 3 => new SolidColorBrush(Color.FromArgb(102, 255, 255, 255)), + _ => null + }; + } + + private void OnShowHeaderToggled(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + if (ShowHeaderCheck.IsChecked == true) + DemoDrawer.DrawerHeader = HeaderTemplateCombo.SelectedIndex == 0 ? DrawerHeaderBorder : (object)"My Application"; + else + DemoDrawer.DrawerHeader = null; + } + + private void OnShowFooterToggled(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + if (ShowFooterCheck.IsChecked == true) + { + DemoDrawer.DrawerFooter = FooterTemplateCombo.SelectedIndex switch + { + 1 => (object)"v12.0", + 2 => (object)"Avalonia", + _ => DrawerFooterBorder + }; + } + else + { + DemoDrawer.DrawerFooter = null; + } + } + + private void OnHeaderTemplateChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + + switch (HeaderTemplateCombo.SelectedIndex) + { + case 1: + DemoDrawer.DrawerHeader = "My Application"; + DemoDrawer.DrawerHeaderTemplate = new FuncDataTemplate((data, _) => + new Border + { + Padding = new Avalonia.Thickness(16), + Child = new StackPanel + { + Spacing = 2, + Children = + { + new TextBlock { Text = data, FontSize = 18, FontWeight = FontWeight.SemiBold, Foreground = Brushes.White }, + new TextBlock { Text = "Navigation", FontSize = 12, Foreground = Brushes.White, Opacity = 0.7 } + } + } + }); + break; + + case 2: + DemoDrawer.DrawerHeader = "My Application"; + DemoDrawer.DrawerHeaderTemplate = new FuncDataTemplate((data, _) => + { + var initial = data?.Length > 0 ? data[0].ToString().ToUpperInvariant() : "?"; + var avatar = new Border + { + Width = 40, + Height = 40, + CornerRadius = new Avalonia.CornerRadius(20), + Background = new SolidColorBrush(Color.Parse("#1976D2")), + Child = new TextBlock + { + Text = initial, + FontSize = 18, + FontWeight = FontWeight.Bold, + Foreground = Brushes.White, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; + var label = new TextBlock { Text = data, FontSize = 14, FontWeight = FontWeight.SemiBold, VerticalAlignment = VerticalAlignment.Center }; + var row = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 10 }; + row.Children.Add(avatar); + row.Children.Add(label); + return new Border { Padding = new Avalonia.Thickness(12), Child = row }; + }); + break; + + default: + DemoDrawer.DrawerHeader = DrawerHeaderBorder; + DemoDrawer.DrawerHeaderTemplate = null; + break; + } + } + + private void OnFooterTemplateChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + + switch (FooterTemplateCombo.SelectedIndex) + { + case 1: + DemoDrawer.DrawerFooter = "v12.0"; + DemoDrawer.DrawerFooterTemplate = new FuncDataTemplate((data, _) => + new Border + { + Padding = new Avalonia.Thickness(12, 8), + Child = new Border + { + Padding = new Avalonia.Thickness(8, 4), + CornerRadius = new Avalonia.CornerRadius(4), + Background = new SolidColorBrush(Color.Parse("#1976D2")), + Child = new TextBlock { Text = data, FontSize = 11, Foreground = Brushes.White, FontWeight = FontWeight.SemiBold } + } + }); + break; + + case 2: + DemoDrawer.DrawerFooter = "Avalonia"; + DemoDrawer.DrawerFooterTemplate = new FuncDataTemplate((data, _) => + { + var icon = new PathIcon + { + Width = 14, + Height = 14, + Data = Geometry.Parse("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"), + Opacity = 0.5 + }; + var label = new TextBlock { Text = data, FontSize = 12, Opacity = 0.6, VerticalAlignment = VerticalAlignment.Center }; + var row = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 6 }; + row.Children.Add(icon); + row.Children.Add(label); + return new Border { Padding = new Avalonia.Thickness(14, 10), Child = row }; + }); + break; + + default: + DemoDrawer.DrawerFooter = DrawerFooterBorder; + DemoDrawer.DrawerFooterTemplate = null; + break; + } + } + + private void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + if (sender is not Button button) return; + var item = button.Tag?.ToString() ?? "Home"; + + DetailTitleText.Text = item; + + if (DemoDrawer.DrawerBehavior != DrawerBehavior.Locked) + DemoDrawer.IsOpen = false; + } + + private static void EnableMouseSwipeGesture(Control control) + { + var recognizer = control.GestureRecognizers + .OfType() + .FirstOrDefault(); + + if (recognizer is not null) + recognizer.IsMouseEnabled = true; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml new file mode 100644 index 000000000..92a205581 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml.cs new file mode 100644 index 000000000..02148dc46 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageEventsPage : UserControl + { + private readonly Dictionary _sectionPages; + + public DrawerPageEventsPage() + { + InitializeComponent(); + + _sectionPages = new Dictionary + { + ["Home"] = CreateSectionPage("Home"), + ["Profile"] = CreateSectionPage("Profile"), + ["Settings"] = CreateSectionPage("Settings"), + }; + + foreach (var (name, page) in _sectionPages) + { + var label = name; + page.NavigatedTo += (_, _) => Log($"{label}: NavigatedTo"); + page.NavigatedFrom += (_, _) => Log($"{label}: NavigatedFrom"); + } + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + DemoDrawer.Opened += OnDrawerOpened; + DemoDrawer.Closing += OnClosing; + DemoDrawer.Closed += OnDrawerClosed; + // Set Content here so the initial NavigatedTo events fire + // (VisualRoot is null in the constructor, which suppresses lifecycle events). + DemoDrawer.Content = _sectionPages["Home"]; + } + + protected override void OnUnloaded(RoutedEventArgs e) + { + base.OnUnloaded(e); + DemoDrawer.Opened -= OnDrawerOpened; + DemoDrawer.Closing -= OnClosing; + DemoDrawer.Closed -= OnDrawerClosed; + } + + private void OnDrawerOpened(object? sender, System.EventArgs e) => Log("Opened"); + private void OnDrawerClosed(object? sender, System.EventArgs e) => Log("Closed"); + + private void OnToggle(object? sender, RoutedEventArgs e) + { + DemoDrawer.IsOpen = !DemoDrawer.IsOpen; + } + + private void OnClosing(object? sender, DrawerClosingEventArgs e) + { + if (CancelCheck.IsChecked == true) + { + e.Cancel = true; + CancelCheck.IsChecked = false; + Log("Closing \u2192 cancelled"); + } + else + { + Log("Closing"); + } + } + + private void OnSelectSection(object? sender, RoutedEventArgs e) + { + if (sender is not Button btn) return; + var section = btn.Tag?.ToString() ?? "Home"; + + if (!_sectionPages.TryGetValue(section, out var page)) return; + if (ReferenceEquals(DemoDrawer.Content, page)) + { + DemoDrawer.IsOpen = false; + return; + } + + Log($"\u2192 {section}"); + DemoDrawer.Content = page; + DemoDrawer.IsOpen = false; + } + + private void OnClearLog(object? sender, RoutedEventArgs e) + { + EventLog.Text = string.Empty; + } + + private void Log(string message) + { + EventLog.Text = $"{message}\n{EventLog.Text}"; + } + + private static ContentPage CreateSectionPage(string header) => new ContentPage + { + Header = header, + Content = new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Spacing = 8, + Children = + { + new TextBlock + { + Text = header, + FontSize = 24, + FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center, + }, + new TextBlock + { + Text = "Tap a drawer item to navigate.\nWatch the event log in the panel.", + TextWrapping = TextWrapping.Wrap, + Opacity = 0.6, + TextAlignment = TextAlignment.Center, + FontSize = 13, + } + } + } + }; + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageFirstLookPage.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageFirstLookPage.xaml new file mode 100644 index 000000000..e257137ed --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageFirstLookPage.xaml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageRtlPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageRtlPage.xaml.cs new file mode 100644 index 000000000..36454ef8d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageRtlPage.xaml.cs @@ -0,0 +1,57 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageRtlPage : UserControl + { + public DrawerPageRtlPage() + { + InitializeComponent(); + } + + private void OnRtlToggled(object? sender, RoutedEventArgs e) + { + if (DemoDrawer == null) return; + DemoDrawer.FlowDirection = RtlCheckBox.IsChecked == true + ? FlowDirection.RightToLeft + : FlowDirection.LeftToRight; + } + + private void OnPlacementChanged(object? sender, SelectionChangedEventArgs e) + { + if (DemoDrawer == null) return; + DemoDrawer.DrawerPlacement = PlacementCombo.SelectedIndex switch + { + 0 => DrawerPlacement.Left, + 1 => DrawerPlacement.Right, + _ => DrawerPlacement.Left + }; + } + + private void OnToggleDrawer(object? sender, RoutedEventArgs e) + { + if (DemoDrawer == null) return; + DemoDrawer.IsOpen = !DemoDrawer.IsOpen; + } + + private void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + if (sender is not Button button) return; + var item = button.Tag?.ToString() ?? "Home"; + + DetailTitleText.Text = item; + DetailDescriptionText.Text = item switch + { + "Home" => "Toggle RTL to see the drawer flip to the right edge.\nGestures are mirrored: drag from right edge to open, drag right to close.", + "Profile" => "View and edit your profile information here.", + "Messages" => "Your messages and notifications appear here.", + "Settings" => "Configure application preferences and options.", + _ => $"Content for {item}" + }; + + DemoDrawer.IsOpen = false; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml new file mode 100644 index 000000000..9776163b6 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml.cs new file mode 100644 index 000000000..23634c170 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml.cs @@ -0,0 +1,94 @@ +using System; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageTransitionsPage : UserControl + { + private string _selectedTransition = "None"; + + public DrawerPageTransitionsPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + // Null out the default transition — OnTransitionChanged runs during init before DetailNav exists. + DetailNav.PageTransition = null; + await DetailNav.PushAsync(BuildPage("Home", _selectedTransition), null); + } + + private void OnTransitionChanged(object? sender, SelectionChangedEventArgs e) + { + if (DetailNav == null) return; + + _selectedTransition = TransitionCombo.SelectedIndex switch + { + 1 => "CrossFade", + 2 => "PageSlide (H)", + 3 => "PageSlide (V)", + 4 => "Composite (Slide + Fade)", + _ => "None" + }; + + DetailNav.PageTransition = TransitionCombo.SelectedIndex switch + { + 1 => new CrossFade(TimeSpan.FromMilliseconds(300)), + 2 => new PageSlide(TimeSpan.FromMilliseconds(300), PageSlide.SlideAxis.Horizontal), + 3 => new PageSlide(TimeSpan.FromMilliseconds(300), PageSlide.SlideAxis.Vertical), + 4 => new CompositePageTransition + { + PageTransitions = + { + new PageSlide(TimeSpan.FromMilliseconds(300), PageSlide.SlideAxis.Horizontal), + new CrossFade(TimeSpan.FromMilliseconds(300)) + } + }, + _ => null + }; + } + + private async void OnSectionClick(object? sender, RoutedEventArgs e) + { + if (sender is not Button button) return; + var section = button.Tag?.ToString() ?? "Home"; + + DemoDrawer.IsOpen = false; + + await DetailNav.ReplaceAsync(BuildPage(section, _selectedTransition)); + } + + private static ContentPage BuildPage(string section, string transitionName) + { + var (iconPath, body) = section switch + { + "Home" => + ("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z", + "Your dashboard with recent activity, quick actions, and personalized content."), + "Explore" => + ("M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5M12,2A7,7 0 0,0 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9A7,7 0 0,0 12,2Z", + "Discover new places, trending topics, and recommended content tailored to your interests."), + "Messages" => + ("M20,8L12,13L4,8V6L12,11L20,6M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4Z", + "Your conversations and notifications. Stay connected with the people who matter."), + "Profile" => + ("M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z", + "View and edit your profile, manage privacy settings, and control your account preferences."), + "Settings" => + ("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.04 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.68 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.04 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z", + "Configure application preferences, notifications, and privacy options."), + _ => ("M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z", "") + }; + + var page = NavigationDemoHelper.MakeSectionPage(section, iconPath, section, body, 0, $"Transition: {transitionName}"); + NavigationPage.SetHasNavigationBar(page, false); + return page; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml new file mode 100644 index 000000000..22320fbc8 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml @@ -0,0 +1,302 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml.cs new file mode 100644 index 000000000..49b13b4d9 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml.cs @@ -0,0 +1,319 @@ +using System; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages; + +public partial class EcoTrackerAppPage : UserControl +{ + static readonly Color Primary = Color.Parse("#2E7D32"); + static readonly Color Accent = Color.Parse("#4CAF50"); + static readonly Color BgLight = Color.Parse("#F1F8E9"); + static readonly Color TextDark = Color.Parse("#1A2E1C"); + static readonly Color TextMuted = Color.Parse("#90A4AE"); + + const string LeafPath = + "M12 3C9 6 6 9 6 13C6 17.4 8.7 21 12 22C15.3 21 18 17.4 18 13C18 9 15 6 12 3Z"; + + NavigationPage? _navPage; + DrawerPage? _drawerPage; + ScrollViewer? _infoPanel; + Button? _selectedBtn; + + public EcoTrackerAppPage() + { + InitializeComponent(); + + _infoPanel = this.FindControl("InfoPanel"); + _navPage = this.FindControl("NavPage"); + _drawerPage = this.FindControl("DrawerPageControl"); + _selectedBtn = this.FindControl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerHomeView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerHomeView.xaml.cs new file mode 100644 index 000000000..3f36a4d48 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerHomeView.xaml.cs @@ -0,0 +1,14 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages; + +public partial class EcoTrackerHomeView : UserControl +{ + public Action? TreeDetailRequested { get; set; } + + public EcoTrackerHomeView() => InitializeComponent(); + + void OnHeroClick(object? sender, RoutedEventArgs e) => TreeDetailRequested?.Invoke(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml new file mode 100644 index 000000000..82a5e938d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml.cs new file mode 100644 index 000000000..90aa02066 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class EcoTrackerStatsView : UserControl +{ + public EcoTrackerStatsView() => InitializeComponent(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernAppPage.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernAppPage.xaml new file mode 100644 index 000000000..77a0e3299 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernAppPage.xaml @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernAppPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernAppPage.xaml.cs new file mode 100644 index 000000000..f0d4d7d62 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernAppPage.xaml.cs @@ -0,0 +1,118 @@ +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages; + +public partial class ModernAppPage : UserControl +{ + // Palette + static readonly Color Primary = Color.Parse("#0dccf2"); + static readonly Color BgLight = Color.Parse("#f5f8f8"); + + static IBrush BgBrush => new SolidColorBrush(BgLight); + + DrawerPage? _drawerPage; + NavigationPage? _navPage; + ScrollViewer? _infoPanel; + TextBlock? _pageTitle; + Button? _selectedNavBtn; + + public ModernAppPage() + { + InitializeComponent(); + + _infoPanel = this.FindControl("InfoPanel"); + _drawerPage = this.FindControl("DrawerPageControl"); + _navPage = this.FindControl("NavPage"); + _pageTitle = this.FindControl("PageTitle"); + + if (_navPage != null) + NavigateToDiscover(); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + UpdateInfoPanelVisibility(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == BoundsProperty) + UpdateInfoPanelVisibility(); + } + + void UpdateInfoPanelVisibility() + { + if (_infoPanel != null) + _infoPanel.IsVisible = Bounds.Width >= 640; + } + + void OnNavClick(object? sender, RoutedEventArgs e) + { + if (sender is not Button btn) return; + SelectNavButton(btn); + _drawerPage!.IsOpen = false; + switch (btn.Tag?.ToString()) + { + case "Discover": NavigateToDiscover(); break; + case "MyTrips": NavigateToMyTrips(); break; + case "Profile": NavigateToProfile(); break; + case "Settings": NavigateToSettings(); break; + } + } + + void OnCloseDrawer(object? sender, RoutedEventArgs e) + { + if (_drawerPage != null) _drawerPage.IsOpen = false; + } + + void SelectNavButton(Button btn) + { + if (_selectedNavBtn != null) + _selectedNavBtn.Background = Brushes.Transparent; + _selectedNavBtn = btn; + btn.Background = new SolidColorBrush(Color.Parse("#1A0dccf2")); + } + + async Task Navigate(ContentPage page) + { + if (_navPage == null) return; + NavigationPage.SetHasBackButton(page, false); + NavigationPage.SetHasNavigationBar(page, false); + await _navPage.PopToRootAsync(); + await _navPage.PushAsync(page); + } + + async void NavigateToDiscover() + { + if (_pageTitle != null) _pageTitle.Text = "Discover"; + SelectNavButton(this.FindControl + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernProfileView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernProfileView.xaml.cs new file mode 100644 index 000000000..ae2cdb945 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernProfileView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class ModernProfileView : UserControl +{ + public ModernProfileView() => InitializeComponent(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml new file mode 100644 index 000000000..a4ee8cd8b --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml.cs new file mode 100644 index 000000000..d50ade3dd --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class ModernSettingsView : UserControl +{ + public ModernSettingsView() => InitializeComponent(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/ExpanderPage.xaml b/samples/AvaloniaControlCatalog/Pages/ExpanderPage.xaml new file mode 100644 index 000000000..322da30a6 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ExpanderPage.xaml @@ -0,0 +1,76 @@ + + + Expands to show nested content + + + + + Expanded content + + + + + Expanded content + + + + + Expanded content + + + + + Expanded content + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/FlyoutsPage.axaml.cs b/samples/AvaloniaControlCatalog/Pages/FlyoutsPage.axaml.cs new file mode 100644 index 000000000..d6f54df07 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/FlyoutsPage.axaml.cs @@ -0,0 +1,94 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class FlyoutsPage : ContentPage + { + public FlyoutsPage() + { + InitializeComponent(); + + AttachedFlyoutPanel.DoubleTapped += Afp_DoubleTapped; + + SetXamlTexts(); + } + + private void Afp_DoubleTapped(object? sender, RoutedEventArgs e) + { + if (sender is Panel p) + { + FlyoutBase.ShowAttachedFlyout(p); + } + } + + private void SetXamlTexts() + { + var bfxt = ButtonFlyoutXamlText; + bfxt.Text = ""; + + var mfxt = this.MenuFlyoutXamlText; + mfxt.Text = ""; + + var afxt = this.AttachedFlyoutXamlText; + afxt.Text = "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n\n In DoubleTapped handler:\n" + + "FlyoutBase.ShowAttachedFlyout(AttachedFlyoutPanel);"; + + var sfxt = this.SharedFlyoutXamlText; + sfxt.Text = "Declare a flyout in Resources:\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n\n\n" + + "Then attach the flyout where you want it:\n" + + " + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/FocusPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/FocusPage.xaml.cs new file mode 100644 index 000000000..18aae471a --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/FocusPage.xaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages; + +public partial class FocusPage : ContentPage +{ + public FocusPage() + { + AvaloniaXamlLoader.Load(this); + } +} + diff --git a/samples/AvaloniaControlCatalog/Pages/GesturePage.xaml b/samples/AvaloniaControlCatalog/Pages/GesturePage.xaml new file mode 100644 index 000000000..da9ed5f80 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/GesturePage.xaml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/GesturePage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/GesturePage.xaml.cs new file mode 100644 index 000000000..56c09573d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/GesturePage.xaml.cs @@ -0,0 +1,39 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class GesturePage : ContentPage + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = + { + ("Touch / Pen", "Pull Gesture", + "Press and drag from colored border zones. A green ball tracks the pull delta and springs back on release.", + () => new GesturePullPage()), + + ("Multi Touch", "Pinch / Zoom", + "Pinch to scale an image using composition visuals. Scroll to pan when zoomed in.", + () => new GesturePinchZoomPage()), + + ("Multi Touch", "Pinch / Rotation", + "Pinch to rotate a rectangle. The Angle property from the pinch event drives a RotateTransform.", + () => new GesturePinchRotationPage()), + + ("Touch / Pen / Mouse", "Swipe Gesture", + "Swipe horizontally or vertically. Configure direction, threshold, and mouse support. Shows live delta, velocity, and direction.", + () => new GestureSwipePage()), + }; + + public GesturePage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchRotationPage.xaml b/samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchRotationPage.xaml new file mode 100644 index 000000000..b46af74c3 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchRotationPage.xaml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchRotationPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchRotationPage.xaml.cs new file mode 100644 index 000000000..55d5e619e --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchRotationPage.xaml.cs @@ -0,0 +1,33 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; + +namespace ControlCatalog.Pages +{ + public partial class GesturePinchRotationPage : UserControl + { + private bool _isInit; + + public GesturePinchRotationPage() + { + InitializeComponent(); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + if (_isInit) + { + return; + } + + _isInit = true; + + RotationGesture.AddHandler(InputElement.PinchEvent, (s, e) => + { + AngleSlider.Value = e.Angle; + }); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchZoomPage.xaml b/samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchZoomPage.xaml new file mode 100644 index 000000000..6932741e5 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchZoomPage.xaml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/LabelsPage.axaml.cs b/samples/AvaloniaControlCatalog/Pages/LabelsPage.axaml.cs new file mode 100644 index 000000000..b26112b35 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/LabelsPage.axaml.cs @@ -0,0 +1,35 @@ +using Avalonia.Controls; +using ControlCatalog.Models; + +namespace ControlCatalog.Pages +{ + public partial class LabelsPage : ContentPage + { + private Person? _person; + + public LabelsPage() + { + CreateDefaultPerson(); + this.InitializeComponent(); + } + + private void CreateDefaultPerson() + { + DataContext = _person = new Person + { + FirstName = "John", + LastName = "Doe", + IsBanned = true, + }; + } + + public void DoSave() + { + + } + public void DoCancel() + { + CreateDefaultPerson(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/LayoutTransformControlPage.xaml b/samples/AvaloniaControlCatalog/Pages/LayoutTransformControlPage.xaml new file mode 100644 index 000000000..d12bbfc30 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/LayoutTransformControlPage.xaml @@ -0,0 +1,28 @@ + + + + Rotation + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/LayoutTransformControlPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/LayoutTransformControlPage.xaml.cs new file mode 100644 index 000000000..e3815238b --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/LayoutTransformControlPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class LayoutTransformControlPage : ContentPage + { + public LayoutTransformControlPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/ListBoxPage.xaml b/samples/AvaloniaControlCatalog/Pages/ListBoxPage.xaml new file mode 100644 index 000000000..f73a4c18e --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ListBoxPage.xaml @@ -0,0 +1,51 @@ + + + + + + + + + + Hosts a collection of ListBoxItem. + Each 5th item is highlighted with nth-child(5n+3) and nth-last-child(5n+4) rules. + + + Multiple + Toggle + AlwaysSelected + AutoScrollToSelectedItem + WrapSelection + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/ListBoxPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/ListBoxPage.xaml.cs new file mode 100644 index 000000000..208b5cce4 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ListBoxPage.xaml.cs @@ -0,0 +1,14 @@ +using Avalonia.Controls; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public partial class ListBoxPage : ContentPage + { + public ListBoxPage() + { + InitializeComponent(); + DataContext = new ListBoxPageViewModel(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/MenuPage.xaml b/samples/AvaloniaControlCatalog/Pages/MenuPage.xaml new file mode 100644 index 000000000..d9fb5d81b --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/MenuPage.xaml @@ -0,0 +1,99 @@ + + + Exported menu fallback + (Should be only visible on platforms without desktop-global menu bar) + + A window menu + + + + Defined in XAML + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dyanamically generated + + + + + + + + + Mixed + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/MenuPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/MenuPage.xaml.cs new file mode 100644 index 000000000..56a229a59 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/MenuPage.xaml.cs @@ -0,0 +1,28 @@ +using System; +using Avalonia.Controls; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public partial class MenuPage : ContentPage + { + public MenuPage() + { + InitializeComponent(); + DataContext = new MenuPageViewModel(); + } + + private MenuPageViewModel? _model; + protected override void OnDataContextChanged(EventArgs e) + { + if (_model != null) + _model.View = null; + _model = DataContext as MenuPageViewModel; + if (_model != null) + _model.View = this; + + base.OnDataContextChanged(e); + } + + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NativeEmbedPage.xaml b/samples/AvaloniaControlCatalog/Pages/NativeEmbedPage.xaml new file mode 100644 index 000000000..71d622bae --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NativeEmbedPage.xaml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + Text + + + Tooltip + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NativeEmbedPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NativeEmbedPage.xaml.cs new file mode 100644 index 000000000..1cd1f78cc --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NativeEmbedPage.xaml.cs @@ -0,0 +1,77 @@ +using System; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Platform; + +namespace ControlCatalog.Pages +{ + public partial class NativeEmbedPage : ContentPage + { + public NativeEmbedPage() + { + InitializeComponent(); + } + + public async void ShowPopupDelay(object sender, RoutedEventArgs args) + { + await Task.Delay(3000); + ShowPopup(sender, args); + } + + public void ShowPopup(object sender, RoutedEventArgs args) + { + new ContextMenu() + { + Items = + { + new MenuItem() { Header = "Test" }, new MenuItem() { Header = "Test" } + } + }.Open((Control)sender); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == BoundsProperty) + { + var isMobile = change.GetNewValue().Width < 1200; + FirstPanel.Classes.Set("mobile", isMobile); + SecondPanel.Classes.Set("mobile", isMobile); + } + } + } + + public class EmbedSample : NativeControlHost + { + public static INativeDemoControl? Implementation { get; set; } + + static EmbedSample() + { + + } + + public bool IsSecond { get; set; } + + protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent) + { + return Implementation?.CreateControl(IsSecond, parent, () => base.CreateNativeControlCore(parent)) + ?? base.CreateNativeControlCore(parent); + } + + protected override void DestroyNativeControlCore(IPlatformHandle control) + { + base.DestroyNativeControlCore(control); + } + } + + public interface INativeDemoControl + { + /// Used to specify which control should be displayed as a demo + /// + /// + IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func createDefault); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationDemoHelper.cs b/samples/AvaloniaControlCatalog/Pages/NavigationDemoHelper.cs new file mode 100644 index 000000000..22e52b6fd --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationDemoHelper.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + /// + /// Shared helpers for ControlCatalog demo pages. + /// + internal static class NavigationDemoHelper + { + /// + /// Pastel background brushes cycled by page index. + /// + internal static readonly IBrush[] PageBrushes = + { + new SolidColorBrush(Color.Parse("#BBDEFB")), + new SolidColorBrush(Color.Parse("#C8E6C9")), + new SolidColorBrush(Color.Parse("#FFE0B2")), + new SolidColorBrush(Color.Parse("#E1BEE7")), + new SolidColorBrush(Color.Parse("#FFCDD2")), + new SolidColorBrush(Color.Parse("#B2EBF2")), + }; + + internal static IBrush GetPageBrush(int index) => + PageBrushes[((index % PageBrushes.Length) + PageBrushes.Length) % PageBrushes.Length]; + + /// + /// Creates a simple demo ContentPage with a centered title and subtitle. + /// + internal static ContentPage MakePage(string header, string body, int colorIndex) => + new ContentPage + { + Header = header, + Background = GetPageBrush(colorIndex), + Content = new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Spacing = 8, + Children = + { + new TextBlock + { + Text = header, + FontSize = 20, + FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center + }, + new TextBlock + { + Text = body, + FontSize = 13, + Opacity = 0.7, + TextWrapping = TextWrapping.Wrap, + TextAlignment = TextAlignment.Center, + MaxWidth = 260 + } + } + }, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + + /// + /// Creates a demo ContentPage with an icon, title, body, and hint text + /// (used by DrawerPage detail pages). + /// + internal static ContentPage MakeSectionPage( + string header, string iconData, string title, string body, + int colorIndex, string? hint = null) + { + var panel = new StackPanel { Margin = new Thickness(24, 20), Spacing = 12 }; + + panel.Children.Add(new PathIcon + { + Width = 48, + Height = 48, + Data = Geometry.Parse(iconData), + Foreground = new SolidColorBrush(Color.Parse("#0078D4")) + }); + panel.Children.Add(new TextBlock + { + Text = title, + FontSize = 26, + FontWeight = FontWeight.Bold + }); + panel.Children.Add(new TextBlock + { + Text = body, + FontSize = 14, + Opacity = 0.8, + TextWrapping = TextWrapping.Wrap + }); + panel.Children.Add(new Separator { Margin = new Thickness(0, 8) }); + + if (hint != null) + { + panel.Children.Add(new TextBlock + { + Text = hint, + FontSize = 12, + Opacity = 0.45, + FontStyle = FontStyle.Italic, + TextWrapping = TextWrapping.Wrap + }); + } + + return new ContentPage + { + Header = header, + Background = GetPageBrush(colorIndex), + Content = new ScrollViewer { Content = panel }, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + } + + private static readonly Geometry CloseIcon = Geometry.Parse( + "M4.397 4.397a1 1 0 0 1 1.414 0L12 10.585l6.19-6.188a1 1 0 0 1 1.414 1.414L13.413 12l6.19 6.189a1 1 0 0 1-1.414 1.414L12 13.413l-6.189 6.19a1 1 0 0 1-1.414-1.414L10.585 12 4.397 5.811a1 1 0 0 1 0-1.414z"); + + /// + /// Builds the demo gallery home page for NavigationPage/TabbedPage/DrawerPage demo registries. + /// + internal static ContentPage CreateGalleryHomePage( + NavigationPage nav, + (string Group, string Title, string Description, Func Factory)[] demos) + { + var stack = new StackPanel { Margin = new Thickness(12), Spacing = 16 }; + + var groups = new Dictionary(); + var groupOrder = new List(); + + foreach (var (group, title, description, factory) in demos) + { + if (!groups.ContainsKey(group)) + { + groups[group] = new WrapPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Left + }; + groupOrder.Add(group); + } + + var demoFactory = factory; + var demoTitle = title; + + var card = new Button + { + Width = 170, + MinHeight = 80, + Margin = new Thickness(0, 0, 8, 8), + VerticalAlignment = VerticalAlignment.Top, + HorizontalContentAlignment = HorizontalAlignment.Left, + VerticalContentAlignment = VerticalAlignment.Top, + Padding = new Thickness(12, 8), + Content = new StackPanel + { + Spacing = 4, + Children = + { + new TextBlock + { + Text = title, + FontSize = 13, + FontWeight = FontWeight.SemiBold, + TextWrapping = TextWrapping.Wrap + }, + new TextBlock + { + Text = description, + FontSize = 11, + Opacity = 0.6, + TextWrapping = TextWrapping.Wrap + } + } + } + }; + + card.Click += async (_, _) => + { + var headerGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("*, Auto") }; + headerGrid.Children.Add(new TextBlock + { + Text = demoTitle, + VerticalAlignment = VerticalAlignment.Center + }); + var closeBtn = new Button + { + Content = new PathIcon { Data = CloseIcon }, + Background = Brushes.Transparent, + BorderThickness = new Thickness(0), + Padding = new Thickness(8, 4), + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(closeBtn, 1); + headerGrid.Children.Add(closeBtn); + closeBtn.Click += async (_, _) => await nav.PopAsync(null); + + var page = new ContentPage + { + Header = headerGrid, + Content = demoFactory(), + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + NavigationPage.SetHasBackButton(page, false); + await nav.PushAsync(page, null); + }; + + groups[group].Children.Add(card); + } + + foreach (var groupName in groupOrder) + { + stack.Children.Add(new TextBlock + { + Text = groupName, + FontSize = 13, + FontWeight = FontWeight.SemiBold, + Margin = new Thickness(0, 0, 0, 4), + Opacity = 0.6 + }); + stack.Children.Add(groups[groupName]); + } + + var homePage = new ContentPage + { + Content = new ScrollViewer { Content = stack }, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + NavigationPage.SetHasNavigationBar(homePage, false); + return homePage; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationDemoPage.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationDemoPage.xaml new file mode 100644 index 000000000..0f7536b6a --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationDemoPage.xaml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationDemoPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationDemoPage.xaml.cs new file mode 100644 index 000000000..7ca41ab89 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationDemoPage.xaml.cs @@ -0,0 +1,94 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class NavigationDemoPage : ContentPage + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = + { + // Overview + ("Overview", "First Look", "Basic NavigationPage with push/pop navigation and back button support.", + () => new NavigationPageFirstLookPage()), + ("Overview", "Modal Navigation", "Push and pop modal pages that appear on top of the navigation stack.", + () => new NavigationPageModalPage()), + ("Overview", "Navigation Events", + "Subscribe to Pushed, Popped, PoppedToRoot, ModalPushed, and ModalPopped events.", + () => new NavigationPageEventsPage()), + + // Appearance + ("Appearance", "Bar Customization", + "Customize the navigation bar background, foreground, shadow, and visibility.", + () => new NavigationPageAppearancePage()), + ("Appearance", "Header", + "Set page header content: a string, icon, or any custom control in the navigation bar.", + () => new NavigationPageTitlePage()), + + // Data + ("Data", "Pass Data", "Pass data during navigation via constructor arguments or DataContext.", + () => new NavigationPagePassDataPage()), + ("Data", "MVVM Navigation", + "Keep navigation decisions in view models by routing NavigationPage push and pop operations through a small INavigationService.", + () => new NavigationPageMvvmPage()), + + // Features + ("Features", "Attached Methods", + "Per-page navigation bar and back button control via static attached methods.", + () => new NavigationPageAttachedMethodsPage()), + ("Features", "Back Button", "Customize, hide, or intercept the back button.", + () => new NavigationPageBackButtonPage()), + ("Features", "CommandBar", + "Add, remove and position CommandBar items inside the navigation bar or as a bottom bar.", + () => new NavigationPageToolbarPage()), + ("Features", "Transitions", + "Configure page transitions: PageSlide, Parallax Slide, CrossFade, Fade Through, and more.", + () => new NavigationPageTransitionsPage()), + ("Features", "Modal Transitions", "Configure modal transition: PageSlide from bottom, CrossFade, or None.", + () => new NavigationPageModalTransitionsPage()), + ("Features", "Stack Management", "Remove or insert pages anywhere in the navigation stack at runtime.", + () => new NavigationPageStackPage()), + ("Features", "Interactive Header", + "Build a header with a title and live search box that filters page content in real time.", + () => new NavigationPageInteractiveHeaderPage()), + ("Features", "Back Swipe Gesture", "Swipe from the left edge to interactively pop the current page.", + () => new NavigationPageGesturePage()), + ("Features", "Scroll-Aware Bar", + "Hide the navigation bar on downward scroll and reveal it on upward scroll.", + () => new NavigationPageScrollAwarePage()), + + // Performance + ("Performance", "Performance Monitor", + "Track stack depth, live page instances, and managed heap size. Observe how memory is reclaimed after popping pages.", + () => new NavigationPagePerformancePage()), + + // Showcases + ("Showcases", "Pulse Fitness", + "Login flow with RemovePage, TabbedPage dashboard with bottom tabs, and NavigationPage push for workout detail.", + () => new PulseAppPage()), + ("Showcases", "L'Avenir", + "Restaurant app with DrawerPage flyout menu, TabbedPage bottom tabs, and NavigationPage push for dish detail.", + () => new LAvenirAppPage()), + ("Showcases", "AvaloniaFlix", + "Streaming app with dark NavigationPage, hidden nav bar on home, and custom bar tint on movie detail pages.", + () => new AvaloniaFlixAppPage()), + ("Showcases", "Retro Gaming", + "Arcade-style app with NavigationPage header, TabbedPage bottom tabs with CenteredTabPanel, and game detail push.", + () => new RetroGamingAppPage()), + ("Showcases", "Curved Header", + "Shop app with dome-bottomed white header on home (nav bar hidden) and blue curved header on detail (BarLayoutBehavior.Overlay).", + () => new NavigationPageCurvedHeaderPage()), + }; + + public NavigationDemoPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml new file mode 100644 index 000000000..6e209c8bb --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml @@ -0,0 +1,258 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml.cs new file mode 100644 index 000000000..a2ae34dd1 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml.cs @@ -0,0 +1,191 @@ +using System; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages; + +public partial class AvaloniaFlixAppPage : UserControl +{ + NavigationPage? _detailNav; + ScrollViewer? _infoPanel; + + public AvaloniaFlixAppPage() + { + InitializeComponent(); + + _detailNav = this.FindControl("DetailNav"); + if (_detailNav != null) + { + _detailNav.ModalTransition = new PageSlide(TimeSpan.FromMilliseconds(300), PageSlide.SlideAxis.Vertical); + + var homeView = new AvaloniaFlixHomeView(); + homeView.MovieSelected = title => PushDetailPage(title); + homeView.SearchRequested = () => _ = PushSearchPageAsync(); + + var homePage = new ContentPage + { + Content = homeView, + Background = Brushes.Transparent, + Header = BuildHomeHeader(), + }; + NavigationPage.SetTopCommandBar(homePage, BuildHomeCommandBar()); + _ = _detailNav.PushAsync(homePage); + } + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + _infoPanel = this.FindControl("InfoPanel"); + UpdateInfoPanelVisibility(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == BoundsProperty) + UpdateInfoPanelVisibility(); + } + + void UpdateInfoPanelVisibility() + { + if (_infoPanel != null) + _infoPanel.IsVisible = Bounds.Width >= 650; + } + + TextBlock BuildHomeHeader() => new TextBlock + { + Text = "AVALONIAFLIX", + Foreground = new SolidColorBrush(Color.Parse("#E50914")), + FontSize = 18, + FontWeight = Avalonia.Media.FontWeight.Black, + VerticalAlignment = VerticalAlignment.Center, + }; + + StackPanel BuildHomeCommandBar() + { + var cmdBar = new StackPanel + { + Orientation = Orientation.Horizontal, + Spacing = 12, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0), + }; + var searchBtn = new Button { Padding = new Thickness(4) }; + searchBtn.Classes.Add("flixTransparent"); + searchBtn.Click += OnSearchClick; + searchBtn.Content = new PathIcon + { + Width = 20, Height = 20, Foreground = Brushes.White, + Data = Avalonia.Media.Geometry.Parse("M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"), + }; + cmdBar.Children.Add(searchBtn); + cmdBar.Children.Add(new Border + { + Width = 30, Height = 30, CornerRadius = new CornerRadius(4), + Background = new SolidColorBrush(Color.Parse("#333333")), + Child = new TextBlock + { + Text = "JD", FontSize = 10, FontWeight = Avalonia.Media.FontWeight.Bold, + Foreground = Brushes.White, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + }, + }); + return cmdBar; + } + + async void PushDetailPage(string title) + { + if (_detailNav == null) return; + + var detailView = new AvaloniaFlixDetailView(title); + + var headerTitle = new TextBlock + { + Text = title, FontSize = 17, FontWeight = Avalonia.Media.FontWeight.Bold, + Foreground = Brushes.White, VerticalAlignment = VerticalAlignment.Center, + }; + + var shareBtnContent = new PathIcon + { + Width = 20, Height = 20, Foreground = Brushes.White, + Data = Avalonia.Media.Geometry.Parse("M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.35C15.11,18.56 15.08,18.78 15.08,19C15.08,20.61 16.39,21.92 18,21.92C19.61,21.92 20.92,20.61 20.92,19C20.92,17.39 19.61,16.08 18,16.08Z"), + }; + var shareBtn = new Button { Padding = new Thickness(8), Content = shareBtnContent }; + shareBtn.Classes.Add("flixTransparent"); + + var bookmarkBtnContent = new PathIcon + { + Width = 20, Height = 20, Foreground = Brushes.White, + Data = Avalonia.Media.Geometry.Parse("M17,3H7A2,2 0 0,0 5,5V21L12,18L19,21V5C19,3.89 18.1,3 17,3Z"), + }; + var bookmarkBtn = new Button { Padding = new Thickness(8), Content = bookmarkBtnContent }; + bookmarkBtn.Classes.Add("flixTransparent"); + + var detailCmdBar = new StackPanel + { + Orientation = Orientation.Horizontal, Spacing = 8, + VerticalAlignment = VerticalAlignment.Center, + }; + detailCmdBar.Children.Add(shareBtn); + detailCmdBar.Children.Add(bookmarkBtn); + + var detailPage = new ContentPage + { + Content = detailView, + Background = Brushes.Transparent, + Header = headerTitle, + }; + NavigationPage.SetTopCommandBar(detailPage, detailCmdBar); + + await _detailNav.PushAsync(detailPage); + + var drawer = this.FindControl("DrawerPageControl"); + if (drawer is { IsOpen: true }) + drawer.IsOpen = false; + } + + async void OnSearchClick(object? sender, RoutedEventArgs e) + { + await PushSearchPageAsync(); + } + + async Task PushSearchPageAsync() + { + if (_detailNav == null) return; + + var searchView = new AvaloniaFlixSearchView(); + searchView.CloseRequested = async () => await (_detailNav?.PopModalAsync() ?? Task.CompletedTask); + searchView.MovieSelected = async title => + { + if (_detailNav != null && _detailNav.ModalStack.Count > 0) + await _detailNav.PopModalAsync(); + PushDetailPage(title); + }; + + var searchPage = new ContentPage + { + Content = searchView, + Background = new SolidColorBrush(Color.Parse("#0A0A0A")), + }; + NavigationPage.SetHasNavigationBar(searchPage, false); + + await (_detailNav?.PushModalAsync(searchPage) ?? Task.CompletedTask); + } + + void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + var drawer = this.FindControl("DrawerPageControl"); + if (drawer != null) + drawer.IsOpen = false; + + _ = _detailNav?.PopToRootAsync(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml new file mode 100644 index 000000000..e6f530669 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml @@ -0,0 +1,332 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml.cs new file mode 100644 index 000000000..659fea52e --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml.cs @@ -0,0 +1,60 @@ +using System; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace ControlCatalog.Pages; + +public partial class AvaloniaFlixDetailView : UserControl +{ + static readonly string[] MovieAssets = + { + "avares://ControlCatalog/Assets/Movies/trending1.jpg", + "avares://ControlCatalog/Assets/Movies/trending2.jpg", + "avares://ControlCatalog/Assets/Movies/toprated1.jpg", + "avares://ControlCatalog/Assets/Movies/toprated2.jpg", + "avares://ControlCatalog/Assets/Movies/toprated3.jpg", + "avares://ControlCatalog/Assets/Movies/toprated4.jpg", + "avares://ControlCatalog/Assets/Movies/continue1.jpg", + "avares://ControlCatalog/Assets/Movies/morelike1.jpg", + "avares://ControlCatalog/Assets/Movies/search1.jpg", + "avares://ControlCatalog/Assets/Movies/hero.jpg", + "avares://ControlCatalog/Assets/Movies/cast1.jpg", + "avares://ControlCatalog/Assets/Movies/cast2.jpg", + }; + + public AvaloniaFlixDetailView() => InitializeComponent(); + + public AvaloniaFlixDetailView(string movieTitle) + { + InitializeComponent(); + + HeroTitleLabel.Text = movieTitle; + + var rng = new Random(movieTitle.GetHashCode()); + int imgIdx = Math.Abs(movieTitle.GetHashCode()) % MovieAssets.Length; + + string year = (2020 + rng.Next(6)).ToString(); + string rating = $"{6.5 + rng.NextDouble() * 3.0:F1}/10"; + int mins = 90 + rng.Next(60); + string duration = $"{mins / 60}h {mins % 60}m"; + + YearLabel.Text = year; + RatingLabel.Text = rating; + DurationLabel.Text = duration; + + try + { + var uri = new Uri(MovieAssets[imgIdx]); + HeroBg.Background = new ImageBrush(new Bitmap(AssetLoader.Open(uri))) + { + Stretch = Stretch.UniformToFill, + }; + } + catch + { + HeroBg.Background = new SolidColorBrush(Color.Parse("#111111")); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml new file mode 100644 index 000000000..4f186e910 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml @@ -0,0 +1,630 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml.cs new file mode 100644 index 000000000..81702bf69 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml.cs @@ -0,0 +1,21 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages; + +public partial class AvaloniaFlixHomeView : UserControl +{ + public Action? MovieSelected { get; set; } + public Action? SearchRequested { get; set; } + + public AvaloniaFlixHomeView() => InitializeComponent(); + + void OnMovieClick(object? sender, RoutedEventArgs e) + { + string title = "Cyber Dune"; + if (sender is Button btn && btn.Tag is string tag) + title = tag; + MovieSelected?.Invoke(title); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml new file mode 100644 index 000000000..d4c536521 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml @@ -0,0 +1,302 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml.cs new file mode 100644 index 000000000..aac3069be --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages; + +public partial class AvaloniaFlixSearchView : UserControl +{ + public Action? CloseRequested { get; set; } + public Action? MovieSelected { get; set; } + + public AvaloniaFlixSearchView() => InitializeComponent(); + + void OnCloseClick(object? sender, RoutedEventArgs e) + { + if (CloseRequested != null) + { + CloseRequested(); + } + else + { + var nav = this.FindAncestorOfType(); + _ = nav?.PopModalAsync() ?? Task.CompletedTask; + } + } + + void OnMovieClick(object? sender, RoutedEventArgs e) + { + string title = "Neon Horizon"; + if (sender is Button btn && btn.Tag is string tag) + title = tag; + MovieSelected?.Invoke(title); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml new file mode 100644 index 000000000..8810c5777 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml.cs new file mode 100644 index 000000000..c8910584b --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml.cs @@ -0,0 +1,16 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages; + +public partial class CurvedHeaderHomeScrollView : UserControl +{ + public Action? NavigateRequested { get; set; } + + public CurvedHeaderHomeScrollView() => InitializeComponent(); + + void OnShopNowClick(object? sender, RoutedEventArgs e) => NavigateRequested?.Invoke(); + + void OnProductClick(object? sender, RoutedEventArgs e) => NavigateRequested?.Invoke(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderProfileScrollView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderProfileScrollView.xaml new file mode 100644 index 000000000..ecfeb1bb7 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderProfileScrollView.xaml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirAppPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirAppPage.xaml.cs new file mode 100644 index 000000000..3ccdcaefa --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirAppPage.xaml.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Shapes; +using Avalonia.Controls.Templates; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Styling; + +namespace ControlCatalog.Pages; + +public partial class LAvenirAppPage : UserControl +{ + static readonly Color Primary = Color.Parse("#4b2bee"); + static readonly Color BgDark = Color.Parse("#131022"); + static readonly Color BgLight = Color.Parse("#f6f6f8"); + static readonly Color TextDark = Color.Parse("#1e293b"); + static readonly Color TextMuted = Color.Parse("#94a3b8"); + static readonly Color BorderLight = Color.Parse("#e2e8f0"); + + NavigationPage? _navPage; + DrawerPage? _drawerPage; + ScrollViewer? _infoPanel; + + public LAvenirAppPage() + { + InitializeComponent(); + + _navPage = this.FindControl("NavPage"); + _drawerPage = this.FindControl("DrawerPageControl"); + + if (_navPage != null) + _ = _navPage.PushAsync(BuildMenuTabbedPage()); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + _infoPanel = this.FindControl("InfoPanel"); + UpdateInfoPanelVisibility(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == BoundsProperty) + UpdateInfoPanelVisibility(); + } + + void UpdateInfoPanelVisibility() + { + if (_infoPanel != null) + _infoPanel.IsVisible = Bounds.Width >= 650; + } + + TabbedPage BuildMenuTabbedPage() + { + var tp = new TabbedPage + { + Background = new SolidColorBrush(BgLight), + TabPlacement = TabPlacement.Bottom, + PageTransition = new PageSlide(TimeSpan.FromMilliseconds(200)), + }; + tp.Resources["TabItemHeaderFontSize"] = 12.0; + tp.Resources["TabbedPageTabStripBackground"] = Brushes.White; + tp.Resources["TabbedPageTabStripBorderThickness"] = new Thickness(0, 1, 0, 0); + tp.Resources["TabbedPageTabStripBorderBrush"] = new SolidColorBrush(BorderLight); + tp.Resources["TabbedPageTabItemHeaderForegroundSelected"] = new SolidColorBrush(Primary); + tp.Resources["TabbedPageTabItemHeaderForegroundUnselected"] = new SolidColorBrush(TextMuted); + + tp.IndicatorTemplate = new FuncDataTemplate((_, _) => + new Ellipse + { + Width = 5, Height = 5, + Margin = new Thickness(0, 10, 0, 0), + HorizontalAlignment = HorizontalAlignment.Center, + Fill = new SolidColorBrush(Primary), + }); + + tp.Header = new TextBlock + { + Text = "L'Avenir", + FontSize = 18, + FontWeight = FontWeight.Bold, + Foreground = new SolidColorBrush(TextDark), + VerticalAlignment = VerticalAlignment.Center, + TextAlignment = TextAlignment.Center, + }; + + NavigationPage.SetTopCommandBar(tp, new Button + { + Width = 40, + Height = 40, + CornerRadius = new CornerRadius(12), + Background = Brushes.Transparent, + Foreground = new SolidColorBrush(TextDark), + Padding = new Thickness(8), + BorderThickness = new Thickness(0), + Content = new PathIcon + { + Data = Geometry.Parse("M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"), + Width = 18, + Height = 18, + }, + VerticalAlignment = VerticalAlignment.Center, + }); + + var menuView = new LAvenirMenuView(); + menuView.DishSelected = PushDishDetail; + + var menuPage = new ContentPage + { + Content = menuView, + Background = new SolidColorBrush(BgLight), + Header = "Menu", + Icon = new PathIcon { Data = Geometry.Parse("M11 9H9V2H7v7H5V2H3v7c0 2.12 1.66 3.84 3.75 3.97V22h2.5v-9.03C11.34 12.84 13 11.12 13 9V2h-2v7zm5-3v8h2.5v8H21V2c-2.76 0-5 2.24-5 4z") }, + }; + + var reservationsPage = new ContentPage + { + Content = new LAvenirReservationsView(), + Background = new SolidColorBrush(BgLight), + Header = "Reservations", + Icon = new PathIcon { Data = Geometry.Parse("M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM9 10H7v2h2v-2zm4 0h-2v2h2v-2zm4 0h-2v2h2v-2z") }, + }; + + var profilePage = new ContentPage + { + Content = new LAvenirProfileView(), + Background = new SolidColorBrush(BgLight), + Header = "Profile", + Icon = new PathIcon { Data = Geometry.Parse("M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z") }, + }; + + tp.Pages = new ObservableCollection { menuPage, reservationsPage, profilePage }; + return tp; + } + + async void PushDishDetail(string name, string price, string description, string imageFile) + { + if (_navPage == null) return; + + var detail = new ContentPage + { + Content = new LAvenirDishDetailView(name, price, description, imageFile), + Background = new SolidColorBrush(BgDark), + Header = name, + }; + NavigationPage.SetBottomCommandBar(detail, BuildFloatingBar(price)); + + _navPage.Background = new SolidColorBrush(BgDark); + _navPage.Resources["NavigationBarBackground"] = new SolidColorBrush(BgDark); + _navPage.Resources["NavigationBarForeground"] = Brushes.White; + + detail.NavigatedFrom += (_, _) => + { + if (_navPage != null) + { + _navPage.Background = new SolidColorBrush(BgLight); + _navPage.Resources["NavigationBarBackground"] = new SolidColorBrush(BgLight); + _navPage.Resources["NavigationBarForeground"] = new SolidColorBrush(TextDark); + } + }; + + await _navPage.PushAsync(detail); + } + + Border BuildFloatingBar(string price) + { + var bar = new Border + { + CornerRadius = new CornerRadius(16), + Background = new SolidColorBrush(Color.FromArgb(178, BgDark.R, BgDark.G, BgDark.B)), + BorderBrush = new SolidColorBrush(Color.FromArgb(51, 255, 255, 255)), + BorderThickness = new Thickness(1), + Padding = new Thickness(16, 12), + Margin = new Thickness(16, 8, 16, 8), + }; + + var barGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("*,Auto") }; + + var info = new StackPanel { VerticalAlignment = VerticalAlignment.Center }; + info.Children.Add(new TextBlock + { + Text = "Add to Order", + FontSize = 14, + FontWeight = FontWeight.Bold, + Foreground = Brushes.White, + }); + info.Children.Add(new TextBlock + { + Text = price, + FontSize = 12, + FontWeight = FontWeight.Medium, + Foreground = new SolidColorBrush(TextMuted), + }); + barGrid.Children.Add(info); + + var addBtn = new Button + { + Content = "Add", + Width = 80, + Height = 40, + CornerRadius = new CornerRadius(10), + Background = new SolidColorBrush(Primary), + Foreground = Brushes.White, + FontWeight = FontWeight.Bold, + FontSize = 14, + HorizontalContentAlignment = HorizontalAlignment.Center, + VerticalContentAlignment = VerticalAlignment.Center, + }; + var hoverStyle = new Style(x => x.OfType + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirReservationsView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirReservationsView.xaml.cs new file mode 100644 index 000000000..5ea6d72cf --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirReservationsView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class LAvenirReservationsView : UserControl +{ + public LAvenirReservationsView() => InitializeComponent(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageAppearancePage.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageAppearancePage.xaml new file mode 100644 index 000000000..f519b418d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageAppearancePage.xaml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseHomeView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseHomeView.xaml.cs new file mode 100644 index 000000000..5f98cee05 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseHomeView.xaml.cs @@ -0,0 +1,25 @@ +using System; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages; + +public partial class PulseHomeView : UserControl +{ + public Action? WorkoutDetailRequested { get; set; } + + public PulseHomeView() => InitializeComponent(); + + void OnRecCard1Pressed(object? sender, PointerPressedEventArgs e) => + WorkoutDetailRequested?.Invoke(); + + void OnRecCard2Pressed(object? sender, PointerPressedEventArgs e) => + WorkoutDetailRequested?.Invoke(); + + void OnRecCard3Pressed(object? sender, PointerPressedEventArgs e) => + WorkoutDetailRequested?.Invoke(); + + void OnPlayButtonClicked(object? sender, RoutedEventArgs e) => + WorkoutDetailRequested?.Invoke(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseLoginView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseLoginView.xaml new file mode 100644 index 000000000..230909040 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseLoginView.xaml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutDetailView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutDetailView.xaml.cs new file mode 100644 index 000000000..7999d3d70 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutDetailView.xaml.cs @@ -0,0 +1,15 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages; + +public partial class PulseWorkoutDetailView : UserControl +{ + public Action? BackRequested { get; set; } + + public PulseWorkoutDetailView() => InitializeComponent(); + + void OnBackClicked(object? sender, RoutedEventArgs e) => + BackRequested?.Invoke(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml new file mode 100644 index 000000000..c1f9659b0 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml @@ -0,0 +1,290 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml.cs new file mode 100644 index 000000000..fec95be3f --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PulseWorkoutsView : UserControl +{ + public PulseWorkoutsView() => InitializeComponent(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml new file mode 100644 index 000000000..23278161d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml.cs new file mode 100644 index 000000000..6e194cf1a --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml.cs @@ -0,0 +1,306 @@ +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingAppPage : UserControl +{ + static readonly Color BgColor = Color.Parse("#120a1f"); + static readonly Color SurfaceColor = Color.Parse("#2d1b4e"); + static readonly Color CyanColor = Color.Parse("#00ffff"); + static readonly Color YellowColor = Color.Parse("#ffff00"); + static readonly Color MutedColor = Color.Parse("#7856a8"); + static readonly Color TextColor = Color.Parse("#e0d0ff"); + + NavigationPage? _nav; + ScrollViewer? _infoPanel; + + public RetroGamingAppPage() + { + InitializeComponent(); + + _nav = this.FindControl("RetroNav"); + if (_nav != null) + _ = _nav.PushAsync(BuildHomePage()); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + _infoPanel = this.FindControl("InfoPanel"); + UpdateInfoPanelVisibility(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == BoundsProperty) + UpdateInfoPanelVisibility(); + } + + void UpdateInfoPanelVisibility() + { + if (_infoPanel != null) + _infoPanel.IsVisible = Bounds.Width >= 650; + } + + ContentPage BuildHomePage() + { + var page = new ContentPage { Background = new SolidColorBrush(BgColor) }; + page.Header = BuildPixelArcadeLogo(); + NavigationPage.SetTopCommandBar(page, BuildNavBarRight()); + + var panel = new Panel(); + panel.Children.Add(BuildHomeTabbedPage()); + panel.Children.Add(BuildSearchFab()); + + page.Content = panel; + return page; + } + + static Control BuildPixelArcadeLogo() + { + var row = new StackPanel + { + Orientation = Avalonia.Layout.Orientation.Horizontal, + Spacing = 10, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + }; + + var iconPanel = new Grid { Width = 36, Height = 30 }; + iconPanel.Children.Add(new Border + { + Width = 36, Height = 20, CornerRadius = new CornerRadius(3), + Background = new SolidColorBrush(Color.Parse("#cc44dd")), + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom, + }); + iconPanel.Children.Add(new Border + { + Width = 9, Height = 9, + Background = new SolidColorBrush(SurfaceColor), + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Left, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom, + Margin = new Thickness(4, 0, 0, 6), + }); + iconPanel.Children.Add(new Border + { + Width = 9, Height = 9, + Background = new SolidColorBrush(SurfaceColor), + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom, + Margin = new Thickness(0, 0, 4, 6), + }); + row.Children.Add(iconPanel); + + var textStack = new StackPanel { Spacing = 1 }; + textStack.Children.Add(new TextBlock + { + Text = "PIXEL", + FontFamily = new FontFamily("Courier New, monospace"), + FontSize = 14, FontWeight = FontWeight.Bold, + Foreground = new SolidColorBrush(YellowColor), LineHeight = 16, + }); + textStack.Children.Add(new TextBlock + { + Text = "ARCADE", + FontFamily = new FontFamily("Courier New, monospace"), + FontSize = 14, FontWeight = FontWeight.Bold, + Foreground = new SolidColorBrush(YellowColor), LineHeight = 16, + }); + row.Children.Add(textStack); + return row; + } + + static Control BuildNavBarRight() + { + var row = new StackPanel + { + Orientation = Avalonia.Layout.Orientation.Horizontal, + Spacing = 10, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0), + }; + row.Children.Add(new PathIcon + { + Width = 16, Height = 16, + Foreground = new SolidColorBrush(TextColor), + Data = Geometry.Parse("M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"), + }); + var avatar = new Border + { + Width = 26, Height = 26, + CornerRadius = new CornerRadius(0), + ClipToBounds = true, + Background = new SolidColorBrush(SurfaceColor), + BorderBrush = new SolidColorBrush(MutedColor), + BorderThickness = new Thickness(1), + }; + avatar.Child = new TextBlock + { + Text = "P1", + FontFamily = new FontFamily("Courier New, monospace"), + FontSize = 7, FontWeight = FontWeight.Bold, + Foreground = new SolidColorBrush(CyanColor), + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + }; + row.Children.Add(avatar); + return row; + } + + TabbedPage BuildHomeTabbedPage() + { + var tp = new TabbedPage + { + Background = new SolidColorBrush(BgColor), + TabPlacement = TabPlacement.Bottom, + PageTransition = new PageSlide(TimeSpan.FromMilliseconds(250)), + }; + tp.Resources["TabItemHeaderFontSize"] = 12.0; + tp.Resources["TabbedPageTabStripBackground"] = new SolidColorBrush(SurfaceColor); + tp.Resources["TabbedPageTabItemHeaderForegroundSelected"] = new SolidColorBrush(Color.Parse("#ad2bee")); + tp.Resources["TabbedPageTabItemHeaderForegroundUnselected"] = new SolidColorBrush(MutedColor); + + var homeView = new RetroGamingHomeView(); + homeView.GameSelected = PushDetailPage; + + var homeTab = new ContentPage + { + Header = "Home", + Icon = new PathIcon { Data = Geometry.Parse("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z") }, + Background = new SolidColorBrush(BgColor), + Content = homeView, + }; + + var gamesView = new RetroGamingGamesView(); + gamesView.GameSelected = PushDetailPage; + + var gamesTab = new ContentPage + { + Header = "Games", + Icon = new PathIcon { Data = Geometry.Parse("M7.97,16L5,19C4.67,19.3 4.23,19.5 3.75,19.5A1.75,1.75 0 0,1 2,17.75V17.5L3,10.12C3.21,7.81 5.14,6 7.5,6H16.5C18.86,6 20.79,7.81 21,10.12L22,17.5V17.75A1.75,1.75 0 0,1 20.25,19.5C19.77,19.5 19.33,19.3 19,19L16.03,16H7.97M7,9V11H5V13H7V15H9V13H11V11H9V9H7M14.5,12A1.5,1.5 0 0,0 13,13.5A1.5,1.5 0 0,0 14.5,15A1.5,1.5 0 0,0 16,13.5A1.5,1.5 0 0,0 14.5,12M17.5,9A1.5,1.5 0 0,0 16,10.5A1.5,1.5 0 0,0 17.5,12A1.5,1.5 0 0,0 19,10.5A1.5,1.5 0 0,0 17.5,9Z") }, + Background = new SolidColorBrush(BgColor), + Content = gamesView, + }; + + var favTab = new ContentPage + { + Header = "Favorites", + Icon = new PathIcon { Data = Geometry.Parse("M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z") }, + Background = new SolidColorBrush(BgColor), + Content = new RetroGamingFavoritesView(), + }; + + var profileTab = new ContentPage + { + Header = "Profile", + Icon = new PathIcon { Data = Geometry.Parse("M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z") }, + Background = new SolidColorBrush(BgColor), + Content = new RetroGamingProfileView(), + }; + + tp.Pages = new ObservableCollection { homeTab, gamesTab, favTab, profileTab }; + return tp; + } + + Control BuildSearchFab() + { + var fab = new Button + { + Width = 50, Height = 50, + CornerRadius = new CornerRadius(0), + Background = new SolidColorBrush(YellowColor), + Padding = new Thickness(0), + }; + fab.Classes.Add("retro-fab"); + fab.Content = new PathIcon + { + Width = 22, Height = 22, + Foreground = new SolidColorBrush(BgColor), + Data = Geometry.Parse("M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"), + }; + fab.Click += (_, _) => _ = _nav?.PushModalAsync(BuildSearchModal()); + + return new Border + { + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom, + Margin = new Thickness(0, 0, 0, 35), + BoxShadow = new BoxShadows(new BoxShadow + { + Blur = 10, Spread = 1, + Color = Color.FromArgb(140, 255, 255, 0), + }), + Child = fab, + }; + } + + ContentPage BuildSearchModal() + { + var page = new ContentPage { Background = new SolidColorBrush(BgColor) }; + + var searchView = new RetroGamingSearchView(); + searchView.CloseRequested = () => _ = _nav?.PopModalAsync(); + searchView.GameSelected = async title => + { + await (_nav?.PopModalAsync() ?? System.Threading.Tasks.Task.CompletedTask); + PushDetailPage(title); + }; + + page.Content = searchView; + return page; + } + + async void PushDetailPage(string gameTitle) + { + if (_nav == null) return; + + var detailView = new RetroGamingDetailView(gameTitle); + + var page = new ContentPage + { + Background = new SolidColorBrush(BgColor), + Content = detailView, + }; + + NavigationPage.SetBarLayoutBehavior(page, BarLayoutBehavior.Overlay); + page.NavigatedTo += (_, _) => { if (_nav != null) _nav.Resources["NavigationBarBackground"] = Brushes.Transparent; }; + page.NavigatedFrom += (_, _) => { if (_nav != null) _nav.Resources["NavigationBarBackground"] = new SolidColorBrush(SurfaceColor); }; + + var cmdBar = new StackPanel + { + Orientation = Avalonia.Layout.Orientation.Horizontal, + Spacing = 4, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0), + }; + var heartBtn = new Button(); + heartBtn.Classes.Add("retro-icon-btn"); + heartBtn.Content = new PathIcon + { + Width = 16, Height = 16, + Foreground = new SolidColorBrush(Color.Parse("#ad2bee")), + Data = Geometry.Parse("M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z"), + }; + var shareBtn = new Button(); + shareBtn.Classes.Add("retro-icon-btn"); + shareBtn.Content = new PathIcon + { + Width = 16, Height = 16, + Foreground = new SolidColorBrush(TextColor), + Data = Geometry.Parse("M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.35C15.11,18.56 15.08,18.78 15.08,19C15.08,20.61 16.39,21.92 18,21.92C19.61,21.92 20.92,20.61 20.92,19C20.92,17.39 19.61,16.08 18,16.08Z"), + }; + cmdBar.Children.Add(heartBtn); + cmdBar.Children.Add(shareBtn); + NavigationPage.SetTopCommandBar(page, cmdBar); + + await _nav.PushAsync(page); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml new file mode 100644 index 000000000..718e8137d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml.cs new file mode 100644 index 000000000..cf49850b8 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingDetailView : UserControl +{ + static readonly Dictionary GameAssets = new() + { + { "Cyber Ninja 2084", "hero.jpg" }, + { "Pixel Quest", "pixel_quest.jpg" }, + { "Neon Racer", "neon_racer.jpg" }, + { "Dungeon Bit", "dungeon_bit.jpg" }, + { "Forest Spirit", "forest_spirit.jpg" }, + { "Cyber City", "cyber_city.jpg" }, + { "Neon Ninja", "neon_ninja.jpg" }, + { "Space Voids", "space_voids.jpg" }, + }; + + public RetroGamingDetailView() => InitializeComponent(); + + public RetroGamingDetailView(string gameTitle) + { + InitializeComponent(); + + DetailTitleText.Text = gameTitle.ToUpperInvariant(); + + var filename = GameAssets.TryGetValue(gameTitle, out var f) ? f + : (GameAssets.TryGetValue("Neon Ninja", out var fb) ? fb : null); + + if (filename != null) + { + try + { + var uri = new Uri($"avares://ControlCatalog/Assets/RetroGaming/{filename}"); + using var stream = AssetLoader.Open(uri); + var bmp = new Bitmap(stream); + DetailHeroImageBorder.Background = new ImageBrush(bmp) + { + Stretch = Stretch.UniformToFill, + }; + } + catch + { + SetFallbackBackground(); + } + } + else + { + SetFallbackBackground(); + } + } + + void SetFallbackBackground() + { + var grad = new LinearGradientBrush + { + StartPoint = new Avalonia.RelativePoint(0, 0, Avalonia.RelativeUnit.Relative), + EndPoint = new Avalonia.RelativePoint(1, 1, Avalonia.RelativeUnit.Relative), + }; + grad.GradientStops.Add(new GradientStop(Avalonia.Media.Color.Parse("#3d2060"), 0)); + grad.GradientStops.Add(new GradientStop(Avalonia.Media.Color.Parse("#120a1f"), 1)); + DetailHeroImageBorder.Background = grad; + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml new file mode 100644 index 000000000..ef11d4e72 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml.cs new file mode 100644 index 000000000..fd67902a0 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingFavoritesView : UserControl +{ + public RetroGamingFavoritesView() => InitializeComponent(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml new file mode 100644 index 000000000..a4e9f4038 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml.cs new file mode 100644 index 000000000..6676b792a --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml.cs @@ -0,0 +1,39 @@ +using System; +using Avalonia.Controls; +using Avalonia.Layout; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingGamesView : UserControl +{ + public Action? GameSelected { get; set; } + + public RetroGamingGamesView() + { + InitializeComponent(); + + GameCyberNinjaBtn.Click += (_, _) => GameSelected?.Invoke("Cyber Ninja 2084"); + GameNeonRacerBtn.Click += (_, _) => GameSelected?.Invoke("Neon Racer"); + GameDungeonBitBtn.Click += (_, _) => GameSelected?.Invoke("Dungeon Bit"); + GameForestSpiritBtn.Click += (_, _) => GameSelected?.Invoke("Forest Spirit"); + GamePixelQuestBtn.Click += (_, _) => GameSelected?.Invoke("Pixel Quest"); + GameSpaceVoidsBtn.Click += (_, _) => GameSelected?.Invoke("Space Voids"); + GameCyberCityBtn.Click += (_, _) => GameSelected?.Invoke("Cyber City"); + + GamesGrid.SizeChanged += OnGridSizeChanged; + } + + void OnGridSizeChanged(object? sender, SizeChangedEventArgs e) + { + const double defaultWidth = 145; + var available = GamesGrid.Bounds.Width; + if (available <= 0) return; + + bool singleColumn = available < defaultWidth * 2; + foreach (var child in GamesGrid.Children) + { + if (child is Button btn && btn.Content is Border card) + card.Width = singleColumn ? available : defaultWidth; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml new file mode 100644 index 000000000..bf5d11253 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml @@ -0,0 +1,346 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml.cs new file mode 100644 index 000000000..d1693f3c6 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml.cs @@ -0,0 +1,22 @@ +using System; +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingHomeView : UserControl +{ + public Action? GameSelected { get; set; } + + public RetroGamingHomeView() + { + InitializeComponent(); + + HeroPlayBtn.Click += (_, _) => GameSelected?.Invoke("Cyber Ninja 2084"); + ContinuePixelQuestBtn.Click += (_, _) => GameSelected?.Invoke("Pixel Quest"); + ContinueSpaceVoidsBtn.Click += (_, _) => GameSelected?.Invoke("Space Voids"); + NewReleaseNeonRacerBtn.Click += (_, _) => GameSelected?.Invoke("Neon Racer"); + NewReleaseDungeonBitBtn.Click += (_, _) => GameSelected?.Invoke("Dungeon Bit"); + NewReleaseForestSpiritBtn.Click += (_, _) => GameSelected?.Invoke("Forest Spirit"); + NewReleaseCyberCityBtn.Click += (_, _) => GameSelected?.Invoke("Cyber City"); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml new file mode 100644 index 000000000..17d7ff2d5 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml.cs new file mode 100644 index 000000000..f2284bdc2 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingProfileView : UserControl +{ + public RetroGamingProfileView() => InitializeComponent(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml new file mode 100644 index 000000000..92389efec --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml @@ -0,0 +1,239 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml.cs new file mode 100644 index 000000000..c5db19c04 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml.cs @@ -0,0 +1,24 @@ +using System; +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingSearchView : UserControl +{ + public Action? CloseRequested { get; set; } + public Action? GameSelected { get; set; } + + public RetroGamingSearchView() + { + InitializeComponent(); + + CloseBtn.Click += (_, _) => CloseRequested?.Invoke(); + SearchCyberNinjaBtn.Click += (_, _) => GameSelected?.Invoke("Cyber Ninja 2084"); + SearchNeonRacerBtn.Click += (_, _) => GameSelected?.Invoke("Neon Racer"); + SearchDungeonBitBtn.Click += (_, _) => GameSelected?.Invoke("Dungeon Bit"); + SearchForestSpiritBtn.Click += (_, _) => GameSelected?.Invoke("Forest Spirit"); + SearchPixelQuestBtn.Click += (_, _) => GameSelected?.Invoke("Pixel Quest"); + SearchSpaceVoidsBtn.Click += (_, _) => GameSelected?.Invoke("Space Voids"); + SearchCyberCityBtn.Click += (_, _) => GameSelected?.Invoke("Cyber City"); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/CompositeTransition.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/CompositeTransition.cs new file mode 100644 index 000000000..0cddd58ef --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/CompositeTransition.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages +{ + /// + /// Example custom IPageTransition: horizontal slide combined with cross-fade. + /// Both pages slide and fade simultaneously for a smooth blended effect. + /// + public class CompositeTransition : IPageTransition + { + public CompositeTransition() { } + + public CompositeTransition(TimeSpan duration) + { + Duration = duration; + } + + public TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(300); + public Easing TransitionEasing { get; set; } = new LinearEasing(); + + public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + return; + + var tasks = new List(); + var parent = GetVisualParent(from, to); + var distance = parent.Bounds.Width > 0 ? parent.Bounds.Width : 500d; + + if (from != null) + { + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = TransitionEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(TranslateTransform.XProperty, 0d), + new Setter(Visual.OpacityProperty, 1d) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(TranslateTransform.XProperty, forward ? -distance : distance), + new Setter(Visual.OpacityProperty, 0d) + } + } + } + }; + tasks.Add(anim.RunAsync(from, cancellationToken)); + } + + if (to != null) + { + to.IsVisible = true; + + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = TransitionEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(TranslateTransform.XProperty, forward ? distance : -distance), + new Setter(Visual.OpacityProperty, 0d) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(TranslateTransform.XProperty, 0d), + new Setter(Visual.OpacityProperty, 1d) + } + } + } + }; + tasks.Add(anim.RunAsync(to, cancellationToken)); + } + + await Task.WhenAll(tasks); + + if (from != null && !cancellationToken.IsCancellationRequested) + from.IsVisible = false; + } + + private static Visual GetVisualParent(Visual? from, Visual? to) + { + var p1 = (from ?? to)!.GetVisualParent(); + if (from != null && to != null && + !ReferenceEquals(from.GetVisualParent(), to.GetVisualParent())) + throw new ArgumentException("Transition elements have different parents."); + return p1 ?? throw new ArgumentException("Transition elements have no parent."); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/FadeThroughTransition.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/FadeThroughTransition.cs new file mode 100644 index 000000000..ece9b1a1d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/FadeThroughTransition.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia.Media; +using Avalonia.Styling; + +namespace ControlCatalog.Pages +{ + /// + /// Example custom IPageTransition: a "fade through" with scale. + /// The outgoing page fades out while scaling down; the incoming page fades in while + /// scaling up, producing a smooth depth-aware transition. + /// + public class FadeThroughTransition : IPageTransition + { + public FadeThroughTransition() { } + + public FadeThroughTransition(TimeSpan duration) + { + Duration = duration; + } + + public TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(300); + public Easing FadeEasing { get; set; } = new CubicEaseOut(); + + public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + return; + + var tasks = new List(); + + if (from != null) + { + from.RenderTransformOrigin = RelativePoint.Center; + from.RenderTransform = new ScaleTransform(1, 1); + } + + if (to != null) + { + to.RenderTransformOrigin = RelativePoint.Center; + to.RenderTransform = new ScaleTransform(1, 1); + to.Opacity = 0; + } + + if (from != null) + { + var outAnim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = FadeEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(Visual.OpacityProperty, 1d), + new Setter(ScaleTransform.ScaleXProperty, 1d), + new Setter(ScaleTransform.ScaleYProperty, 1d) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(Visual.OpacityProperty, 0d), + new Setter(ScaleTransform.ScaleXProperty, forward ? 0.92 : 1.08), + new Setter(ScaleTransform.ScaleYProperty, forward ? 0.92 : 1.08) + } + } + } + }; + tasks.Add(outAnim.RunAsync(from, cancellationToken)); + } + + if (to != null) + { + to.IsVisible = true; + + var inAnim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = FadeEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(Visual.OpacityProperty, 0d), + new Setter(ScaleTransform.ScaleXProperty, forward ? 1.08 : 0.92), + new Setter(ScaleTransform.ScaleYProperty, forward ? 1.08 : 0.92) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(Visual.OpacityProperty, 1d), + new Setter(ScaleTransform.ScaleXProperty, 1d), + new Setter(ScaleTransform.ScaleYProperty, 1d) + } + } + } + }; + tasks.Add(inAnim.RunAsync(to, cancellationToken)); + } + + await Task.WhenAll(tasks); + + if (to != null && !cancellationToken.IsCancellationRequested) + { + to.Opacity = 1; + to.RenderTransform = null; + } + + if (from != null) + { + if (!cancellationToken.IsCancellationRequested) + from.IsVisible = false; + from.Opacity = 1; + from.RenderTransform = null; + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/PageSlideTransition.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/PageSlideTransition.cs new file mode 100644 index 000000000..dfbe8478f --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/PageSlideTransition.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages +{ + /// + /// Example custom IPageTransition: a directional page slide. + /// Both pages slide together in the specified axis direction. + /// Demonstrates how to implement a custom horizontal or vertical slide from scratch. + /// + public class PageSlideTransition : IPageTransition + { + public enum SlideAxis { Horizontal, Vertical } + + public PageSlideTransition() { } + + public PageSlideTransition(TimeSpan duration, SlideAxis axis = SlideAxis.Horizontal) + { + Duration = duration; + Axis = axis; + } + + public TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(300); + public SlideAxis Axis { get; set; } = SlideAxis.Horizontal; + public Easing SlideEasing { get; set; } = new LinearEasing(); + + public Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) => + Axis == SlideAxis.Horizontal + ? StartAxis(from, to, forward, cancellationToken, TranslateTransform.XProperty, () => GetVisualParent(from, to).Bounds.Width) + : StartAxis(from, to, forward, cancellationToken, TranslateTransform.YProperty, () => GetVisualParent(from, to).Bounds.Height); + + private async Task StartAxis( + Visual? from, Visual? to, bool forward, CancellationToken cancellationToken, + Avalonia.AvaloniaProperty prop, Func getDistance) + { + if (cancellationToken.IsCancellationRequested) + return; + var tasks = new List(); + var distance = getDistance() is > 0 and var d ? d : 500d; + + if (from != null) + { + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = SlideEasing, + Duration = Duration, + Children = + { + new KeyFrame { Cue = new Cue(0d), Setters = { new Setter(prop, 0d) } }, + new KeyFrame { Cue = new Cue(1d), Setters = { new Setter(prop, forward ? -distance : distance) } } + } + }; + tasks.Add(anim.RunAsync(from, cancellationToken)); + } + + if (to != null) + { + to.IsVisible = true; + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = SlideEasing, + Duration = Duration, + Children = + { + new KeyFrame { Cue = new Cue(0d), Setters = { new Setter(prop, forward ? distance : -distance) } }, + new KeyFrame { Cue = new Cue(1d), Setters = { new Setter(prop, 0d) } } + } + }; + tasks.Add(anim.RunAsync(to, cancellationToken)); + } + + await Task.WhenAll(tasks); + + if (from != null && !cancellationToken.IsCancellationRequested) + from.IsVisible = false; + } + + private static Visual GetVisualParent(Visual? from, Visual? to) + { + var p1 = (from ?? to)!.GetVisualParent(); + if (from != null && to != null && + !ReferenceEquals(from.GetVisualParent(), to.GetVisualParent())) + throw new ArgumentException("Transition elements have different parents."); + return p1 ?? throw new ArgumentException("Transition elements have no parent."); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/ParallaxSlideTransition.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/ParallaxSlideTransition.cs new file mode 100644 index 000000000..7326c6e93 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/ParallaxSlideTransition.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages +{ + /// + /// Example custom IPageTransition: a parallax slide. + /// The incoming page slides full-width from the right while the outgoing page shifts ~30% + /// to the left with a subtle opacity fade, producing a depth-layered push effect. + /// + public class ParallaxSlideTransition : IPageTransition + { + public ParallaxSlideTransition() { } + + public ParallaxSlideTransition(TimeSpan duration) + { + Duration = duration; + } + + public TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(350); + public Easing SlideEasing { get; set; } = new CubicEaseOut(); + + public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + return; + + var tasks = new List(); + var parent = GetVisualParent(from, to); + var distance = parent.Bounds.Width > 0 ? parent.Bounds.Width : 500d; + + if (from != null) + { + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = SlideEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(TranslateTransform.XProperty, 0d), + new Setter(Visual.OpacityProperty, 1d) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(TranslateTransform.XProperty, forward ? -distance * 0.3 : distance), + new Setter(Visual.OpacityProperty, forward ? 0.7 : 1d) + } + } + } + }; + tasks.Add(anim.RunAsync(from, cancellationToken)); + } + + if (to != null) + { + to.IsVisible = true; + + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = SlideEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(TranslateTransform.XProperty, forward ? distance : -distance * 0.3), + new Setter(Visual.OpacityProperty, forward ? 1d : 0.7) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(TranslateTransform.XProperty, 0d), + new Setter(Visual.OpacityProperty, 1d) + } + } + } + }; + tasks.Add(anim.RunAsync(to, cancellationToken)); + } + + await Task.WhenAll(tasks); + + if (from != null && !cancellationToken.IsCancellationRequested) + from.IsVisible = false; + } + + private static Visual GetVisualParent(Visual? from, Visual? to) + { + var p1 = (from ?? to)!.GetVisualParent(); + if (from != null && to != null && + !ReferenceEquals(from.GetVisualParent(), to.GetVisualParent())) + throw new ArgumentException("Transition elements have different parents."); + return p1 ?? throw new ArgumentException("Transition elements have no parent."); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPerformanceMonitorHelper.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPerformanceMonitorHelper.cs new file mode 100644 index 000000000..3178c35fa --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPerformanceMonitorHelper.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Threading; + +namespace ControlCatalog.Pages +{ + /// + /// Shared helpers for the performance-monitor demo pages + /// (NavigationPage, TabbedPage, DrawerPage, ContentPage). + /// + internal sealed class NavigationPerformanceMonitorHelper + { + internal static readonly IBrush PositiveDeltaBrush = new SolidColorBrush(Color.Parse("#D32F2F")); + internal static readonly IBrush NegativeDeltaBrush = new SolidColorBrush(Color.Parse("#388E3C")); + internal static readonly IBrush ZeroDeltaBrush = new SolidColorBrush(Color.Parse("#757575")); + internal static readonly IBrush CurrentBorderBrush = new SolidColorBrush(Color.Parse("#0078D4")); + internal static readonly IBrush DefaultBorderBrush = new SolidColorBrush(Color.Parse("#CCCCCC")); + + private readonly List> _trackedPages = new(); + private double _previousHeapMB; + private DispatcherTimer? _autoRefreshTimer; + + internal readonly Stopwatch OpStopwatch = new(); + internal int TotalCreated; + + /// + /// Track a newly-created page via WeakReference and increment TotalCreated. + /// + internal void TrackPage(Page page) + { + TotalCreated++; + _trackedPages.Add(new WeakReference(page)); + } + + /// + /// Count live (not yet GC'd) tracked page instances. + /// + internal int CountLiveInstances() + { + int alive = 0; + for (int i = _trackedPages.Count - 1; i >= 0; i--) + { + if (_trackedPages[i].TryGetTarget(out _)) + alive++; + else + _trackedPages.RemoveAt(i); + } + return alive; + } + + /// + /// Update heap and delta text blocks. Call from RefreshAll(). + /// + internal void UpdateHeapDelta(TextBlock heapText, TextBlock deltaText) + { + var heapMB = GC.GetTotalMemory(false) / (1024.0 * 1024.0); + heapText.Text = $"Managed Heap: {heapMB:##0.0} MB"; + + var delta = heapMB - _previousHeapMB; + if (Math.Abs(delta) < 0.05) + { + deltaText.Text = "(no change)"; + deltaText.Foreground = ZeroDeltaBrush; + } + else + { + var sign = delta > 0 ? "+" : ""; + deltaText.Text = $"({sign}{delta:0.0} MB)"; + deltaText.Foreground = delta > 0 ? PositiveDeltaBrush : NegativeDeltaBrush; + } + _previousHeapMB = heapMB; + } + + /// + /// Initialize previous heap baseline. + /// + internal void InitHeap() + { + _previousHeapMB = GC.GetTotalMemory(false) / (1024.0 * 1024.0); + } + + /// + /// Stop the stopwatch and write elapsed ms to the given TextBlock. + /// + internal void StopMetrics(TextBlock lastOpText) + { + if (!OpStopwatch.IsRunning) return; + OpStopwatch.Stop(); + lastOpText.Text = $"Last Op: {OpStopwatch.ElapsedMilliseconds} ms"; + } + + /// + /// Force full GC, then invoke the refresh callback. + /// + internal void ForceGC(Action refresh) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + refresh(); + } + + /// + /// Start a 2-second auto-refresh timer. + /// + internal void StartAutoRefresh(Action refresh) + { + if (_autoRefreshTimer != null) return; + _autoRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) }; + _autoRefreshTimer.Tick += (_, _) => refresh(); + _autoRefreshTimer.Start(); + } + + /// + /// Stop the auto-refresh timer. + /// + internal void StopAutoRefresh() + { + _autoRefreshTimer?.Stop(); + _autoRefreshTimer = null; + } + + /// + /// Toggle auto-refresh based on a CheckBox. + /// + internal void OnAutoRefreshChanged(CheckBox check, Action refresh) + { + if (check.IsChecked == true) + StartAutoRefresh(refresh); + else + StopAutoRefresh(); + } + + /// + /// Append a timestamped log entry to a StackPanel inside a ScrollViewer. + /// + internal void LogOperation(string action, string detail, + StackPanel logPanel, ScrollViewer logScroll, string? extraInfo = null) + { + var heapMB = GC.GetTotalMemory(false) / (1024.0 * 1024.0); + var timing = OpStopwatch.ElapsedMilliseconds; + var extra = extraInfo != null ? $" {extraInfo}," : ""; + + logPanel.Children.Add(new TextBlock + { + Text = $"{DateTime.Now:HH:mm:ss} [{action}] {detail} —{extra} heap {heapMB:##0.0} MB, {timing} ms", + FontSize = 10, + FontFamily = new FontFamily("Cascadia Mono,Consolas,Menlo,monospace"), + Padding = new Thickness(6, 2), + TextTrimming = TextTrimming.CharacterEllipsis, + }); + logScroll.ScrollToEnd(); + } + + /// + /// Build a tracked ContentPage with a 50 KB dummy allocation. + /// + internal ContentPage BuildTrackedPage(string title, int index, int allocBytes = 51200) + { + var page = NavigationDemoHelper.MakePage(title, + $"Stack position #{index}\nPush more pages ...", index); + page.Tag = new byte[allocBytes]; + TrackPage(page); + return page; + } + + /// + /// Create a reusable stack/history row (badge + title + label). + /// + internal static (Border Container, Border Badge, TextBlock IndexText, + TextBlock TitleText, TextBlock BadgeText) CreateStackRow() + { + var indexText = new TextBlock + { + FontSize = 10, FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + }; + var badge = new Border + { + Width = 22, Height = 22, + CornerRadius = new CornerRadius(11), + VerticalAlignment = VerticalAlignment.Center, + Child = indexText, + }; + var titleText = new TextBlock + { + VerticalAlignment = VerticalAlignment.Center, + TextTrimming = TextTrimming.CharacterEllipsis, + Margin = new Thickness(6, 0, 0, 0), + }; + var badgeText = new TextBlock + { + FontSize = 10, Opacity = 0.5, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(4, 0, 0, 0), + IsVisible = false, + }; + + var row = new DockPanel(); + row.Children.Add(badge); + row.Children.Add(titleText); + row.Children.Add(badgeText); + + var container = new Border + { + CornerRadius = new CornerRadius(6), + Padding = new Thickness(8, 6), + Child = row, + }; + + return (container, badge, indexText, titleText, badgeText); + } + + /// + /// Update a stack row with page data. + /// + internal static void UpdateStackRow( + (Border Container, Border Badge, TextBlock IndexText, + TextBlock TitleText, TextBlock BadgeText) row, + int stackIndex, string title, bool isCurrent, bool isRoot) + { + row.Badge.Background = NavigationDemoHelper.GetPageBrush(stackIndex); + row.IndexText.Text = (stackIndex + 1).ToString(); + row.TitleText.Text = title; + row.TitleText.FontWeight = isCurrent ? FontWeight.SemiBold : FontWeight.Normal; + + string? label = isCurrent ? "current" : (isRoot ? "root" : null); + row.BadgeText.Text = label ?? ""; + row.BadgeText.IsVisible = label != null; + + row.Container.BorderBrush = isCurrent ? CurrentBorderBrush : DefaultBorderBrush; + row.Container.BorderThickness = new Thickness(isCurrent ? 2 : 1); + } + + /// + /// Sync a StackPanel of stack rows with data, growing/shrinking the row cache as needed. + /// + internal static void RefreshStackPanel( + StackPanel panel, + List<(Border Container, Border Badge, TextBlock IndexText, + TextBlock TitleText, TextBlock BadgeText)> rowCache, + IReadOnlyList stack, Page? currentPage) + { + int count = stack.Count; + + while (rowCache.Count < count) + rowCache.Add(CreateStackRow()); + + while (panel.Children.Count > count) + panel.Children.RemoveAt(panel.Children.Count - 1); + while (panel.Children.Count < count) + panel.Children.Add(rowCache[panel.Children.Count].Container); + + for (int displayIdx = 0; displayIdx < count; displayIdx++) + { + int stackIdx = count - 1 - displayIdx; + var page = stack[stackIdx]; + bool isCurrent = ReferenceEquals(page, currentPage); + bool isRoot = stackIdx == 0; + + var row = rowCache[displayIdx]; + if (!ReferenceEquals(panel.Children[displayIdx], row.Container)) + panel.Children[displayIdx] = row.Container; + + UpdateStackRow(row, stackIdx, page.Header?.ToString() ?? "(untitled)", isCurrent, isRoot); + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NotificationsPage.xaml b/samples/AvaloniaControlCatalog/Pages/NotificationsPage.xaml new file mode 100644 index 000000000..61d8afdea --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NotificationsPage.xaml @@ -0,0 +1,25 @@ + + + TopLevel bound notification manager. + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/OpenGlPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/OpenGlPage.xaml.cs new file mode 100644 index 000000000..76a79290d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/OpenGlPage.xaml.cs @@ -0,0 +1,82 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Controls; +using Avalonia.Rendering.Composition; +using ControlCatalog.Pages.OpenGl; + +// ReSharper disable StringLiteralTypo + +namespace ControlCatalog.Pages +{ + public partial class OpenGlPage : ContentPage + { + public OpenGlPage() + { + InitializeComponent(); + GL.Init(Knobs); + + AttachedToVisualTree += delegate + { + if (TopLevel.GetTopLevel(this) is Window) + Snapshot.IsVisible = true; + }; + } + + private async void SnapshotClick(object sender, RoutedEventArgs e) + { + var v = ElementComposition.GetElementVisual(this)!; + var snap = await v.Compositor.CreateCompositionVisualSnapshot(v, 1.5); + await new Window() + { + Content = new ScrollViewer() + { + HorizontalScrollBarVisibility = ScrollBarVisibility.Auto, + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + Content = new Image() + { + Source = snap + } + } + }.ShowDialog((Window)TopLevel.GetTopLevel(this)!); + } + } + + public class OpenGlPageControl : OpenGlControlBase + { + private OpenGlContent _content = new(); + private GlPageKnobs? _knobs; + + public void Init(GlPageKnobs knobs) + { + _knobs = knobs; + _knobs.PropertyChanged += KnobsPropertyChanged; + } + + private void KnobsPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == GlPageKnobs.YawProperty + || change.Property == GlPageKnobs.RollProperty + || change.Property == GlPageKnobs.PitchProperty + || change.Property == GlPageKnobs.DiscoProperty) + RequestNextFrameRendering(); + } + + protected override unsafe void OnOpenGlInit(GlInterface GL) => _content.Init(GL, GlVersion); + + protected override void OnOpenGlDeinit(GlInterface GL) => _content.Deinit(GL); + + protected override unsafe void OnOpenGlRender(GlInterface gl, int fb) + { + if (_knobs == null) + return; + _content.OnOpenGlRender(gl, fb, new PixelSize((int)Bounds.Width, (int)Bounds.Height), + _knobs.Yaw, _knobs.Pitch, _knobs.Roll, _knobs.Disco); + if (_knobs.Disco > 0.01) + RequestNextFrameRendering(); + _knobs?.Info = _content.Info; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml new file mode 100644 index 000000000..b75b5c37c --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml.cs new file mode 100644 index 000000000..f42bb10ce --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerCarouselPage : UserControl +{ + public PipsPagerCarouselPage() + { + InitializeComponent(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomButtonThemesPage.xaml b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomButtonThemesPage.xaml new file mode 100644 index 000000000..3e32f253f --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomButtonThemesPage.xaml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomButtonThemesPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomButtonThemesPage.xaml.cs new file mode 100644 index 000000000..26682b9d6 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomButtonThemesPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerCustomButtonThemesPage : UserControl +{ + public PipsPagerCustomButtonThemesPage() + { + InitializeComponent(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml new file mode 100644 index 000000000..260536d7a --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml.cs new file mode 100644 index 000000000..a9276f11b --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerCustomColorsPage : UserControl +{ + public PipsPagerCustomColorsPage() + { + InitializeComponent(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml new file mode 100644 index 000000000..fe748b248 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml.cs new file mode 100644 index 000000000..cce9e6c5e --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerCustomTemplatesPage : UserControl +{ + public PipsPagerCustomTemplatesPage() + { + InitializeComponent(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml new file mode 100644 index 000000000..a69c10168 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml.cs new file mode 100644 index 000000000..d97165397 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml.cs @@ -0,0 +1,29 @@ +using System.Collections.ObjectModel; +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerEventsPage : UserControl +{ + private readonly ObservableCollection _events = new(); + + public PipsPagerEventsPage() + { + InitializeComponent(); + + EventLog.ItemsSource = _events; + + EventPager.PropertyChanged += (_, e) => + { + if (e.Property != PipsPager.SelectedPageIndexProperty) + return; + + var newIndex = (int)e.NewValue!; + StatusText.Text = $"Selected: {newIndex}"; + _events.Insert(0, $"SelectedPageIndex changed to {newIndex}"); + + if (_events.Count > 20) + _events.RemoveAt(_events.Count - 1); + }; + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml new file mode 100644 index 000000000..5eead2fb3 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml.cs new file mode 100644 index 000000000..80a1569f3 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerGettingStartedPage : UserControl +{ + public PipsPagerGettingStartedPage() + { + InitializeComponent(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml new file mode 100644 index 000000000..5cc416d41 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml.cs new file mode 100644 index 000000000..2dc936b54 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerLargeCollectionPage : UserControl +{ + public PipsPagerLargeCollectionPage() + { + InitializeComponent(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPagerPage.xaml b/samples/AvaloniaControlCatalog/Pages/PipsPagerPage.xaml new file mode 100644 index 000000000..aee21a7d0 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPagerPage.xaml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPagerPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/PipsPagerPage.xaml.cs new file mode 100644 index 000000000..5a3b142f5 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPagerPage.xaml.cs @@ -0,0 +1,54 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class PipsPagerPage : ContentPage + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = + { + ("Getting Started", "First Look", + "Default PipsPager with horizontal and vertical orientation, with and without navigation buttons.", + () => new PipsPagerGettingStartedPage()), + + ("Features", "Carousel Integration", + "Bind SelectedPageIndex to a Carousel's SelectedIndex for two-way synchronized page navigation.", + () => new PipsPagerCarouselPage()), + ("Features", "Large Collections", + "Use MaxVisiblePips to limit visible indicators when the page count is large. Pips scroll automatically.", + () => new PipsPagerLargeCollectionPage()), + ("Features", "Events", + "Monitor SelectedPageIndex changes to react to user navigation.", + () => new PipsPagerEventsPage()), + + ("Appearance", "Custom Colors", + "Override pip indicator colors using resource keys for normal, selected, and hover states.", + () => new PipsPagerCustomColorsPage()), + ("Appearance", "Custom Button Themes", + "Replace the default chevron navigation buttons with custom button themes.", + () => new PipsPagerCustomButtonThemesPage()), + ("Appearance", "Custom Templates", + "Override pip item templates to create squares, pills, numbers, or any custom shape.", + () => new PipsPagerCustomTemplatesPage()), + + ("Showcases", "Care Companion", + "A health care onboarding flow using PipsPager as the page indicator for a CarouselPage.", + () => new CareCompanionAppPage()), + ("Showcases", "Sanctuary", + "A travel discovery app using PipsPager as the page indicator for a CarouselPage.", + () => new SanctuaryShowcasePage()), + }; + + public PipsPagerPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PlatformInfoPage.xaml b/samples/AvaloniaControlCatalog/Pages/PlatformInfoPage.xaml new file mode 100644 index 000000000..f86d53b74 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PlatformInfoPage.xaml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/PlatformInfoPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/PlatformInfoPage.xaml.cs new file mode 100644 index 000000000..db1bbef40 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PlatformInfoPage.xaml.cs @@ -0,0 +1,14 @@ +using Avalonia.Controls; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public partial class PlatformInfoPage : ContentPage + { + public PlatformInfoPage() + { + InitializeComponent(); + DataContext = new PlatformInformationViewModel(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PlatformSettingsPage.xaml b/samples/AvaloniaControlCatalog/Pages/PlatformSettingsPage.xaml new file mode 100644 index 000000000..a6213558c --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PlatformSettingsPage.xaml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/PlatformSettingsPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/PlatformSettingsPage.xaml.cs new file mode 100644 index 000000000..8f3e6f4b4 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PlatformSettingsPage.xaml.cs @@ -0,0 +1,30 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.VisualTree; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public partial class PlatformSettingsPage : ContentPage + { + private readonly PlatformSettingsViewModel _viewModel = new(); + + public PlatformSettingsPage() + { + InitializeComponent(); + DataContext = _viewModel; + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _viewModel.Subscribe(this.GetPlatformSettings() ?? Application.Current?.PlatformSettings); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _viewModel.Unsubscribe(); + base.OnDetachedFromVisualTree(e); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PointerCanvas.cs b/samples/AvaloniaControlCatalog/Pages/PointerCanvas.cs new file mode 100644 index 000000000..1311a1a14 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PointerCanvas.cs @@ -0,0 +1,236 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Threading; + +namespace ControlCatalog.Pages; + +public class PointerCanvas : Control +{ + private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); + private int _events; + private IDisposable? _statusUpdated; + private Dictionary _pointers = new(); + private PointerPointProperties? _lastProperties; + private PointerUpdateKind? _lastNonOtherUpdateKind; + class PointerPoints + { + struct CanvasPoint + { + public IBrush? Brush; + public Point Point; + public double Radius; + public double? Pressure; + } + + readonly CanvasPoint[] _points = new CanvasPoint[1000]; + int _index; + + public void Render(DrawingContext context, bool drawPoints) + { + CanvasPoint? prev = null; + for (var c = 0; c < _points.Length; c++) + { + var i = (c + _index) % _points.Length; + var pt = _points[i]; + var pressure = (pt.Pressure ?? prev?.Pressure ?? 0.5); + var thickness = pressure * 10; + var radius = pressure * pt.Radius; + + if (drawPoints) + { + if (pt.Brush != null) + { + context.DrawEllipse(pt.Brush, null, pt.Point, radius, radius); + } + } + else + { + if (prev.HasValue && prev.Value.Brush != null && pt.Brush != null + && prev.Value.Pressure != null && pt.Pressure != null) + { + var linePen = new Pen(Brushes.Black, thickness, null, PenLineCap.Round, PenLineJoin.Round); + context.DrawLine(linePen, prev.Value.Point, pt.Point); + } + } + prev = pt; + } + + } + + void AddPoint(Point pt, IBrush brush, double radius, float? pressure = null) + { + _points[_index] = new CanvasPoint { Point = pt, Brush = brush, Radius = radius, Pressure = pressure }; + _index = (_index + 1) % _points.Length; + } + + public void HandleEvent(PointerEventArgs e, Visual v) + { + e.Handled = true; + e.PreventGestureRecognition(); + var currentPoint = e.GetCurrentPoint(v); + if (e.RoutedEvent == PointerPressedEvent) + AddPoint(currentPoint.Position, Brushes.Green, 10); + else if (e.RoutedEvent == PointerReleasedEvent) + AddPoint(currentPoint.Position, Brushes.Red, 10); + else + { + var pts = e.GetIntermediatePoints(v); + for (var c = 0; c < pts.Count; c++) + { + var pt = pts[c]; + AddPoint(pt.Position, c == pts.Count - 1 ? Brushes.Blue : Brushes.Black, + c == pts.Count - 1 ? 5 : 2, pt.Properties.Pressure); + } + } + } + } + + private int _threadSleep; + public static readonly DirectProperty ThreadSleepProperty = + AvaloniaProperty.RegisterDirect(nameof(ThreadSleep), c => c.ThreadSleep, (c, v) => c.ThreadSleep = v); + + public int ThreadSleep + { + get => _threadSleep; + set => SetAndRaise(ThreadSleepProperty, ref _threadSleep, value); + } + + private bool _drawOnlyPoints; + public static readonly DirectProperty DrawOnlyPointsProperty = + AvaloniaProperty.RegisterDirect(nameof(DrawOnlyPoints), c => c.DrawOnlyPoints, (c, v) => c.DrawOnlyPoints = v); + + public bool DrawOnlyPoints + { + get => _drawOnlyPoints; + set => SetAndRaise(DrawOnlyPointsProperty, ref _drawOnlyPoints, value); + } + + private string? _status; + public static readonly DirectProperty StatusProperty = + AvaloniaProperty.RegisterDirect(nameof(Status), c => c.Status, (c, v) => c.Status = v, + defaultBindingMode: Avalonia.Data.BindingMode.TwoWay); + + public string? Status + { + get => _status; + set => SetAndRaise(StatusProperty, ref _status, value); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + _statusUpdated = DispatcherTimer.Run(() => + { + if (_stopwatch.Elapsed.TotalMilliseconds > 250) + { + Status = $@"Events per second: {(_events / _stopwatch.Elapsed.TotalSeconds)} +PointerUpdateKind: {_lastProperties?.PointerUpdateKind} +Last PointerUpdateKind != Other: {_lastNonOtherUpdateKind} +IsLeftButtonPressed: {_lastProperties?.IsLeftButtonPressed} +IsRightButtonPressed: {_lastProperties?.IsRightButtonPressed} +IsMiddleButtonPressed: {_lastProperties?.IsMiddleButtonPressed} +IsXButton1Pressed: {_lastProperties?.IsXButton1Pressed} +IsXButton2Pressed: {_lastProperties?.IsXButton2Pressed} +IsBarrelButtonPressed: {_lastProperties?.IsBarrelButtonPressed} +IsEraser: {_lastProperties?.IsEraser} +IsInverted: {_lastProperties?.IsInverted} +Pressure: {_lastProperties?.Pressure} +XTilt: {_lastProperties?.XTilt} +YTilt: {_lastProperties?.YTilt} +Twist: {_lastProperties?.Twist}"; + _stopwatch.Restart(); + _events = 0; + } + + return true; + }, TimeSpan.FromMilliseconds(10)); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + _statusUpdated?.Dispose(); + } + + void HandleEvent(PointerEventArgs e) + { + _events++; + + if (_threadSleep != 0) + { + Thread.Sleep(_threadSleep); + } + InvalidateVisual(); + + var lastPointer = e.GetCurrentPoint(this); + _lastProperties = lastPointer.Properties; + + if (_lastProperties?.PointerUpdateKind != PointerUpdateKind.Other) + { + _lastNonOtherUpdateKind = _lastProperties?.PointerUpdateKind; + } + + if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch) + { + _pointers.Remove(e.Pointer.Id); + return; + } + + if (e.Pointer.Type != PointerType.Pen + || lastPointer.Properties.Pressure > 0) + { + if (!_pointers.TryGetValue(e.Pointer.Id, out var pt)) + _pointers[e.Pointer.Id] = pt = new PointerPoints(); + pt.HandleEvent(e, this); + } + } + + public override void Render(DrawingContext context) + { + context.FillRectangle(Brushes.White, Bounds); + foreach (var pt in _pointers.Values) + pt.Render(context, _drawOnlyPoints); + base.Render(context); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + if (e.ClickCount == 2) + { + _pointers.Clear(); + InvalidateVisual(); + return; + } + + HandleEvent(e); + base.OnPointerPressed(e); + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + HandleEvent(e); + base.OnPointerMoved(e); + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + HandleEvent(e); + base.OnPointerReleased(e); + } + + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + _lastProperties = null; + base.OnPointerCaptureLost(e); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PointerContactsTab.cs b/samples/AvaloniaControlCatalog/Pages/PointerContactsTab.cs new file mode 100644 index 000000000..323ac685e --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PointerContactsTab.cs @@ -0,0 +1,109 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace ControlCatalog.Pages; + +public class PointerContactsTab : Control +{ + class PointerInfo + { + public Point Point { get; set; } + public Color Color { get; set; } + } + + private static Color[] AllColors = new[] + { + Colors.Aqua, + Colors.Beige, + Colors.Chartreuse, + Colors.Coral, + Colors.Fuchsia, + Colors.Crimson, + Colors.Lavender, + Colors.Orange, + Colors.Orchid, + Colors.ForestGreen, + Colors.SteelBlue, + Colors.PapayaWhip, + Colors.PaleVioletRed, + Colors.Goldenrod, + Colors.Maroon, + Colors.Moccasin, + Colors.Navy, + Colors.Wheat, + Colors.Violet, + Colors.Sienna, + Colors.Indigo, + Colors.Honeydew + }; + + private Dictionary _pointers = new Dictionary(); + + public PointerContactsTab() + { + ClipToBounds = true; + } + + void UpdatePointer(PointerEventArgs e) + { + if (!_pointers.TryGetValue(e.Pointer, out var info)) + { + if (e.RoutedEvent == PointerMovedEvent) + return; + var colors = AllColors.Except(_pointers.Values.Select(c => c.Color)).ToArray(); + var color = colors[new Random().Next(0, colors.Length - 1)]; + _pointers[e.Pointer] = info = new PointerInfo { Color = color }; + } + + info.Point = e.GetPosition(this); + InvalidateVisual(); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + UpdatePointer(e); + e.Pointer.Capture(this); + e.Handled = true; + e.PreventGestureRecognition(); + base.OnPointerPressed(e); + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + UpdatePointer(e); + e.Handled = true; + base.OnPointerMoved(e); + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + _pointers.Remove(e.Pointer); + e.Handled = true; + InvalidateVisual(); + } + + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + _pointers.Remove(e.Pointer); + InvalidateVisual(); + } + + public override void Render(DrawingContext context) + { + context.FillRectangle(Brushes.Transparent, new Rect(default, Bounds.Size)); + foreach (var pt in _pointers.Values) + { + var brush = new ImmutableSolidColorBrush(pt.Color); + + context.DrawEllipse(brush, null, pt.Point, 75, 75); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PointersPage.xaml b/samples/AvaloniaControlCatalog/Pages/PointersPage.xaml new file mode 100644 index 000000000..2f958b901 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PointersPage.xaml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + Capture 1 + + + Capture 2 + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/PointersPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/PointersPage.xaml.cs new file mode 100644 index 000000000..388c8fa38 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PointersPage.xaml.cs @@ -0,0 +1,59 @@ +using System; +using Avalonia.Controls; +using Avalonia.Input; + +namespace ControlCatalog.Pages; + +public partial class PointersPage : TabbedPage +{ + public PointersPage() + { + InitializeComponent(); + } + + private void Border_PointerUpdated(object? sender, PointerEventArgs e) + { + if (sender is Border border && border.Child is TextBlock textBlock) + { + var position = e.GetPosition(border); + textBlock.Text = @$"Type: {e.Pointer.Type} +Captured: {e.Pointer.Captured == sender} +PointerId: {e.Pointer.Id} +Position: {(int)position.X} {(int)position.Y}"; + e.Handled = true; + } + } + + private void Border_PointerCaptureLost(object? sender, PointerCaptureLostEventArgs e) + { + if (sender is Border border && border.Child is TextBlock textBlock) + { + textBlock.Text = @$"Type: {e.Pointer.Type} +Captured: {e.Pointer.Captured == sender} +PointerId: {e.Pointer.Id} +Position: ??? ???"; + e.Handled = true; + + } + } + + private void Border_PointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (e.Pointer.Captured == sender) + { + e.Pointer.Capture(null); + e.Handled = true; + } + else if (e.Pointer.Captured is not null) + { + throw new InvalidOperationException("How?"); + } + } + + private void Border_PointerPressed(object? sender, PointerPressedEventArgs e) + { + e.Pointer.Capture(sender as Border); + e.Handled = true; + e.PreventGestureRecognition(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/ProgressBarPage.xaml b/samples/AvaloniaControlCatalog/Pages/ProgressBarPage.xaml new file mode 100644 index 000000000..3b80b5a6b --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ProgressBarPage.xaml @@ -0,0 +1,43 @@ + + + A progress bar control + + + Maximum + + + + Minimum + + + + Progress Text Format + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/ProgressBarPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/ProgressBarPage.xaml.cs new file mode 100644 index 000000000..244161a61 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ProgressBarPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class ProgressBarPage : ContentPage + { + public ProgressBarPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/RadioButtonPage.xaml b/samples/AvaloniaControlCatalog/Pages/RadioButtonPage.xaml new file mode 100644 index 000000000..efb63ea12 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/RadioButtonPage.xaml @@ -0,0 +1,41 @@ + + + Allows the selection of a single option of many + + + + _Option 1 + O_ption 2 + Op_tion 3 + Disabled + + + Three States: Option 1 + Three States: Option 2 + Three States: Option 3 + Disabled + + + Group A: Option 1 + Group A: Disabled + Group B: Option 1 + Group B: Option 3 + + + Group A: Option 2 + Group B: Option 2 + Group B: Option 4 + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/RadioButtonPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/RadioButtonPage.xaml.cs new file mode 100644 index 000000000..8199a7caf --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/RadioButtonPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class RadioButtonPage : ContentPage + { + public RadioButtonPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/RefreshContainerPage.axaml b/samples/AvaloniaControlCatalog/Pages/RefreshContainerPage.axaml new file mode 100644 index 000000000..5e25d927b --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/RefreshContainerPage.axaml @@ -0,0 +1,45 @@ + + + + + + Used pull direction: + + + + TopToBottom + LeftToRight + RightToLeft + BottomToTop + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageFabPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageFabPage.xaml.cs new file mode 100644 index 000000000..b52bfd4d8 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageFabPage.xaml.cs @@ -0,0 +1,45 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class TabbedPageFabPage : UserControl + { + private static readonly StreamGeometry FeedGeometry = + StreamGeometry.Parse("M12.9942 2.79444C12.4118 2.30208 11.5882 2.30208 11.0058 2.79444L3.50582 9.39444C3.18607 9.66478 3 10.0634 3 10.4828V20.25C3 20.9404 3.55964 21.5 4.25 21.5H8.25C8.94036 21.5 9.5 20.9404 9.5 20.25V14.75C9.5 14.6119 9.61193 14.5 9.75 14.5H14.25C14.3881 14.5 14.5 14.6119 14.5 14.75V20.25C14.5 20.9404 15.0596 21.5 15.75 21.5H19.75C20.4404 21.5 21 20.9404 21 20.25V10.4828C21 10.0634 20.8139 9.66478 20.4942 9.39444L12.9942 2.79444Z"); + private static readonly StreamGeometry DiscoverGeometry = + StreamGeometry.Parse("M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm4.24 5.76-3.03 6.55-6.55 3.03L9.69 10.8l6.55-3.04zM12 13.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"); + private static readonly StreamGeometry AlertsGeometry = + StreamGeometry.Parse("M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"); + private static readonly StreamGeometry ProfileGeometry = + StreamGeometry.Parse("M12 2C9.243 2 7 4.243 7 7s2.243 5 5 5 5-2.243 5-5-2.243-5-5-5zM12 14c-5.523 0-10 3.582-10 8a1 1 0 001 1h18a1 1 0 001-1c0-4.418-4.477-8-10-8z"); + + private int _postCount; + + public TabbedPageFabPage() + { + InitializeComponent(); + SetupIcons(); + + FabButton.Click += OnFabClicked; + TriggerFabButton.Click += OnFabClicked; + } + + private void SetupIcons() + { + FeedPage.Icon = new PathIcon { Data = FeedGeometry }; + DiscoverPage.Icon = new PathIcon { Data = DiscoverGeometry }; + AlertsPage.Icon = new PathIcon { Data = AlertsGeometry }; + ProfilePage.Icon = new PathIcon { Data = ProfileGeometry }; + } + + private void OnFabClicked(object? sender, RoutedEventArgs e) + { + _postCount++; + StatusText.Text = _postCount == 1 + ? "Post created! Check your feed." + : $"{_postCount} posts created!"; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageFirstLookPage.xaml b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageFirstLookPage.xaml new file mode 100644 index 000000000..1dabb6ba0 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageFirstLookPage.xaml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithDrawerPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithDrawerPage.xaml.cs new file mode 100644 index 000000000..88f688d74 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithDrawerPage.xaml.cs @@ -0,0 +1,114 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class TabbedPageWithDrawerPage : UserControl + { + public TabbedPageWithDrawerPage() + { + InitializeComponent(); + Loaded += (_, _) => ShowSection("Home"); + } + + private void OnSectionSelected(object? sender, RoutedEventArgs e) + { + if (sender is Button btn && btn.Tag is string section) + { + ShowSection(section); + DemoDrawer.IsOpen = false; + } + } + + private void ShowSection(string section) + { + SectionHost.Content = section switch + { + "Home" => CreateHomeTabbed(), + _ => CreatePlainPage(section) + }; + } + + private static Control CreatePlainPage(string section) + { + var (subtitle, icon) = section switch + { + "Explore" => ("Discover new content.", "📍"), + "Favorites" => ("Items you've saved.", "❤"), + _ => (string.Empty, string.Empty) + }; + + return new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Spacing = 8, + Children = + { + new TextBlock + { + Text = section, + FontSize = 22, + FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center + }, + new TextBlock + { + Text = subtitle, + FontSize = 13, + Opacity = 0.7, + TextWrapping = TextWrapping.Wrap, + TextAlignment = TextAlignment.Center, + MaxWidth = 300 + } + } + }; + } + + private static TabbedPage CreateHomeTabbed() => new() + { + TabPlacement = TabPlacement.Bottom, + Pages = new[] + { + new ContentPage + { + Header = "Featured", + Content = new TextBlock + { + Text = "Featured content", + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + FontSize = 18, + Opacity = 0.7 + } + }, + new ContentPage + { + Header = "Recent", + Content = new TextBlock + { + Text = "Recent activity", + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + FontSize = 18, + Opacity = 0.7 + } + }, + new ContentPage + { + Header = "Popular", + Content = new TextBlock + { + Text = "Popular right now", + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + FontSize = 18, + Opacity = 0.7 + } + } + } + }; + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml new file mode 100644 index 000000000..22878ec3c --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml.cs new file mode 100644 index 000000000..2b7a5a3a4 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml.cs @@ -0,0 +1,100 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class TabbedPageWithNavigationPage : UserControl + { + private bool _initialized; + + public TabbedPageWithNavigationPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + if (_initialized) + return; + + _initialized = true; + await BrowseNav.PushAsync(CreateListPage("Browse", "Items", BrowseNav), null); + await SearchNav.PushAsync(CreateListPage("Search", "Results", SearchNav), null); + await AccountNav.PushAsync(CreateListPage("Account", "Options", AccountNav), null); + } + + private static ContentPage CreateListPage(string tabName, string listTitle, NavigationPage nav) + { + var list = new ListBox + { + Margin = new Avalonia.Thickness(8), + Items = + { + $"{listTitle} item 1", + $"{listTitle} item 2", + $"{listTitle} item 3", + $"{listTitle} item 4", + $"{listTitle} item 5" + } + }; + + list.SelectionChanged += async (_, args) => + { + if (args.AddedItems.Count == 0) return; + var item = args.AddedItems[0]?.ToString() ?? string.Empty; + list.SelectedItem = null; + + var detail = new ContentPage + { + Header = item, + Content = new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Spacing = 8, + Children = + { + new TextBlock + { + Text = item, + FontSize = 20, + FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center + }, + new TextBlock + { + Text = $"Detail view for \"{item}\" in the {tabName} tab.", + FontSize = 13, + Opacity = 0.7, + TextWrapping = TextWrapping.Wrap, + TextAlignment = Avalonia.Media.TextAlignment.Center, + MaxWidth = 280 + } + } + } + }; + + await nav.PushAsync(detail, nav.PageTransition); + }; + + var page = new ContentPage + { + Header = tabName, + Content = list, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + NavigationPage.SetHasNavigationBar(page, false); + return page; + } + + private void OnPlacementChanged(object? sender, SelectionChangedEventArgs e) + { + if (DemoTabs == null) return; + DemoTabs.TabPlacement = PlacementCombo.SelectedIndex == 0 ? TabPlacement.Top : TabPlacement.Bottom; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/TableViewPage.axaml b/samples/AvaloniaControlCatalog/Pages/TableViewPage.axaml new file mode 100644 index 000000000..bd730eb65 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TableViewPage.axaml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/TableViewPage.axaml.cs b/samples/AvaloniaControlCatalog/Pages/TableViewPage.axaml.cs new file mode 100644 index 000000000..af753dd79 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TableViewPage.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages; + +public partial class TableViewPage : ContentPage +{ + public TableViewPage() + { + InitializeComponent(); + DataContext = new TableViewPageViewModel(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/TextBlockPage.xaml b/samples/AvaloniaControlCatalog/Pages/TextBlockPage.xaml new file mode 100644 index 000000000..5993f65b5 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TextBlockPage.xaml @@ -0,0 +1,146 @@ + + + A control that can display text + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This is a + TextBlock + with several + Span elements, + + using a variety of styles + . + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/TextBlockPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/TextBlockPage.xaml.cs new file mode 100644 index 000000000..d10d255a4 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TextBlockPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class TextBlockPage : ContentPage + { + public TextBlockPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/TextBoxPage.xaml b/samples/AvaloniaControlCatalog/Pages/TextBoxPage.xaml new file mode 100644 index 000000000..f09095c2f --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TextBoxPage.xaml @@ -0,0 +1,106 @@ + + + + + + + + + + Custom context flyout + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/TextBoxPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/TextBoxPage.xaml.cs new file mode 100644 index 000000000..3c38a05c5 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TextBoxPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class TextBoxPage : ContentPage + { + public TextBoxPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/ThemePage.axaml b/samples/AvaloniaControlCatalog/Pages/ThemePage.axaml new file mode 100644 index 000000000..ccc5769fe --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ThemePage.axaml @@ -0,0 +1,80 @@ + + + + + + + Black + + + White + + + #ffe5ea + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Nested ToolTips + + + + ToolTip replaced on the fly + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/ToolTipPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/ToolTipPage.xaml.cs new file mode 100644 index 000000000..e436407dc --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ToolTipPage.xaml.cs @@ -0,0 +1,42 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class ToolTipPage : ContentPage + { + public ToolTipPage() + { + InitializeComponent(); + } + + private void ToolTipOpening(object? sender, CancelRoutedEventArgs args) + { + ((Control)args.Source!).SetValue(ToolTip.TipProperty, "New tip set from ToolTipOpening."); + } + + public void CustomPlacementCallback(CustomPopupPlacement placement) + { + var r = new Random().Next(); + + placement.Anchor = (r % 4) switch + { + 1 => PopupAnchor.Top, + 2 => PopupAnchor.Left, + 3 => PopupAnchor.Right, + _ => PopupAnchor.Bottom, + }; + placement.Gravity = (r % 4) switch + { + 1 => PopupGravity.Top, + 2 => PopupGravity.Left, + 3 => PopupGravity.Right, + _ => PopupGravity.Bottom, + }; + placement.Offset = new Point(r % 20, r % 20); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/TransitioningContentControlPage.axaml b/samples/AvaloniaControlCatalog/Pages/TransitioningContentControlPage.axaml new file mode 100644 index 000000000..5435ed278 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TransitioningContentControlPage.axaml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + 8 + + + + + The TransitioningContentControl control allows you to show a page transition whenever the Content changes. + + + + + + + + + + + + + + + + + + + + + + + + + Single + Multiple + Toggle + AlwaysSelected + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/TreeViewPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/TreeViewPage.xaml.cs new file mode 100644 index 000000000..d03590feb --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TreeViewPage.xaml.cs @@ -0,0 +1,14 @@ +using Avalonia.Controls; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public partial class TreeViewPage : ContentPage + { + public TreeViewPage() + { + InitializeComponent(); + DataContext = new TreeViewPageViewModel(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/ViewboxPage.xaml b/samples/AvaloniaControlCatalog/Pages/ViewboxPage.xaml new file mode 100644 index 000000000..3848e405d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ViewboxPage.xaml @@ -0,0 +1,54 @@ + + + + + A control used to scale single child. + + + + + + + + + + + + + + + + + + + + + + Uniform + UniformToFill + Fill + None + + + + + + + + Both + DownOnly + UpOnly + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/ViewboxPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/ViewboxPage.xaml.cs new file mode 100644 index 000000000..f7b1ea3f7 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ViewboxPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class ViewboxPage : ContentPage + { + public ViewboxPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/WindowCustomizationsPage.xaml b/samples/AvaloniaControlCatalog/Pages/WindowCustomizationsPage.xaml new file mode 100644 index 000000000..f304f7889 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/WindowCustomizationsPage.xaml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/WindowCustomizationsPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/WindowCustomizationsPage.xaml.cs new file mode 100644 index 000000000..2ee8677eb --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/WindowCustomizationsPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class WindowCustomizationsPage : ContentPage + { + public WindowCustomizationsPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/WrapPanelPage.axaml b/samples/AvaloniaControlCatalog/Pages/WrapPanelPage.axaml new file mode 100644 index 000000000..a10d6106f --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/WrapPanelPage.axaml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/WrapPanelPage.axaml.cs b/samples/AvaloniaControlCatalog/Pages/WrapPanelPage.axaml.cs new file mode 100644 index 000000000..802b10b67 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/WrapPanelPage.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages; + +public partial class WrapPanelPage : ContentPage +{ + public WrapPanelPage() + { + InitializeComponent(); + DataContext = new WrapPanelPageViewModel(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/teapot.bin b/samples/AvaloniaControlCatalog/Pages/teapot.bin new file mode 100644 index 000000000..589eeb912 Binary files /dev/null and b/samples/AvaloniaControlCatalog/Pages/teapot.bin differ diff --git a/samples/AvaloniaControlCatalog/Program.cs b/samples/AvaloniaControlCatalog/Program.cs new file mode 100644 index 000000000..32d62a865 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Program.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using Avalonia; +using ControlCatalog; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Renderers.Avalonia; + +namespace AvaloniaControlCatalog; + +internal static class Program +{ + [STAThread] + public static void Main(string[] args) + => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + + private static AppBuilder BuildAvaloniaApp() + { + DrawingBackendMode backendMode = Environment.GetEnvironmentVariable("IMAGESHARP_DRAWING_BACKEND")?.ToLowerInvariant() switch + { + "cpu" => DrawingBackendMode.Cpu, + "webgpu" => DrawingBackendMode.WebGpu, + _ => DrawingBackendMode.Auto + }; + + if (int.TryParse(Environment.GetEnvironmentVariable("IMAGESHARP_DRAWING_MAX_PARALLELISM"), out int maxParallelism)) + { + Configuration.Default.MaxDegreeOfParallelism = maxParallelism; + } + + AppBuilder builder = AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont(); + + // Temporary backend switch for visual comparison: ISD_BACKEND=skia uses Avalonia's default (Skia). + if (!string.Equals(Environment.GetEnvironmentVariable("ISD_BACKEND"), "skia", StringComparison.OrdinalIgnoreCase)) + { + builder = builder.UseImageSharpDrawing(backendMode); + } + + return builder; + } +} diff --git a/samples/AvaloniaControlCatalog/ScrollPage.xaml b/samples/AvaloniaControlCatalog/ScrollPage.xaml new file mode 100644 index 000000000..8c6157018 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ScrollPage.xaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/TransparentStyles.axaml b/samples/AvaloniaControlCatalog/TransparentStyles.axaml new file mode 100644 index 000000000..adf924462 --- /dev/null +++ b/samples/AvaloniaControlCatalog/TransparentStyles.axaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/TransparentStyles.axaml.cs b/samples/AvaloniaControlCatalog/TransparentStyles.axaml.cs new file mode 100644 index 000000000..85310f4b0 --- /dev/null +++ b/samples/AvaloniaControlCatalog/TransparentStyles.axaml.cs @@ -0,0 +1,5 @@ +using Avalonia.Styling; + +namespace ControlCatalog; + +public class TransparentStyles : Styles; diff --git a/samples/AvaloniaControlCatalog/ViewModels/ApplicationViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/ApplicationViewModel.cs new file mode 100644 index 000000000..509feca5e --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/ApplicationViewModel.cs @@ -0,0 +1,26 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class ApplicationViewModel : ViewModelBase + { + public ApplicationViewModel() + { + ExitCommand = MiniCommand.Create(() => + { + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) + { + lifetime.Shutdown(); + } + }); + + RestoreDefault = MiniCommand.Create(() => { }); + } + + public MiniCommand ExitCommand { get; } + + public MiniCommand RestoreDefault { get; } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/ComboBoxPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/ComboBoxPageViewModel.cs new file mode 100644 index 000000000..7d415e5d6 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/ComboBoxPageViewModel.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Selection; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class ComboBoxPageViewModel : ViewModelBase + { + private bool _wrapSelection; + private string _textValue = string.Empty; + private IdAndName? _selectedItem = null; + + public bool WrapSelection + { + get => _wrapSelection; + set => this.RaiseAndSetIfChanged(ref _wrapSelection, value); + } + + public string TextValue + { + get => _textValue; + set => this.RaiseAndSetIfChanged(ref _textValue, value); + } + + public IdAndName? SelectedItem + { + get => _selectedItem; + set => this.RaiseAndSetIfChanged(ref _selectedItem, value); + } + + public ObservableCollection Values { get; set; } = new ObservableCollection + { + new IdAndName(){ Id = "Id 1", Name = "Name 1", SearchText = "A" }, + new IdAndName(){ Id = "Id 2", Name = "Name 2", SearchText = "B" }, + new IdAndName(){ Id = "Id 3", Name = "Name 3", SearchText = "C" }, + new IdAndName(){ Id = "Id 4", Name = "Name 4", SearchText = "D" }, + new IdAndName(){ Id = "Id 5", Name = "Name 5", SearchText = "E" }, + }; + } + + public class IdAndName + { + public string? Id { get; set; } + public string? Name { get; set; } + public string? SearchText { get; set; } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/ContextPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/ContextPageViewModel.cs new file mode 100644 index 000000000..03156f696 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/ContextPageViewModel.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.VisualTree; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class ContextPageViewModel + { + public Control? View { get; set; } + public ContextPageViewModel() + { + OpenCommand = MiniCommand.CreateFromTask(Open); + SaveCommand = MiniCommand.Create(Save); + OpenRecentCommand = MiniCommand.Create(OpenRecent); + + MenuItems = new[] + { + new MenuItemViewModel { Header = "_Open...", Command = OpenCommand }, + new MenuItemViewModel { Header = "Save", Command = SaveCommand }, + new MenuItemViewModel { Header = "-" }, + new MenuItemViewModel + { + Header = "Recent", + Items = new[] + { + new MenuItemViewModel + { + Header = "File1.txt", + Command = OpenRecentCommand, + CommandParameter = @"c:\foo\File1.txt" + }, + new MenuItemViewModel + { + Header = "File2.txt", + Command = OpenRecentCommand, + CommandParameter = @"c:\foo\File2.txt" + }, + } + }, + }; + } + + public IReadOnlyList MenuItems { get; set; } + public MiniCommand OpenCommand { get; } + public MiniCommand SaveCommand { get; } + public MiniCommand OpenRecentCommand { get; } + + public async Task Open() + { + var window = TopLevel.GetTopLevel(View) as Window; + if (window == null) + return; + + var result = await window.StorageProvider.OpenFilePickerAsync(new Avalonia.Platform.Storage.FilePickerOpenOptions() { AllowMultiple = true }); + + foreach (var file in result) + { + System.Diagnostics.Debug.WriteLine($"Opened: {file.Name}"); + } + } + + public void Save() + { + System.Diagnostics.Debug.WriteLine("Save"); + } + + public void OpenRecent(string path) + { + System.Diagnostics.Debug.WriteLine($"Open recent: {path}"); + } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/CursorPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/CursorPageViewModel.cs new file mode 100644 index 000000000..fc0147382 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/CursorPageViewModel.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia; +using Avalonia.Input; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class CursorPageViewModel : ViewModelBase + { + public CursorPageViewModel() + { + StandardCursors = Enum.GetValues() + .Select(x => new StandardCursorModel(x)) + .ToList(); + + var s = AssetLoader.Open(new Uri("avares://ControlCatalog/Assets/avalonia-32.png")); + var bitmap = new Bitmap(s); + CustomCursor = new Cursor(bitmap, new PixelPoint(16, 16)); + } + + public IEnumerable StandardCursors { get; } + + public Cursor CustomCursor { get; } + } + + public class StandardCursorModel + { + public StandardCursorModel(StandardCursorType type) + { + Type = type; + Cursor = new Cursor(type); + } + + public StandardCursorType Type { get; } + + public Cursor Cursor { get; } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/DataValidationViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/DataValidationViewModel.cs new file mode 100644 index 000000000..a50c9227e --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/DataValidationViewModel.cs @@ -0,0 +1,47 @@ +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using MiniMvvm; + +namespace ControlCatalog.ViewModels; + +public class DataValidationViewModel : ViewModelBase +{ + private string? _DataAnnotationsSample; + + [Required] + [EmailAddress] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Does not apply to 'string' type.")] + [MinLength(5)] + public string? DataAnnotationsSample + { + get => _DataAnnotationsSample; + set => RaiseAndSetIfChanged(ref _DataAnnotationsSample, value); + } + + public Func Converter { get; } = new Func(o => + { + return $"Error: {o}"; + }); + + + private string? _ExceptionInsideSetterSample; + + public string? ExceptionInsideSetterSample + { + get => _ExceptionInsideSetterSample; + set + { + if (value is null || value.Length < 5) + throw new ArgumentOutOfRangeException(nameof(value), "Give me 5 or more letter please :-)"); + + RaiseAndSetIfChanged(ref _ExceptionInsideSetterSample, value); + } + } + + public Func ExceptionConverter { get; } = new Func(o => + { + return o is Exception ex ? $"Huh, there was an Exception: {ex.Message}" : "Something went really wrong!"; + }); +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/ExpanderPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/ExpanderPageViewModel.cs new file mode 100644 index 000000000..aa7f710ee --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/ExpanderPageViewModel.cs @@ -0,0 +1,27 @@ +using Avalonia; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class ExpanderPageViewModel : ViewModelBase + { + private object _cornerRadius = AvaloniaProperty.UnsetValue; + private bool _rounded; + + public object CornerRadius + { + get => _cornerRadius; + private set => RaiseAndSetIfChanged(ref _cornerRadius, value); + } + + public bool Rounded + { + get => _rounded; + set + { + if (RaiseAndSetIfChanged(ref _rounded, value)) + CornerRadius = _rounded ? new CornerRadius(25) : AvaloniaProperty.UnsetValue; + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/ListBoxPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/ListBoxPageViewModel.cs new file mode 100644 index 000000000..a1dd17af5 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/ListBoxPageViewModel.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Selection; +using ControlCatalog.Pages; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class ListBoxPageViewModel : ViewModelBase + { + private bool _multiple; + private bool _toggle; + private bool _alwaysSelected; + private bool _autoScrollToSelectedItem = true; + private bool _wrapSelection; + private int _counter; + private IObservable _selectionMode; + + public ListBoxPageViewModel() + { + Items = new ObservableCollection(Enumerable.Range(1, 10000).Select(i => GenerateItem())); + + Selection = new SelectionModel(); + Selection.Select(1); + + _selectionMode = this.WhenAnyValue( + x => x.Multiple, + x => x.Toggle, + x => x.AlwaysSelected, + (m, t, a) => + (m ? Avalonia.Controls.SelectionMode.Multiple : 0) | + (t ? Avalonia.Controls.SelectionMode.Toggle : 0) | + (a ? Avalonia.Controls.SelectionMode.AlwaysSelected : 0)); + + AddItemCommand = MiniCommand.Create(() => + { + var item = GenerateItem(); + Items.Add(item); + Selection.Clear(); + Selection.Select(Items.Count - 1); + }); + + RemoveItemCommand = MiniCommand.Create(() => + { + var items = Selection.SelectedItems.ToList(); + + foreach (var item in items) + { + Items.Remove(item!); + } + }); + + SelectRandomItemCommand = MiniCommand.Create(() => + { + var random = new Random(); + + using (Selection.BatchUpdate()) + { + Selection.Clear(); + Selection.Select(random.Next(Items.Count - 1)); + } + }); + } + + public ObservableCollection Items { get; } + public SelectionModel Selection { get; } + public IObservable SelectionMode => _selectionMode; + + public bool Multiple + { + get => _multiple; + set => this.RaiseAndSetIfChanged(ref _multiple, value); + } + + public bool Toggle + { + get => _toggle; + set => this.RaiseAndSetIfChanged(ref _toggle, value); + } + + public bool AlwaysSelected + { + get => _alwaysSelected; + set => this.RaiseAndSetIfChanged(ref _alwaysSelected, value); + } + + public bool AutoScrollToSelectedItem + { + get => _autoScrollToSelectedItem; + set => this.RaiseAndSetIfChanged(ref _autoScrollToSelectedItem, value); + } + + public bool WrapSelection + { + get => _wrapSelection; + set => this.RaiseAndSetIfChanged(ref _wrapSelection, value); + } + + public MiniCommand AddItemCommand { get; } + public MiniCommand RemoveItemCommand { get; } + public MiniCommand SelectRandomItemCommand { get; } + + private ItemModel GenerateItem() => new ItemModel(_counter++); + } + + /// + /// An Item model for the + /// + public class ItemModel + { + /// + /// Creates a new ItemModel with the given ID + /// + /// The ID to display + public ItemModel(int id) + { + ID = id; + } + + /// + /// The ID of this Item + /// + public int ID { get; } + + public override string ToString() + { + return $"Item {ID}"; + } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/MainWindowViewModel.cs new file mode 100644 index 000000000..445261950 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,202 @@ +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls.Chrome; +using Avalonia.Dialogs; +using System; +using System.ComponentModel.DataAnnotations; +using System.Runtime.CompilerServices; +using Avalonia; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + partial class MainWindowViewModel : ViewModelBase + { + private WindowState _windowState; + private WindowState[] _windowStates = Array.Empty(); + private bool _extendClientAreaEnabled; + private double _titleBarHeight; + private bool _isSystemBarVisible; + private bool _displayEdgeToEdge; + private Thickness _safeAreaPadding; + private bool _canResize; + private bool _canMinimize; + private bool _canMaximize; + private int _selectedDecorationIndex; + + public MainWindowViewModel() + { + AboutCommand = MiniCommand.CreateFromTask(async () => + { + var dialog = new AboutAvaloniaDialog(); + + if ((App.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow is { } mainWindow) + { + await dialog.ShowDialog(mainWindow); + } + }); + ExitCommand = MiniCommand.Create(() => + { + (App.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.Shutdown(); + }); + + WindowState = WindowState.Normal; + + WindowStates = new WindowState[] + { + WindowState.Minimized, + WindowState.Normal, + WindowState.Maximized, + WindowState.FullScreen, + }; + + TitleBarHeight = -1; + CanResize = true; + CanMinimize = true; + CanMaximize = true; + + Filter(); + } + + public bool ExtendClientAreaEnabled + { + get { return _extendClientAreaEnabled; } + set { RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value); } + } + + public double TitleBarHeight + { + get { return _titleBarHeight; } + set { RaiseAndSetIfChanged(ref _titleBarHeight, value); } + } + + public WindowState WindowState + { + get { return _windowState; } + set { RaiseAndSetIfChanged(ref _windowState, value); } + } + + public WindowState[] WindowStates + { + get { return _windowStates; } + set { RaiseAndSetIfChanged(ref _windowStates, value); } + } + + public bool IsSystemBarVisible + { + get { return _isSystemBarVisible; } + set { RaiseAndSetIfChanged(ref _isSystemBarVisible, value); } + } + + public bool DisplayEdgeToEdge + { + get { return _displayEdgeToEdge; } + set { RaiseAndSetIfChanged(ref _displayEdgeToEdge, value); } + } + + public Thickness SafeAreaPadding + { + get { return _safeAreaPadding; } + set { RaiseAndSetIfChanged(ref _safeAreaPadding, value); } + } + + public bool CanResize + { + get { return _canResize; } + set { RaiseAndSetIfChanged(ref _canResize, value); } + } + + public bool CanMinimize + { + get { return _canMinimize; } + set { RaiseAndSetIfChanged(ref _canMinimize, value); } + } + + public bool CanMaximize + { + get { return _canMaximize; } + set { RaiseAndSetIfChanged(ref _canMaximize, value); } + } + + public int SelectedDecorationIndex + { + get { return _selectedDecorationIndex; } + set { RaiseAndSetIfChanged(ref _selectedDecorationIndex, value); } + } + + public TitleBarDecorations TitleBarDecorations + { + get; + set { RaiseAndSetIfChanged(ref field, value); } + } = TitleBarDecorations.All; + + public bool ShowTitle + { + get => HasTitleBarDecoration(TitleBarDecorations.Title); + set => SetTitleBarDecoration(TitleBarDecorations.Title, value); + } + + public bool ShowFullScreenButton + { + get => HasTitleBarDecoration(TitleBarDecorations.FullScreenButton); + set => SetTitleBarDecoration(TitleBarDecorations.FullScreenButton, value); + } + + public bool ShowMinimizeButton + { + get => HasTitleBarDecoration(TitleBarDecorations.MinimizeButton); + set => SetTitleBarDecoration(TitleBarDecorations.MinimizeButton, value); + } + + public bool ShowMaximizeButton + { + get => HasTitleBarDecoration(TitleBarDecorations.MaximizeButton); + set => SetTitleBarDecoration(TitleBarDecorations.MaximizeButton, value); + } + + public bool ShowCloseButton + { + get => HasTitleBarDecoration(TitleBarDecorations.CloseButton); + set => SetTitleBarDecoration(TitleBarDecorations.CloseButton, value); + } + + private bool HasTitleBarDecoration(TitleBarDecorations decoration) + => (TitleBarDecorations & decoration) != 0; + + private void SetTitleBarDecoration(TitleBarDecorations decoration, bool value, [CallerMemberName] string? propertyName = null) + { + var newDecorations = value ? TitleBarDecorations | decoration : TitleBarDecorations & ~decoration; + if (newDecorations == TitleBarDecorations) + return; + + TitleBarDecorations = newDecorations; + RaisePropertyChanged(propertyName); + RaisePropertyChanged(nameof(TitleBarDecorations)); + } + + public MiniCommand AboutCommand { get; } + + public MiniCommand ExitCommand { get; } + + private DateTime? _validatedDateExample; + + /// + /// A required DateTime which should demonstrate validation for the DateTimePicker + /// + [Required] + public DateTime? ValidatedDateExample + { + get => _validatedDateExample; + set => RaiseAndSetIfChanged(ref _validatedDateExample, value); + } + + public Win32Properties.WindowCornerPreference[] Win32WindowCornerPreferences { get; } = + Enum.GetValues(); + + public Win32Properties.WindowCornerPreference Win32WindowCornerPreference + { + get; + set { RaiseAndSetIfChanged(ref field, value); } + } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/MainWindowViewModel_PageList.cs b/samples/AvaloniaControlCatalog/ViewModels/MainWindowViewModel_PageList.cs new file mode 100644 index 000000000..61476a040 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/MainWindowViewModel_PageList.cs @@ -0,0 +1,250 @@ +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Dialogs; +using System; +using System.ComponentModel.DataAnnotations; +using Avalonia; +using MiniMvvm; +using Avalonia.Collections; +using ControlCatalog.Pages; +using System.Globalization; +using System.Text; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Linq; + +namespace ControlCatalog.ViewModels +{ + partial class MainWindowViewModel + { + private int _selectedPageIndex; + private bool _isDrawerOpened = true; + private bool _ignoreListChange = false; + private string? _query = ""; + private PageItem? _currentItem; + private SplitViewDisplayMode _displayMode; + + private List _items = new() + { + new PageItem("Composition", () => new CompositionPage(), Icons.Layers), + new PageItem("Accelerator", () => new AcceleratorPage(), Icons.Keyboard), + new PageItem("Acrylic", () => new AcrylicPage(), Icons.Blur), + new PageItem("AdornerLayer", () => new AdornerLayerPage(), Icons.Sparkle), + new PageItem("AutoCompleteBox",() => new AutoCompleteBoxPage(), Icons.TextInput), + new PageItem("Border",() => new BorderPage(), Icons.Border), + new PageItem("BitmapCache",() => new BitmapCachePage(), Icons.Lightning), + new PageItem("Buttons",() => new ButtonsPage(), Icons.CursorClick), + new PageItem("ButtonSpinner",() => new ButtonSpinnerPage(), Icons.Spinner), + new PageItem("Calendar",() => new CalendarPage(), Icons.Calendar), + new PageItem("Canvas",() => new CanvasPage(), Icons.Canvas), + new PageItem("CommandBar",() => new CommandBarPage(), Icons.Terminal), + new PageItem("Carousel",() => new Pages.CarouselPage(), Icons.Slides), + new PageItem("CarouselPage",() => new CarouselDemoPage(), Icons.Slides), + new PageItem("CheckBox",() => new CheckBoxPage(), Icons.Checkbox), + new PageItem("Clipboard",() => new ClipboardPage(), Icons.Clipboard), + new PageItem("ColorPicker",() => new ColorPickerPage(), Icons.Palette), + new PageItem("ComboBox",() => new ComboBoxPage(), Icons.Dropdown), + new PageItem("Container Queries",() => new ContainerQueryPage(), Icons.Container), + new PageItem("ContentPage",() => new ContentDemoPage(), Icons.Document), + new PageItem("ContextFlyout",() => new ContextFlyoutPage(), Icons.Menu), + new PageItem("ContextMenu",() => new ContextMenuPage(), Icons.Menu), + new PageItem("Cursor",() => new CursorPage(), Icons.Cursor), + new PageItem("Custom Drawing",() => new CustomDrawing(), Icons.Brush), + new PageItem("Data Validation",() => new DataValidationPage(), Icons.Shield), + new PageItem("Date/Time Picker",() => new DateTimePickerPage(), Icons.Clock), + new PageItem("CalendarDatePicker",() => new CalendarDatePickerPage(), Icons.Calendar), + new PageItem("Dialogs",() => new DialogsPage(), Icons.Dialog), + new PageItem("Drag+Drop",() => new DragAndDropPage(), Icons.DragDrop), + new PageItem("DrawerPage",() => new DrawerDemoPage(), Icons.Drawer), + new PageItem("Expander",() => new ExpanderPage(), Icons.Expand), + new PageItem("Flyouts",() => new FlyoutsPage(), Icons.Flyout), + new PageItem("Focus",() => new FocusPage(), Icons.Target), + new PageItem("Gestures",() => new GesturePage(), Icons.Gesture), + new PageItem("Image",() => new ImagePage(), Icons.Image), + new PageItem("Label",() => new LabelsPage(), Icons.Tag), + new PageItem("LayoutTransformControl",() => new LayoutTransformControlPage(), Icons.Transform), + new PageItem("ListBox",() => new ListBoxPage(), Icons.List), + new PageItem("Menu",() => new MenuPage(), Icons.Menu), + new PageItem("NavigationPage",() => new NavigationDemoPage(), Icons.Navigation), + new PageItem("Notifications",() => new NotificationsPage(), Icons.Bell), + new PageItem("NumericUpDown",() => new NumericUpDownPage(), Icons.Number), + new PageItem("OpenGL",() => new OpenGlPage(), Icons.Cube3D), + new PageItem("OpenGL Lease",() => new OpenGlLeasePage(), Icons.Cube3D), + new PageItem("PipsPager",() => new PipsPagerPage(), Icons.HorizontalDots), + new PageItem("Platform Information",() => new PlatformInfoPage(), Icons.Info), + new PageItem("Platform Settings",() => new PlatformSettingsPage(), Icons.Tune), + new PageItem("Pointers",() => new PointersPage(), Icons.Cursor), + new PageItem("ProgressBar",() => new ProgressBarPage(), Icons.Progress), + new PageItem("RadioButton",() => new RadioButtonPage(), Icons.Radio), + new PageItem("RefreshContainer",() => new RefreshContainerPage(), Icons.Refresh), + new PageItem("RelativePanel",() => new RelativePanelPage(), Icons.Layout), + new PageItem("ScrollViewer",() => new ScrollViewerPage(), Icons.Scroll), + new PageItem("Slider",() => new SliderPage(), Icons.Tune), + new PageItem("SplitView",() => new SplitViewPage(), Icons.Split), + new PageItem("TabbedPage",() => new TabbedDemoPage(), Icons.Tab), + new PageItem("TabControl",() => new TabControlPage(), Icons.Tab), + new PageItem("TabStrip",() => new TabStripPage(), Icons.Tab), + new PageItem("TableView",() => new TableViewPage(), Icons.Grid), + new PageItem("TextBox",() => new TextBoxPage(), Icons.TextInput), + new PageItem("TextBlock",() => new TextBlockPage(), Icons.TextInput), + new PageItem("Theme Variants",() => new ThemePage(), Icons.Theme), + new PageItem("ToggleSwitch",() => new ToggleSwitchPage(), Icons.Toggle), + new PageItem("ToolTip",() => new ToolTipPage(), Icons.Tooltip), + new PageItem("TransitioningContentControl",() => new TransitioningContentControlPage(), Icons.Transition), + new PageItem("TreeView",() => new TreeViewPage(), Icons.Tree), + new PageItem("Viewbox",() => new ViewboxPage(), Icons.Viewbox), + new PageItem("WrapPanel",() => new WrapPanelPage(), Icons.Layout), + new PageItem("Native Embed",() => new NativeEmbedPage(), Icons.Puzzle), + new PageItem("Window Customizations",() => new WindowCustomizationsPage(), Icons.Window), + new PageItem("HeaderedContentControl",() => new HeaderedContentPage(), Icons.Header), + new PageItem("Screens",() => new ScreenPage(), Icons.Monitor), + }; + + public AvaloniaList Pages { get; } = new AvaloniaList(); + + public void Filter(string? query = "") + { + try + { + _ignoreListChange = true; + Pages.Clear(); + + if (string.IsNullOrWhiteSpace(query)) + { + Pages.AddRange(_items); + } + else + { + var querySearchKey = PageItem.CreateSearchKey(query); + + if (querySearchKey.Length == 0) + { + Pages.AddRange(_items); + } + else + { + foreach (var item in _items) + { + if (item.MatchesSearch(querySearchKey)) + { + Pages.Add(item); + } + } + } + } + } + finally + { + _ignoreListChange = false; + if (_currentItem != null) + { + var newIndex = Pages.IndexOf(_currentItem); + if (newIndex != -1) + { + SelectedPageIndex = newIndex; + } + } + } + } + + public INavigation? Navigator { get; internal set; } + + public int SelectedPageIndex + { + get { return _selectedPageIndex; } + set + { + RaiseAndSetIfChanged(ref _selectedPageIndex, value); + + if (!_ignoreListChange) + { + NavigateTo(_selectedPageIndex); + + if (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay) + IsDrawerOpened = false; + } + } + } + + public bool IsDrawerOpened + { + get { return _isDrawerOpened; } + set { RaiseAndSetIfChanged(ref _isDrawerOpened, value); } + } + + public SplitViewDisplayMode DisplayMode + { + get { return _displayMode; } + set { RaiseAndSetIfChanged(ref _displayMode, value); } + } + + public string? Query + { + get { return _query; } + set + { + RaiseAndSetIfChanged(ref _query, value); + + Filter(value); + } + } + + private async void NavigateTo(int pageIndex) + { + if (pageIndex < 0 || pageIndex >= Pages.Count || Navigator is null) + return; + + var item = Pages[pageIndex]; + + if (item != null) + { + var view = item.Factory(); + if (view is Page page && view.GetType() != Navigator.NavigationStack.LastOrDefault()?.GetType()) + { + _currentItem = item; + await Navigator.ReplaceAsync(page); + } + } + } + } + + class PageItem(string header, Func factory, string? iconData = null) + { + public string Header { get; } = header; + public Func Factory { get; } = factory; + public string? IconData { get; } = iconData; + private string SearchKey { get; } = CreateSearchKey(header); + + public bool IsVisible { get; set; } = true; + + public bool MatchesSearch(string searchKey) + { + return SearchKey.Contains(searchKey, StringComparison.Ordinal); + } + + public static string CreateSearchKey(string value) + { + var normalizedValue = value.Normalize(NormalizationForm.FormKD); + var builder = new StringBuilder(normalizedValue.Length); + + foreach (var c in normalizedValue) + { + var category = CharUnicodeInfo.GetUnicodeCategory(c); + + if (category is UnicodeCategory.NonSpacingMark or + UnicodeCategory.SpacingCombiningMark or + UnicodeCategory.EnclosingMark) + { + continue; + } + + if (char.IsLetterOrDigit(c)) + { + builder.Append(char.ToUpperInvariant(c)); + } + } + + return builder.ToString(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/MenuItemViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/MenuItemViewModel.cs new file mode 100644 index 000000000..190bb4c8a --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/MenuItemViewModel.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Windows.Input; + +namespace ControlCatalog.ViewModels +{ + public class MenuItemViewModel + { + public string? Header { get; set; } + public ICommand? Command { get; set; } + public object? CommandParameter { get; set; } + public IList? Items { get; set; } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/MenuPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/MenuPageViewModel.cs new file mode 100644 index 000000000..f6b406d0d --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/MenuPageViewModel.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.VisualTree; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class MenuPageViewModel + { + public Control? View { get; set; } + public MenuPageViewModel() + { + OpenCommand = MiniCommand.CreateFromTask(Open); + SaveCommand = MiniCommand.Create(Save); + OpenRecentCommand = MiniCommand.Create(OpenRecent); + + var recentItems = new[] + { + new MenuItemViewModel + { + Header = "File1.txt", + Command = OpenRecentCommand, + CommandParameter = @"c:\foo\File1.txt" + }, + new MenuItemViewModel + { + Header = "File2.txt", + Command = OpenRecentCommand, + CommandParameter = @"c:\foo\File2.txt" + }, + }; + + RecentItems = recentItems; + MenuItems = new[] + { + new MenuItemViewModel + { + Header = "_File", + Items = new[] + { + new MenuItemViewModel { Header = "O_pen...", Command = OpenCommand }, + new MenuItemViewModel { Header = "Save", Command = SaveCommand }, + new MenuItemViewModel { Header = "-" }, + new MenuItemViewModel + { + Header = "Recent", + Items = recentItems + }, + } + }, + new MenuItemViewModel + { + Header = "_Edit", + Items = new[] + { + new MenuItemViewModel { Header = "_Copy" }, + new MenuItemViewModel { Header = "_Paste" }, + } + } + }; + } + + public IReadOnlyList MenuItems { get; set; } + public IReadOnlyList RecentItems { get; set; } + public MiniCommand OpenCommand { get; } + public MiniCommand SaveCommand { get; } + public MiniCommand OpenRecentCommand { get; } + + public async Task Open() + { + var window = TopLevel.GetTopLevel(View) as Window; + if (window == null) + return; + var result = await window.StorageProvider.OpenFilePickerAsync(new Avalonia.Platform.Storage.FilePickerOpenOptions() { AllowMultiple = true }); + + if (result != null) + { + foreach (var file in result) + { + System.Diagnostics.Debug.WriteLine($"Opened: {file.Name}"); + } + } + } + + public void Save() + { + System.Diagnostics.Debug.WriteLine("Save"); + } + + public void OpenRecent(string path) + { + System.Diagnostics.Debug.WriteLine($"Open recent: {path}"); + } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/NotificationViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/NotificationViewModel.cs new file mode 100644 index 000000000..40af6e033 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/NotificationViewModel.cs @@ -0,0 +1,44 @@ +using Avalonia.Controls.Notifications; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class NotificationViewModel + { + public WindowNotificationManager? NotificationManager { get; set; } + + public NotificationViewModel() + { + ShowCustomManagedNotificationCommand = MiniCommand.Create(() => + { + NotificationManager?.Show(new NotificationViewModel() { Title = "Hey There!", Message = "Did you know that Avalonia now supports Custom In-Window Notifications?" , NotificationManager = NotificationManager}, NotificationType.Warning); + }); + + ShowManagedNotificationCommand = MiniCommand.Create(() => + { + NotificationManager?.Show(new Avalonia.Controls.Notifications.Notification("Welcome", "Avalonia now supports Notifications.", NotificationType.Information)); + }); + + YesCommand = MiniCommand.Create(() => + { + NotificationManager?.Show(new Avalonia.Controls.Notifications.Notification("Avalonia Notifications", "Start adding notifications to your app today.")); + }); + + NoCommand = MiniCommand.Create(() => + { + NotificationManager?.Show(new Avalonia.Controls.Notifications.Notification("Avalonia Notifications", "Start adding notifications to your app today. To find out more visit...")); + }); + } + + public string? Title { get; set; } + public string? Message { get; set; } + + public MiniCommand YesCommand { get; } + + public MiniCommand NoCommand { get; } + + public MiniCommand ShowCustomManagedNotificationCommand { get; } + + public MiniCommand ShowManagedNotificationCommand { get; } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/PlatformInformationViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/PlatformInformationViewModel.cs new file mode 100644 index 000000000..21c87bab0 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/PlatformInformationViewModel.cs @@ -0,0 +1,58 @@ +using System.Runtime.InteropServices; +using Avalonia; +using Avalonia.Platform; +using MiniMvvm; + +namespace ControlCatalog.ViewModels; + +public class PlatformInformationViewModel : ViewModelBase +{ + public PlatformInformationViewModel() + { + /* NOTE: + * ------------ + * The below API is not meant to be used in production Apps. + * If you need to consume this info, please use: + * - OperatingSystem ( https://learn.microsoft.com/en-us/dotnet/api/system.operatingsystem | if .NET 5 or greater) + * - or RuntimeInformation ( https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.runtimeinformation ) + */ + + var runtimeInfo = AvaloniaLocator.Current.GetService()?.GetRuntimeInfo(); + + if (runtimeInfo is { } info) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER"))) + { + if (info.IsDesktop) + { + PlatformInfo = "Platform: Desktop (browser)"; + } + else if (info.IsMobile) + { + PlatformInfo = "Platform: Mobile (browser)"; + } + else + { + PlatformInfo = "Platform: Unknown (browser) - please report"; + } + } + else + { + if (info.IsDesktop) + { + PlatformInfo = "Platform: Desktop (native)"; + } + else if (info.IsMobile) + { + PlatformInfo = "Platform: Mobile (native)"; + } + else + { + PlatformInfo = "Platform: Unknown (native) - please report"; + } + } + } + } + + public string? PlatformInfo { get; } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/PlatformSettingsViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/PlatformSettingsViewModel.cs new file mode 100644 index 000000000..070a486f0 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/PlatformSettingsViewModel.cs @@ -0,0 +1,80 @@ +using System; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Platform; +using MiniMvvm; + +namespace ControlCatalog.ViewModels; + +public class PlatformSettingsViewModel : ViewModelBase +{ + private IPlatformSettings? _platformSettings; + private PlatformColorValues? _colorValues; + private string? _preferredLanguage; + + public void Subscribe(IPlatformSettings? platformSettings) + { + _platformSettings = platformSettings; + + if (platformSettings is not null) + { + OnColorValuesChanged(platformSettings, platformSettings.GetColorValues()); + OnPreferredLanguageChanged(platformSettings, EventArgs.Empty); + + platformSettings?.ColorValuesChanged += OnColorValuesChanged; + platformSettings?.PreferredApplicationLanguageChanged += OnPreferredLanguageChanged; + } + } + + public void Unsubscribe() + { + if (_platformSettings != null) + { + _platformSettings.ColorValuesChanged -= OnColorValuesChanged; + _platformSettings.PreferredApplicationLanguageChanged -= OnPreferredLanguageChanged; + _platformSettings = null; + } + } + + private void OnColorValuesChanged(object? sender, PlatformColorValues e) + { + _colorValues = e; + RaisePropertyChanged(nameof(ThemeVariant)); + RaisePropertyChanged(nameof(ContrastPreference)); + RaisePropertyChanged(nameof(AccentColor1)); + RaisePropertyChanged(nameof(AccentColor2)); + RaisePropertyChanged(nameof(AccentColor3)); + } + + private void OnPreferredLanguageChanged(object? sender, EventArgs e) + { + _preferredLanguage = ((IPlatformSettings)sender!).PreferredApplicationLanguage; + RaisePropertyChanged(nameof(PreferredLanguage)); + } + + public string PreferredLanguage => _preferredLanguage ?? "Not available"; + + public string ThemeVariant => _colorValues?.ThemeVariant.ToString() ?? "Not available"; + + public string ContrastPreference => _colorValues?.ContrastPreference.ToString() ?? "Not available"; + + public Color AccentColor1 => _colorValues?.AccentColor1 ?? Colors.Gray; + + public Color AccentColor2 => _colorValues?.AccentColor2 ?? Colors.Gray; + + public Color AccentColor3 => _colorValues?.AccentColor3 ?? Colors.Gray; + + public string HoldWaitDuration => _platformSettings?.HoldWaitDuration.ToString() ?? "Not available"; + + public string TapSizeTouch => _platformSettings?.GetTapSize(PointerType.Touch).ToString() ?? "Not available"; + + public string TapSizeMouse => _platformSettings?.GetTapSize(PointerType.Mouse).ToString() ?? "Not available"; + + public string DoubleTapSizeTouch => _platformSettings?.GetDoubleTapSize(PointerType.Touch).ToString() ?? "Not available"; + + public string DoubleTapSizeMouse => _platformSettings?.GetDoubleTapSize(PointerType.Mouse).ToString() ?? "Not available"; + + public string DoubleTapTimeTouch => _platformSettings?.GetDoubleTapTime(PointerType.Touch).ToString() ?? "Not available"; + + public string DoubleTapTimeMouse => _platformSettings?.GetDoubleTapTime(PointerType.Mouse).ToString() ?? "Not available"; +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/RefreshContainerViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/RefreshContainerViewModel.cs new file mode 100644 index 000000000..d2361178e --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/RefreshContainerViewModel.cs @@ -0,0 +1,23 @@ +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class RefreshContainerViewModel : ViewModelBase + { + public ObservableCollection Items { get; } + + public RefreshContainerViewModel() + { + Items = new ObservableCollection(Enumerable.Range(1, 200).Select(i => $"Item {i}")); + } + + public async Task AddToTop() + { + await Task.Delay(3000); + Items.Insert(0, $"Item {200 - Items.Count}"); + } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/SplitViewPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/SplitViewPageViewModel.cs new file mode 100644 index 000000000..9664331a3 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/SplitViewPageViewModel.cs @@ -0,0 +1,54 @@ +using System; +using Avalonia.Controls; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class SplitViewPageViewModel : ViewModelBase + { + private int _displayMode = 3; //CompactOverlay + private int _placement = 0; //Left + + public int Placement + { + get => _placement; + set + { + this.RaiseAndSetIfChanged(ref _placement, value); + this.RaisePropertyChanged(nameof(PanePlacement)); + } + } + + public int DisplayMode + { + get => _displayMode; + set + { + this.RaiseAndSetIfChanged(ref _displayMode, value); + this.RaisePropertyChanged(nameof(CurrentDisplayMode)); + } + } + + public SplitViewPanePlacement PanePlacement => + _placement switch + { + 0 => SplitViewPanePlacement.Left, + 1 => SplitViewPanePlacement.Right, + 2 => SplitViewPanePlacement.Top, + 3 => SplitViewPanePlacement.Bottom, + _ => SplitViewPanePlacement.Left + }; + + public SplitViewDisplayMode CurrentDisplayMode + { + get + { + if (Enum.IsDefined(typeof(SplitViewDisplayMode), _displayMode)) + { + return (SplitViewDisplayMode)_displayMode; + } + return SplitViewDisplayMode.CompactOverlay; + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/TabControlPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/TabControlPageViewModel.cs new file mode 100644 index 000000000..b2334625e --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/TabControlPageViewModel.cs @@ -0,0 +1,26 @@ +using Avalonia.Controls; +using Avalonia.Media.Imaging; +using MiniMvvm; + +namespace ControlCatalog.ViewModels; + +public class TabControlPageViewModel : ViewModelBase +{ + private Dock _tabPlacement; + + public TabControlPageViewModelItem[]? Tabs { get; set; } + + public Dock TabPlacement + { + get { return _tabPlacement; } + set { this.RaiseAndSetIfChanged(ref _tabPlacement, value); } + } +} + +public class TabControlPageViewModelItem +{ + public string? Header { get; set; } + public string? Text { get; set; } + public Bitmap? Image { get; set; } + public bool IsEnabled { get; set; } = true; +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/TableViewPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/TableViewPageViewModel.cs new file mode 100644 index 000000000..1a969ceea --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/TableViewPageViewModel.cs @@ -0,0 +1,246 @@ +using System.Collections.ObjectModel; +using MiniMvvm; + +namespace ControlCatalog.ViewModels; + +public sealed class TableViewPageViewModel : ViewModelBase +{ + public ObservableCollection Countries { get; } = + [ + new("Afghanistan", "ASIA (EX. NEAR EAST)", 31056997, 647500, 700), + new("Albania", "EASTERN EUROPE", 3581655, 28748, 4500), + new("Algeria", "NORTHERN AFRICA", 32930091, 2381740, 6000), + new("American Samoa", "OCEANIA", 57794, 199, 8000), + new("Andorra", "WESTERN EUROPE", 71201, 468, 19000), + new("Angola", "SUB-SAHARAN AFRICA", 12127071, 1246700, 1900), + new("Anguilla", "LATIN AMER. & CARIB", 13477, 102, 8600), + new("Antigua & Barbuda", "LATIN AMER. & CARIB", 69108, 443, 11000), + new("Argentina", "LATIN AMER. & CARIB", 39921833, 2766890, 11200), + new("Armenia", "C.W. OF IND. STATES", 2976372, 29800, 3500), + new("Aruba", "LATIN AMER. & CARIB", 71891, 193, 28000), + new("Australia", "OCEANIA", 20264082, 7686850, 29000), + new("Austria", "WESTERN EUROPE", 8192880, 83870, 30000), + new("Azerbaijan", "C.W. OF IND. STATES", 7961619, 86600, 3400), + new("The Bahamas", "LATIN AMER. & CARIB", 303770, 13940, 16700), + new("Bahrain", "NEAR EAST", 698585, 665, 16900), + new("Bangladesh", "ASIA (EX. NEAR EAST)", 147365352, 144000, 1900), + new("Barbados", "LATIN AMER. & CARIB", 279912, 431, 15700), + new("Belarus", "C.W. OF IND. STATES", 10293011, 207600, 6100), + new("Belgium", "WESTERN EUROPE", 10379067, 30528, 29100), + new("Belize", "LATIN AMER. & CARIB", 287730, 22966, 4900), + new("Benin", "SUB-SAHARAN AFRICA", 7862944, 112620, 1100), + new("Bermuda", "NORTHERN AMERICA", 65773, 53, 36000), + new("Bhutan", "ASIA (EX. NEAR EAST)", 2279723, 47000, 1300), + new("Bolivia", "LATIN AMER. & CARIB", 8989046, 1098580, 2400), + new("Bosnia & Herzegovina", "EASTERN EUROPE", 4498976, 51129, 6100), + new("Botswana", "SUB-SAHARAN AFRICA", 1639833, 600370, 9000), + new("Brazil", "LATIN AMER. & CARIB", 188078227, 8511965, 7600), + new("British Virgin Is.", "LATIN AMER. & CARIB", 23098, 153, 16000), + new("Brunei", "ASIA (EX. NEAR EAST)", 379444, 5770, 18600), + new("Bulgaria", "EASTERN EUROPE", 7385367, 110910, 7600), + new("Burkina Faso", "SUB-SAHARAN AFRICA", 13902972, 274200, 1100), + new("Burma", "ASIA (EX. NEAR EAST)", 47382633, 678500, 1800), + new("Burundi", "SUB-SAHARAN AFRICA", 8090068, 27830, 600), + new("Cambodia", "ASIA (EX. NEAR EAST)", 13881427, 181040, 1900), + new("Cameroon", "SUB-SAHARAN AFRICA", 17340702, 475440, 1800), + new("Canada", "NORTHERN AMERICA", 33098932, 9984670, 29800), + new("Cape Verde", "SUB-SAHARAN AFRICA", 420979, 4033, 1400), + new("Cayman Islands", "LATIN AMER. & CARIB", 45436, 262, 35000), + new("Central African Rep.", "SUB-SAHARAN AFRICA", 4303356, 622984, 1100), + new("Chad", "SUB-SAHARAN AFRICA", 9944201, 1284000, 1200), + new("Chile", "LATIN AMER. & CARIB", 16134219, 756950, 9900), + new("China", "ASIA (EX. NEAR EAST)", 1313973713, 9596960, 5000), + new("Colombia", "LATIN AMER. & CARIB", 43593035, 1138910, 6300), + new("Comoros", "SUB-SAHARAN AFRICA", 690948, 2170, 700), + new("Congo, Dem.Rep.", "SUB - SAHARAN AFRICA", 62660551, 2345410, 700), + new("Congo, Repub.of the", "SUB - SAHARAN AFRICA", 3702314, 342000, 700), + new("Cook Islands", "OCEANIA", 21388, 240, 5000), + new("Costa Rica", "LATIN AMER. & CARIB", 4075261, 51100, 9100), + new("Cote d'Ivoire", "SUB-SAHARAN AFRICA", 17654843, 322460, 1400), + new("Croatia", "EASTERN EUROPE", 4494749, 56542, 10600), + new("Cuba", "LATIN AMER. & CARIB", 11382820, 110860, 2900), + new("Cyprus", "NEAR EAST", 784301, 9250, 19200), + new("Czech Republic", "EASTERN EUROPE", 10235455, 78866, 15700), + new("Denmark", "WESTERN EUROPE", 5450661, 43094, 31100), + new("Djibouti", "SUB-SAHARAN AFRICA", 486530, 23000, 1300), + new("Dominica", "LATIN AMER. & CARIB", 68910, 754, 5400), + new("Dominican Republic", "LATIN AMER. & CARIB", 9183984, 48730, 6000), + new("East Timor", "ASIA (EX. NEAR EAST)", 1062777, 15007, 500), + new("Ecuador", "LATIN AMER. & CARIB", 13547510, 283560, 3300), + new("Egypt", "NORTHERN AFRICA", 78887007, 1001450, 4000), + new("El Salvador", "LATIN AMER. & CARIB", 6822378, 21040, 4800), + new("Equatorial Guinea", "SUB-SAHARAN AFRICA", 540109, 28051, 2700), + new("Eritrea", "SUB-SAHARAN AFRICA", 4786994, 121320, 700), + new("Estonia", "BALTICS", 1324333, 45226, 12300), + new("Ethiopia", "SUB-SAHARAN AFRICA", 74777981, 1127127, 700), + new("Faroe Islands", "WESTERN EUROPE", 47246, 1399, 22000), + new("Fiji", "OCEANIA", 905949, 18270, 5800), + new("Finland", "WESTERN EUROPE", 5231372, 338145, 27400), + new("France", "WESTERN EUROPE", 60876136, 547030, 27600), + new("French Guiana", "LATIN AMER. & CARIB", 199509, 91000, 8300), + new("French Polynesia", "OCEANIA", 274578, 4167, 17500), + new("Gabon", "SUB-SAHARAN AFRICA", 1424906, 267667, 5500), + new("Gambia, The", "SUB - SAHARAN AFRICA", 1641564, 11300, 1700), + new("Gaza Strip", "NEAR EAST", 1428757, 360, 600), + new("Georgia", "C.W. OF IND. STATES", 4661473, 69700, 2500), + new("Germany", "WESTERN EUROPE", 82422299, 357021, 27600), + new("Ghana", "SUB-SAHARAN AFRICA", 22409572, 239460, 2200), + new("Gibraltar", "WESTERN EUROPE", 27928, 7, 17500), + new("Greece", "WESTERN EUROPE", 10688058, 131940, 20000), + new("Greenland", "NORTHERN AMERICA", 56361, 2166086, 20000), + new("Grenada", "LATIN AMER. & CARIB", 89703, 344, 5000), + new("Guadeloupe", "LATIN AMER. & CARIB", 452776, 1780, 8000), + new("Guam", "OCEANIA", 171019, 541, 21000), + new("Guatemala", "LATIN AMER. & CARIB", 12293545, 108890, 4100), + new("Guernsey", "WESTERN EUROPE", 65409, 78, 20000), + new("Guinea", "SUB-SAHARAN AFRICA", 9690222, 245857, 2100), + new("Guinea-Bissau", "SUB-SAHARAN AFRICA", 1442029, 36120, 800), + new("Guyana", "LATIN AMER. & CARIB", 767245, 214970, 4000), + new("Haiti", "LATIN AMER. & CARIB", 8308504, 27750, 1600), + new("Honduras", "LATIN AMER. & CARIB", 7326496, 112090, 2600), + new("Hong Kong", "ASIA (EX. NEAR EAST)", 6940432, 1092, 28800), + new("Hungary", "EASTERN EUROPE", 9981334, 93030, 13900), + new("Iceland", "WESTERN EUROPE", 299388, 103000, 30900), + new("India", "ASIA (EX. NEAR EAST)", 1095351995, 3287590, 2900), + new("Indonesia", "ASIA (EX. NEAR EAST)", 245452739, 1919440, 3200), + new("Iran", "ASIA (EX. NEAR EAST)", 68688433, 1648000, 7000), + new("Iraq", "NEAR EAST", 26783383, 437072, 1500), + new("Ireland", "WESTERN EUROPE", 4062235, 70280, 29600), + new("Isle of Man", "WESTERN EUROPE", 75441, 572, 21000), + new("Israel", "NEAR EAST", 6352117, 20770, 19800), + new("Italy", "WESTERN EUROPE", 58133509, 301230, 26700), + new("Jamaica", "LATIN AMER. & CARIB", 2758124, 10991, 3900), + new("Japan", "ASIA (EX. NEAR EAST)", 127463611, 377835, 28200), + new("Jersey", "WESTERN EUROPE", 91084, 116, 24800), + new("Jordan", "NEAR EAST", 5906760, 92300, 4300), + new("Kazakhstan", "C.W. OF IND. STATES", 15233244, 2717300, 6300), + new("Kenya", "SUB-SAHARAN AFRICA", 34707817, 582650, 1000), + new("Kiribati", "OCEANIA", 105432, 811, 800), + new("North Korea", "ASIA(EX.NEAR EAST)", 23113019, 120540, 1300), + new("South Korea", "ASIA(EX.NEAR EAST)", 48846823, 98480, 17800), + new("Kuwait", "NEAR EAST", 2418393, 17820, 19000), + new("Kyrgyzstan", "C.W. OF IND. STATES", 5213898, 198500, 1600), + new("Laos", "ASIA (EX. NEAR EAST)", 6368481, 236800, 1700), + new("Latvia", "BALTICS", 2274735, 64589, 10200), + new("Lebanon", "NEAR EAST", 3874050, 10400, 4800), + new("Lesotho", "SUB-SAHARAN AFRICA", 2022331, 30355, 3000), + new("Liberia", "SUB-SAHARAN AFRICA", 3042004, 111370, 1000), + new("Libya", "NORTHERN AFRICA", 5900754, 1759540, 6400), + new("Liechtenstein", "WESTERN EUROPE", 33987, 160, 25000), + new("Lithuania", "BALTICS", 3585906, 65200, 11400), + new("Luxembourg", "WESTERN EUROPE", 474413, 2586, 55100), + new("Macau", "ASIA (EX. NEAR EAST)", 453125, 28, 19400), + new("Macedonia", "EASTERN EUROPE", 2050554, 25333, 6700), + new("Madagascar", "SUB-SAHARAN AFRICA", 18595469, 587040, 800), + new("Malawi", "SUB-SAHARAN AFRICA", 13013926, 118480, 600), + new("Malaysia", "ASIA (EX. NEAR EAST)", 24385858, 329750, 9000), + new("Maldives", "ASIA (EX. NEAR EAST)", 359008, 300, 3900), + new("Mali", "SUB-SAHARAN AFRICA", 11716829, 1240000, 900), + new("Malta", "WESTERN EUROPE", 400214, 316, 17700), + new("Marshall Islands", "OCEANIA", 60422, 11854, 1600), + new("Martinique", "LATIN AMER. & CARIB", 436131, 1100, 14400), + new("Mauritania", "SUB-SAHARAN AFRICA", 3177388, 1030700, 1800), + new("Mauritius", "SUB-SAHARAN AFRICA", 1240827, 2040, 11400), + new("Mayotte", "SUB-SAHARAN AFRICA", 201234, 374, 2600), + new("Mexico", "LATIN AMER. & CARIB", 107449525, 1972550, 9000), + new("Micronesia, Fed.St.", "OCEANIA", 108004, 702, 2000), + new("Moldova", "C.W. OF IND. STATES", 4466706, 33843, 1800), + new("Monaco", "WESTERN EUROPE", 32543, 2, 27000), + new("Mongolia", "ASIA (EX. NEAR EAST)", 2832224, 1564116, 1800), + new("Montserrat", "LATIN AMER. & CARIB", 9439, 102, 3400), + new("Morocco", "NORTHERN AFRICA", 33241259, 446550, 4000), + new("Mozambique", "SUB-SAHARAN AFRICA", 19686505, 801590, 1200), + new("Namibia", "SUB-SAHARAN AFRICA", 2044147, 825418, 7200), + new("Nauru", "OCEANIA", 13287, 21, 5000), + new("Nepal", "ASIA (EX. NEAR EAST)", 28287147, 147181, 1400), + new("Netherlands", "WESTERN EUROPE", 16491461, 41526, 28600), + new("Netherlands Antilles", "LATIN AMER. & CARIB", 221736, 960, 11400), + new("New Caledonia", "OCEANIA", 219246, 19060, 15000), + new("New Zealand", "OCEANIA", 4076140, 268680, 21600), + new("Nicaragua", "LATIN AMER. & CARIB", 5570129, 129494, 2300), + new("Niger", "SUB-SAHARAN AFRICA", 12525094, 1267000, 800), + new("Nigeria", "SUB-SAHARAN AFRICA", 131859731, 923768, 900), + new("N. Mariana Islands", "OCEANIA", 82459, 477, 12500), + new("Norway", "WESTERN EUROPE", 4610820, 323802, 37800), + new("Oman", "NEAR EAST", 3102229, 212460, 13100), + new("Pakistan", "ASIA (EX. NEAR EAST)", 165803560, 803940, 2100), + new("Palau", "OCEANIA", 20579, 458, 9000), + new("Panama", "LATIN AMER. & CARIB", 3191319, 78200, 6300), + new("Papua New Guinea", "OCEANIA", 5670544, 462840, 2200), + new("Paraguay", "LATIN AMER. & CARIB", 6506464, 406750, 4700), + new("Peru", "LATIN AMER. & CARIB", 28302603, 1285220, 5100), + new("Philippines", "ASIA (EX. NEAR EAST)", 89468677, 300000, 4600), + new("Poland", "EASTERN EUROPE", 38536869, 312685, 11100), + new("Portugal", "WESTERN EUROPE", 10605870, 92391, 18000), + new("Puerto Rico", "LATIN AMER. & CARIB", 3927188, 13790, 16800), + new("Qatar", "NEAR EAST", 885359, 11437, 21500), + new("Reunion", "SUB-SAHARAN AFRICA", 787584, 2517, 5800), + new("Romania", "EASTERN EUROPE", 22303552, 237500, 7000), + new("Russia", "C.W. OF IND. STATES", 142893540, 17075200, 8900), + new("Rwanda", "SUB-SAHARAN AFRICA", 8648248, 26338, 1300), + new("Saint Helena", "SUB-SAHARAN AFRICA", 7502, 413, 2500), + new("Saint Kitts & Nevis", "LATIN AMER. & CARIB", 39129, 261, 8800), + new("Saint Lucia", "LATIN AMER. & CARIB", 168458, 616, 5400), + new("St Pierre & Miquelon", "NORTHERN AMERICA", 7026, 242, 6900), + new("Saint Vincent and the Grenadines", "LATIN AMER. & CARIB", 117848, 389, 2900), + new("Samoa", "OCEANIA", 176908, 2944, 5600), + new("San Marino", "WESTERN EUROPE", 29251, 61, 34600), + new("Sao Tome & Principe", "SUB-SAHARAN AFRICA", 193413, 1001, 1200), + new("Saudi Arabia", "NEAR EAST", 27019731, 1960582, 11800), + new("Senegal", "SUB-SAHARAN AFRICA", 11987121, 196190, 1600), + new("Serbia", "EASTERN EUROPE", 9396411, 88361, 2200), + new("Seychelles", "SUB-SAHARAN AFRICA", 81541, 455, 7800), + new("Sierra Leone", "SUB-SAHARAN AFRICA", 6005250, 71740, 500), + new("Singapore", "ASIA (EX. NEAR EAST)", 4492150, 693, 23700), + new("Slovakia", "EASTERN EUROPE", 5439448, 48845, 13300), + new("Slovenia", "EASTERN EUROPE", 2010347, 20273, 19000), + new("Solomon Islands", "OCEANIA", 552438, 28450, 1700), + new("Somalia", "SUB-SAHARAN AFRICA", 8863338, 637657, 500), + new("South Africa", "SUB-SAHARAN AFRICA", 44187637, 1219912, 10700), + new("Spain", "WESTERN EUROPE", 40397842, 504782, 22000), + new("Sri Lanka", "ASIA (EX. NEAR EAST)", 20222240, 65610, 3700), + new("Sudan", "SUB-SAHARAN AFRICA", 41236378, 2505810, 1900), + new("Suriname", "LATIN AMER. & CARIB", 439117, 163270, 4000), + new("Swaziland", "SUB-SAHARAN AFRICA", 1136334, 17363, 4900), + new("Sweden", "WESTERN EUROPE", 9016596, 449964, 26800), + new("Switzerland", "WESTERN EUROPE", 7523934, 41290, 32700), + new("Syria", "NEAR EAST", 18881361, 185180, 3300), + new("Taiwan", "ASIA (EX. NEAR EAST)", 23036087, 35980, 23400), + new("Tajikistan", "C.W. OF IND. STATES", 7320815, 143100, 1000), + new("Tanzania", "SUB-SAHARAN AFRICA", 37445392, 945087, 600), + new("Thailand", "ASIA (EX. NEAR EAST)", 64631595, 514000, 7400), + new("Togo", "SUB-SAHARAN AFRICA", 5548702, 56785, 1500), + new("Tonga", "OCEANIA", 114689, 748, 2200), + new("Trinidad & Tobago", "LATIN AMER. & CARIB", 1065842, 5128, 9500), + new("Tunisia", "NORTHERN AFRICA", 10175014, 163610, 6900), + new("Turkey", "NEAR EAST", 70413958, 780580, 6700), + new("Turkmenistan", "C.W. OF IND. STATES", 5042920, 488100, 5800), + new("Turks & Caicos Is", "LATIN AMER. & CARIB", 21152, 430, 9600), + new("Tuvalu", "OCEANIA", 11810, 26, 1100), + new("Uganda", "SUB-SAHARAN AFRICA", 28195754, 236040, 1400), + new("Ukraine", "C.W. OF IND. STATES", 46710816, 603700, 5400), + new("United Arab Emirates", "NEAR EAST", 2602713, 82880, 23200), + new("United Kingdom", "WESTERN EUROPE", 60609153, 244820, 27700), + new("United States", "NORTHERN AMERICA", 298444215, 9631420, 37800), + new("Uruguay", "LATIN AMER. & CARIB", 3431932, 176220, 12800), + new("Uzbekistan", "C.W. OF IND. STATES", 27307134, 447400, 1700), + new("Vanuatu", "OCEANIA", 208869, 12200, 2900), + new("Venezuela", "LATIN AMER. & CARIB", 25730435, 912050, 4800), + new("Vietnam", "ASIA (EX. NEAR EAST)", 84402966, 329560, 2500), + new("Virgin Islands", "LATIN AMER. & CARIB", 108605, 1910, 17200), + new("Wallis and Futuna", "OCEANIA", 16025, 274, 3700), + new("West Bank", "NEAR EAST", 2460492, 5860, 800), + new("Yemen", "NEAR EAST", 21456188, 527970, 800), + new("Zambia", "SUB-SAHARAN AFRICA", 11502010, 752614, 800), + new("Zimbabwe", "SUB-SAHARAN AFRICA", 12236805, 390580, 1900) + ]; + + public sealed class Country(string name, string region, int population, int area, int gdp) + { + public string Name { get; set; } = name; + public string Region { get; set; } = region; + public int Population { get; set; } = population; + public int Area { get; set; } = area; // Square miles + public int GDP { get; set; } = gdp; + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs new file mode 100644 index 000000000..5bad6c4b0 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs @@ -0,0 +1,317 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Styling; +using Avalonia.VisualTree; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class TransitioningContentControlPageViewModel : ViewModelBase + { + public TransitioningContentControlPageViewModel() + { + var images = new string[] + { + "delicate-arch-896885_640.jpg", + "hirsch-899118_640.jpg", + "maple-leaf-888807_640.jpg" + }; + + foreach (var image in images) + { + var path = $"avares://ControlCatalog/Assets/{image}"; + Images.Add(new Bitmap(AssetLoader.Open(new Uri(path)))); + } + + SetupTransitions(); + + _SelectedTransition = PageTransitions[1]; + _SelectedImage = Images[0]; + } + + public List PageTransitions { get; } = new List(); + + public List Images { get; } = new List(); + + + private Bitmap _SelectedImage; + private bool _Reversed; + + /// + /// Gets or Sets the selected image + /// + public Bitmap SelectedImage + { + get { return _SelectedImage; } + set { this.RaiseAndSetIfChanged(ref _SelectedImage, value); } + } + + + private PageTransition _SelectedTransition; + + /// + /// Gets or sets the transition to play + /// + public PageTransition SelectedTransition + { + get { return _SelectedTransition; } + set { this.RaiseAndSetIfChanged(ref _SelectedTransition, value); } + } + + + + private bool _ClipToBounds; + + /// + /// Gets or sets if the content should be clipped to bounds + /// + public bool ClipToBounds + { + get { return _ClipToBounds; } + set { this.RaiseAndSetIfChanged(ref _ClipToBounds, value); } + } + + + private int _Duration = 500; + + /// + /// Gets or Sets the duration + /// + public int Duration + { + get { return _Duration; } + set + { + this.RaiseAndSetIfChanged(ref _Duration , value); + SetupTransitions(); + } + } + + /// + /// Gets or sets a value indicating whether the animation is reversed. + /// + public bool Reversed + { + get => _Reversed; + set => this.RaiseAndSetIfChanged(ref _Reversed, value); + } + + private void SetupTransitions() + { + if (PageTransitions.Count == 0) + { + PageTransitions.AddRange(new[] + { + new PageTransition("None"), + new PageTransition("CrossFade"), + new PageTransition("Slide horizontally"), + new PageTransition("Slide vertically"), + new PageTransition("Composite"), + new PageTransition("Custom") + }); + } + + PageTransitions[1].Transition = new CrossFade(TimeSpan.FromMilliseconds(Duration)); + PageTransitions[2].Transition = new PageSlide(TimeSpan.FromMilliseconds(Duration), PageSlide.SlideAxis.Horizontal); + PageTransitions[3].Transition = new PageSlide(TimeSpan.FromMilliseconds(Duration), PageSlide.SlideAxis.Vertical); + + var compositeTransition = new CompositePageTransition(); + compositeTransition.PageTransitions.Add(PageTransitions[1].Transition!); + compositeTransition.PageTransitions.Add(PageTransitions[2].Transition!); + compositeTransition.PageTransitions.Add(PageTransitions[3].Transition!); + PageTransitions[4].Transition = compositeTransition; + + PageTransitions[5].Transition = new CustomTransition(TimeSpan.FromMilliseconds(Duration)); + } + + public void NextImage() + { + Reversed = false; + var index = Images.IndexOf(SelectedImage) + 1; + + if (index >= Images.Count) + { + index = 0; + } + + SelectedImage = Images[index]; + } + + public void PrevImage() + { + Reversed = true; + var index = Images.IndexOf(SelectedImage) - 1; + + if (index < 0) + { + index = Images.Count-1; + } + + SelectedImage = Images[index]; + } + } + + public class PageTransition : ViewModelBase + { + public PageTransition(string displayTitle) + { + DisplayTitle = displayTitle; + } + + public string DisplayTitle { get; } + + + private IPageTransition? _Transition; + + /// + /// Gets or sets the transition + /// + public IPageTransition? Transition + { + get { return _Transition; } + set { this.RaiseAndSetIfChanged(ref _Transition, value); } + } + + public override string ToString() + { + return DisplayTitle; + } + + } + + public class CustomTransition : IPageTransition + { + /// + /// Initializes a new instance of the class. + /// + public CustomTransition() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The duration of the animation. + public CustomTransition(TimeSpan duration) + { + Duration = duration; + } + + /// + /// Gets the duration of the animation. + /// + public TimeSpan Duration { get; set; } + + public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + var tasks = new List(); + var parent = GetVisualParent(from, to); + var scaleProperty = ScaleTransform.ScaleYProperty; + + if (from != null) + { + var animation = new Animation + { + Children = + { + new KeyFrame + { + Setters = { new Setter { Property = scaleProperty, Value = 1d } }, + Cue = new Cue(0d) + }, + new KeyFrame + { + Setters = + { + new Setter + { + Property = scaleProperty, + Value = 0d + } + }, + Cue = new Cue(1d) + } + }, + Duration = Duration + }; + tasks.Add(animation.RunAsync(from, cancellationToken)); + } + + if (to != null) + { + to.IsVisible = true; + var animation = new Animation + { + Children = + { + new KeyFrame + { + Setters = + { + new Setter + { + Property = scaleProperty, + Value = 0d + } + }, + Cue = new Cue(0d) + }, + new KeyFrame + { + Setters = { new Setter { Property = scaleProperty, Value = 1d } }, + Cue = new Cue(1d) + } + }, + Duration = Duration + }; + tasks.Add(animation.RunAsync(to, cancellationToken)); + } + + await Task.WhenAll(tasks); + + if (from != null && !cancellationToken.IsCancellationRequested) + { + from.IsVisible = false; + } + } + + /// + /// Gets the common visual parent of the two control. + /// + /// The from control. + /// The to control. + /// The common parent. + /// + /// The two controls do not share a common parent. + /// + /// + /// Any one of the parameters may be null, but not both. + /// + private static Visual GetVisualParent(Visual? from, Visual? to) + { + var p1 = (from ?? to)!.GetVisualParent(); + var p2 = (to ?? from)!.GetVisualParent(); + + if (p1 != null && p2 != null && p1 != p2) + { + throw new ArgumentException("Controls for PageSlide must have same parent."); + } + + return p1 ?? throw new InvalidOperationException("Cannot determine visual parent."); + } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/TreeViewPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/TreeViewPageViewModel.cs new file mode 100644 index 000000000..7b24f61e1 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/TreeViewPageViewModel.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using Avalonia.Controls; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class TreeViewPageViewModel : ViewModelBase + { + private readonly Node _root; + private SelectionMode _selectionMode; + + public TreeViewPageViewModel() + { + _root = new Node(); + + Items = _root.Children; + SelectedItems = new ObservableCollection(); + + AddItemCommand = MiniCommand.Create(AddItem); + RemoveItemCommand = MiniCommand.Create(RemoveItem); + SelectRandomItemCommand = MiniCommand.Create(SelectRandomItem); + } + + public ObservableCollection Items { get; } + public ObservableCollection SelectedItems { get; } + public MiniCommand AddItemCommand { get; } + public MiniCommand RemoveItemCommand { get; } + public MiniCommand SelectRandomItemCommand { get; } + + public SelectionMode SelectionMode + { + get => _selectionMode; + set + { + SelectedItems.Clear(); + this.RaiseAndSetIfChanged(ref _selectionMode, value); + } + } + + private void AddItem() + { + var parentItem = SelectedItems.Count > 0 ? (Node)SelectedItems[0] : _root; + parentItem.AddItem(); + } + + private void RemoveItem() + { + while (SelectedItems.Count > 0) + { + var lastItem = SelectedItems[0]; + RecursiveRemove(Items, lastItem); + } + + bool RecursiveRemove(ObservableCollection items, Node selectedItem) + { + if (items.Remove(selectedItem)) + { + return true; + } + + foreach (Node item in items) + { + if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem)) + { + return true; + } + } + + return false; + } + } + + private void SelectRandomItem() + { + var random = new Random(); + var depth = random.Next(4); + var indexes = Enumerable.Range(0, depth).Select(x => random.Next(10)); + var node = _root; + + foreach (var i in indexes) + { + node = node.Children[i]; + } + + SelectedItems.Clear(); + SelectedItems.Add(node); + } + + public class Node + { + private ObservableCollection? _children; + private int _childIndex = 10; + + public Node() + { + Header = "Item"; + } + + public Node(Node parent, int index) + { + Parent = parent; + Header = parent.Header + ' ' + index; + } + + public Node? Parent { get; } + public string Header { get; } + public bool AreChildrenInitialized => _children != null; + public ObservableCollection Children => _children ??= CreateChildren(); + public void AddItem() => Children.Add(new Node(this, _childIndex++)); + public void RemoveItem(Node child) => Children.Remove(child); + public override string ToString() => Header; + + private ObservableCollection CreateChildren() + { + return new ObservableCollection( + Enumerable.Range(0, 10).Select(i => new Node(this, i))); + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/WrapPanelPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/WrapPanelPageViewModel.cs new file mode 100644 index 000000000..a2b36fcef --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/WrapPanelPageViewModel.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using MiniMvvm; + +namespace ControlCatalog.ViewModels; + +public class WrapPanelPageViewModel : ViewModelBase +{ + public WrapPanelItemsAlignment[] ItemsAlignments { get; } = Enum.GetValues(); + + public Orientation[] Orientations { get; } = Enum.GetValues(); + + public IReadOnlyList Items { get; } = CreateItems(); + + public WrapPanelItemsAlignment ItemsAlignment + { + get; + set => RaiseAndSetIfChanged(ref field, value); + } = WrapPanelItemsAlignment.Start; + + public Orientation Orientation + { + get; + set => RaiseAndSetIfChanged(ref field, value); + } = Orientation.Horizontal; + + public double ItemSpacing + { + get; + set => RaiseAndSetIfChanged(ref field, value); + } = 5; + + public double LineSpacing + { + get; + set => RaiseAndSetIfChanged(ref field, value); + } = 5; + + public bool IsItemWidthEnabled + { + get; + set + { + if (RaiseAndSetIfChanged(ref field, value)) + RaisePropertyChanged(nameof(EffectiveItemWidth)); + } + } + + public double ItemWidthValue + { + get; + set + { + if (RaiseAndSetIfChanged(ref field, value)) + RaisePropertyChanged(nameof(EffectiveItemWidth)); + } + } = 140; + + public bool IsItemHeightEnabled + { + get; + set + { + if (RaiseAndSetIfChanged(ref field, value)) + RaisePropertyChanged(nameof(EffectiveItemHeight)); + } + } + + public double ItemHeightValue + { + get; + set + { + if (RaiseAndSetIfChanged(ref field, value)) + RaisePropertyChanged(nameof(EffectiveItemHeight)); + } + } = 90; + + public double EffectiveItemWidth + => IsItemWidthEnabled ? ItemWidthValue : double.NaN; + + public double EffectiveItemHeight + => IsItemHeightEnabled ? ItemHeightValue : double.NaN; + + private static WrapPanelItemViewModel[] CreateItems() + { + var random = new Random(42); + var items = new WrapPanelItemViewModel[50]; + + for (var i = 0; i < items.Length; ++i) + items[i] = new WrapPanelItemViewModel(i + 1, new Thickness(random.Next(15, 56), random.Next(8, 31))); + + return items; + } +} + +public class WrapPanelItemViewModel(int number, Thickness padding) +{ + public int Number { get; } = number; + + public Thickness Padding { get; } = padding; +} diff --git a/samples/AvaloniaControlCatalog/Views/CustomNotificationView.xaml b/samples/AvaloniaControlCatalog/Views/CustomNotificationView.xaml new file mode 100644 index 000000000..f06ea029a --- /dev/null +++ b/samples/AvaloniaControlCatalog/Views/CustomNotificationView.xaml @@ -0,0 +1,21 @@ + + + + + + + + + +