diff --git a/.gitmodules b/.gitmodules
index 10c780c09f..2d11fdfa9e 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -2,5 +2,5 @@
path = nukebuild/Numerge
url = https://github.com/kekekeks/Numerge.git
[submodule "src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github"]
- path = src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github
- url = https://github.com/kekekeks/XamlIl.git
+ path = src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
+ url = https://github.com/kekekeks/XamlX.git
diff --git a/.ncrunch/NativeEmbedSample.v3.ncrunchproject b/.ncrunch/NativeEmbedSample.v3.ncrunchproject
new file mode 100644
index 0000000000..319cd523ce
--- /dev/null
+++ b/.ncrunch/NativeEmbedSample.v3.ncrunchproject
@@ -0,0 +1,5 @@
+
+
+ True
+
+
\ No newline at end of file
diff --git a/Avalonia.sln b/Avalonia.sln
index 5bff2fa0a0..ffd7b4d4f4 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -123,10 +123,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.NetCore", "s
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}"
ProjectSection(SolutionItems) = preProject
+ build\AndroidWorkarounds.props = build\AndroidWorkarounds.props
build\Base.props = build\Base.props
build\Binding.props = build\Binding.props
- build\BuildTargets.targets = build\BuildTargets.targets
+ build\CoreLibraries.props = build\CoreLibraries.props
+ build\EmbedXaml.props = build\EmbedXaml.props
build\HarfBuzzSharp.props = build\HarfBuzzSharp.props
+ build\iOSWorkarounds.props = build\iOSWorkarounds.props
build\JetBrains.Annotations.props = build\JetBrains.Annotations.props
build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props
build\Magick.NET-Q16-AnyCPU.props = build\Magick.NET-Q16-AnyCPU.props
@@ -136,17 +139,25 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\NetCore.props = build\NetCore.props
build\NetFX.props = build\NetFX.props
build\ReactiveUI.props = build\ReactiveUI.props
+ build\ReferenceCoreLibraries.props = build\ReferenceCoreLibraries.props
build\Rx.props = build\Rx.props
build\SampleApp.props = build\SampleApp.props
+ build\SharedVersion.props = build\SharedVersion.props
build\SharpDX.props = build\SharpDX.props
build\SkiaSharp.props = build\SkiaSharp.props
+ build\SourceLink.props = build\SourceLink.props
+ build\System.Drawing.Common.props = build\System.Drawing.Common.props
build\System.Memory.props = build\System.Memory.props
+ build\UnitTests.NetFX.props = build\UnitTests.NetFX.props
build\XUnit.props = build\XUnit.props
+ build\ApiDiff.props = build\ApiDiff.props
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6FAF79-58B4-482F-9122-0668C346364C}"
ProjectSection(SolutionItems) = preProject
build\UnitTests.NetCore.targets = build\UnitTests.NetCore.targets
+ build\BuildTargets.targets = build\BuildTargets.targets
+ build\LegacyProject.targets = build\LegacyProject.targets
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Linux", "Linux", "{86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}"
@@ -203,14 +214,22 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NativeEmbedSample", "samples\interop\NativeEmbedSample\NativeEmbedSample.csproj", "{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless", "src\Avalonia.Headless\Avalonia.Headless.csproj", "{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.Vnc", "src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj", "{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Markup.Xaml.Loader", "src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj", "{909A8CBD-7D0E-42FD-B841-022AD8925820}"
+EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
src\Shared\RenderHelpers\RenderHelpers.projitems*{3e908f67-5543-4879-a1dc-08eace79b3cd}*SharedItemsImports = 5
- src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 4
- src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 4
+ src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 5
+ src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 5
src\Shared\RenderHelpers\RenderHelpers.projitems*{7d2d3083-71dd-4cc9-8907-39a0d86fb322}*SharedItemsImports = 5
src\Shared\PlatformSupport\PlatformSupport.projitems*{88060192-33d5-4932-b0f9-8bd2763e857d}*SharedItemsImports = 5
src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13
@@ -1824,6 +1843,54 @@ Global
{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhone.Build.0 = Release|Any CPU
{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhone.Build.0 = Debug|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhone.Build.0 = Release|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhone.Build.0 = Debug|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhone.Build.0 = Release|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
@@ -1896,6 +1963,30 @@ Global
{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhone.Build.0 = Release|Any CPU
{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|iPhone.Build.0 = Debug|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|iPhone.Build.0 = Release|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
@@ -1920,6 +2011,30 @@ Global
{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhone.Build.0 = Release|Any CPU
{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.AppStore|iPhone.Build.0 = Debug|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|Any CPU.Build.0 = Release|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhone.Build.0 = Release|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1977,6 +2092,8 @@ Global
{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
+ {3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
+ {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}
diff --git a/Avalonia.v3.ncrunchsolution b/Avalonia.v3.ncrunchsolution
index a2208a9a91..bef7e45524 100644
--- a/Avalonia.v3.ncrunchsolution
+++ b/Avalonia.v3.ncrunchsolution
@@ -3,6 +3,7 @@
tests\TestFiles\**.*
src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Avalonia.Build.Tasks.dll
+ src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Mono.Cecil.dll
True
.ncrunch
diff --git a/Directory.Build.props b/Directory.Build.props
index 1f26df9bbc..b41f8c488e 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,5 +1,6 @@
$(MSBuildThisFileDirectory)build-intermediate/nuget
+ $(MSBuildThisFileDirectory)\src\tools\Avalonia.Designer.HostApp\bin\$(Configuration)\netcoreapp2.0\Avalonia.Designer.HostApp.dll
diff --git a/NuGet.Config b/NuGet.Config
index 2a1e0af74d..3abd236d42 100644
--- a/NuGet.Config
+++ b/NuGet.Config
@@ -1,7 +1,9 @@
+
+
diff --git a/build/ApiDiff.props b/build/ApiDiff.props
new file mode 100644
index 0000000000..da82fbcc51
--- /dev/null
+++ b/build/ApiDiff.props
@@ -0,0 +1,12 @@
+
+
+ 0.10.0-preview3
+ $(PackageId)
+ Avalonia
+
+
+
+
+
+
+
diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props
index 873048ef21..88c4d36282 100644
--- a/build/HarfBuzzSharp.props
+++ b/build/HarfBuzzSharp.props
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/build/ReactiveUI.props b/build/ReactiveUI.props
index f827cb9a32..d8e86e917e 100644
--- a/build/ReactiveUI.props
+++ b/build/ReactiveUI.props
@@ -1,5 +1,5 @@
-
+
diff --git a/build/SharedVersion.props b/build/SharedVersion.props
index bd183faab3..d3cebef418 100644
--- a/build/SharedVersion.props
+++ b/build/SharedVersion.props
@@ -2,7 +2,7 @@
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
Avalonia
- 0.9.999
+ 0.10.999
Copyright 2020 © The AvaloniaUI Project
https://avaloniaui.net
https://github.com/AvaloniaUI/Avalonia/
diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props
index 4def44cbd0..bbef48050e 100644
--- a/build/SkiaSharp.props
+++ b/build/SkiaSharp.props
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/dirs.proj b/dirs.proj
index 4b3b1183f0..26c8f54b23 100644
--- a/dirs.proj
+++ b/dirs.proj
@@ -6,7 +6,7 @@
-
+
diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h
index f9bfaf0b47..9ff6130e5f 100644
--- a/native/Avalonia.Native/inc/avalonia-native.h
+++ b/native/Avalonia.Native/inc/avalonia-native.h
@@ -26,7 +26,8 @@ struct IAvnStringArray;
struct IAvnDndResultCallback;
struct IAvnGCHandleDeallocatorCallback;
struct IAvnMenuEvents;
-
+struct IAvnNativeControlHost;
+struct IAvnNativeControlHostTopLevelAttachment;
enum SystemDecorations {
SystemDecorationsNone = 0,
SystemDecorationsBorderOnly = 1,
@@ -204,6 +205,15 @@ enum AvnMenuItemToggleType
Radio
};
+enum AvnExtendClientAreaChromeHints
+{
+ AvnNoChrome = 0,
+ AvnSystemChrome = 0x01,
+ AvnPreferSystemChrome = 0x02,
+ AvnOSXThickTitleBar = 0x08,
+ AvnDefaultChrome = AvnSystemChrome,
+};
+
AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown
{
public:
@@ -256,6 +266,7 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown
virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0;
virtual HRESULT ObtainNSViewHandle(void** retOut) = 0;
virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0;
+ virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost** retOut) = 0;
virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point,
IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) = 0;
virtual HRESULT SetBlurEnabled (bool enable) = 0;
@@ -276,6 +287,11 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase
virtual HRESULT SetTitleBarColor (AvnColor color) = 0;
virtual HRESULT SetWindowState(AvnWindowState state) = 0;
virtual HRESULT GetWindowState(AvnWindowState*ret) = 0;
+ virtual HRESULT TakeFocusFromChildren() = 0;
+ virtual HRESULT SetExtendClientArea (bool enable) = 0;
+ virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) = 0;
+ virtual HRESULT GetExtendTitleBarHeight (double*ret) = 0;
+ virtual HRESULT SetExtendTitleBarHeight (double value) = 0;
};
AVNCOM(IAvnWindowBaseEvents, 05) : IUnknown
@@ -295,6 +311,7 @@ AVNCOM(IAvnWindowBaseEvents, 05) : IUnknown
virtual bool RawTextInputEvent (unsigned int timeStamp, const char* text) = 0;
virtual void ScalingChanged(double scaling) = 0;
virtual void RunRenderPriorityJobs() = 0;
+ virtual void LostFocus() = 0;
virtual AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position,
AvnInputModifiers modifiers, AvnDragDropEffects effects,
IAvnClipboard* clipboard, void* dataObjectHandle) = 0;
@@ -478,4 +495,22 @@ AVNCOM(IAvnGCHandleDeallocatorCallback, 22) : IUnknown
virtual void FreeGCHandle(void* handle) = 0;
};
+AVNCOM(IAvnNativeControlHost, 20) : IUnknown
+{
+ virtual HRESULT CreateDefaultChild(void* parent, void** retOut) = 0;
+ virtual IAvnNativeControlHostTopLevelAttachment* CreateAttachment() = 0;
+ virtual void DestroyDefaultChild(void* child) = 0;
+};
+
+AVNCOM(IAvnNativeControlHostTopLevelAttachment, 21) : IUnknown
+{
+ virtual void* GetParentHandle() = 0;
+ virtual HRESULT InitializeWithChildHandle(void* child) = 0;
+ virtual HRESULT AttachTo(IAvnNativeControlHost* host) = 0;
+ virtual void ShowInBounds(float x, float y, float width, float height) = 0;
+ virtual void HideWithSize(float width, float height) = 0;
+ virtual void ReleaseChild() = 0;
+};
+
+
extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative();
diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
index ea28780c81..d5cad4d1ca 100644
--- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
+++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
@@ -10,6 +10,8 @@
1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; };
1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */; };
1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */; };
+ 1A1852DC23E05814008F0DED /* deadlock.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A1852DB23E05814008F0DED /* deadlock.mm */; };
+ 1AFD334123E03C4F0042899B /* controlhost.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1AFD334023E03C4F0042899B /* controlhost.mm */; };
1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */; };
1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */; };
1A465D10246AB61600C5858B /* dnd.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A465D0F246AB61600C5858B /* dnd.mm */; };
@@ -32,6 +34,8 @@
1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = ""; };
1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = rendertarget.mm; sourceTree = ""; };
1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOSurface.framework; path = System/Library/Frameworks/IOSurface.framework; sourceTree = SDKROOT; };
+ 1A1852DB23E05814008F0DED /* deadlock.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = deadlock.mm; sourceTree = ""; };
+ 1AFD334023E03C4F0042899B /* controlhost.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = controlhost.mm; sourceTree = ""; };
1A3E5EAD23E9FB1300EDE661 /* cgl.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cgl.mm; sourceTree = ""; };
1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
1A465D0F246AB61600C5858B /* dnd.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = dnd.mm; sourceTree = ""; };
@@ -86,11 +90,13 @@
AB7A61E62147C814003C5833 = {
isa = PBXGroup;
children = (
+ 1A1852DB23E05814008F0DED /* deadlock.mm */,
1A002B9D232135EE00021753 /* app.mm */,
37DDA9B121933371002E132B /* AvnString.h */,
37DDA9AF219330F8002E132B /* AvnString.mm */,
37A4E71A2178846A00EACBCD /* headers */,
1A3E5EAD23E9FB1300EDE661 /* cgl.mm */,
+ 1AFD334023E03C4F0042899B /* controlhost.mm */,
5BF943652167AD1D009CAE35 /* cursor.h */,
5B21A981216530F500CEE36E /* cursor.mm */,
5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */,
@@ -191,6 +197,7 @@
files = (
1A002B9E232135EE00021753 /* app.mm in Sources */,
5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */,
+ 1A1852DC23E05814008F0DED /* deadlock.mm in Sources */,
5B21A982216530F500CEE36E /* cursor.mm in Sources */,
37DDA9B0219330F8002E132B /* AvnString.mm in Sources */,
AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */,
@@ -199,6 +206,7 @@
37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */,
520624B322973F4100C4DCEF /* menu.mm in Sources */,
37A517B32159597E00FBA241 /* Screens.mm in Sources */,
+ 1AFD334123E03C4F0042899B /* controlhost.mm in Sources */,
1A465D10246AB61600C5858B /* dnd.mm in Sources */,
AB00E4F72147CA920032A60A /* main.mm in Sources */,
37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */,
diff --git a/native/Avalonia.Native/src/OSX/Screens.mm b/native/Avalonia.Native/src/OSX/Screens.mm
index 278daf9a18..455cfa2e41 100644
--- a/native/Avalonia.Native/src/OSX/Screens.mm
+++ b/native/Avalonia.Native/src/OSX/Screens.mm
@@ -4,6 +4,14 @@ class Screens : public ComSingleObject
{
public:
FORWARD_IUNKNOWN()
+
+ private:
+ CGFloat PrimaryDisplayHeight()
+ {
+ return NSMaxY([[[NSScreen screens] firstObject] frame]);
+ }
+
+public:
virtual HRESULT GetScreenCount (int* ret) override
{
@autoreleasepool
@@ -25,15 +33,15 @@ class Screens : public ComSingleObject
auto screen = [[NSScreen screens] objectAtIndex:index];
- ret->Bounds.X = [screen frame].origin.x;
- ret->Bounds.Y = [screen frame].origin.y;
ret->Bounds.Height = [screen frame].size.height;
ret->Bounds.Width = [screen frame].size.width;
+ ret->Bounds.X = [screen frame].origin.x;
+ ret->Bounds.Y = PrimaryDisplayHeight() - [screen frame].origin.y - ret->Bounds.Height;
- ret->WorkingArea.X = [screen visibleFrame].origin.x;
- ret->WorkingArea.Y = [screen visibleFrame].origin.y;
ret->WorkingArea.Height = [screen visibleFrame].size.height;
ret->WorkingArea.Width = [screen visibleFrame].size.width;
+ ret->WorkingArea.X = [screen visibleFrame].origin.x;
+ ret->WorkingArea.Y = ret->Bounds.Height - [screen visibleFrame].origin.y - ret->WorkingArea.Height;
ret->PixelDensity = [screen backingScaleFactor];
diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm
index 1e74a70e66..814b91cb62 100644
--- a/native/Avalonia.Native/src/OSX/app.mm
+++ b/native/Avalonia.Native/src/OSX/app.mm
@@ -29,9 +29,43 @@ NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivati
@end
+@interface AvnApplication : NSApplication
+
+
+@end
+
+@implementation AvnApplication
+{
+ BOOL _isHandlingSendEvent;
+}
+
+- (void)sendEvent:(NSEvent *)event
+{
+ bool oldHandling = _isHandlingSendEvent;
+ _isHandlingSendEvent = true;
+ @try {
+ [super sendEvent: event];
+ } @finally {
+ _isHandlingSendEvent = oldHandling;
+ }
+}
+
+// This is needed for certain embedded controls
+- (BOOL) isHandlingSendEvent
+{
+ return _isHandlingSendEvent;
+}
+
+- (void)setHandlingSendEvent:(BOOL)handlingSendEvent
+{
+ _isHandlingSendEvent = handlingSendEvent;
+}
+
+@end
+
extern void InitializeAvnApp()
{
- NSApplication* app = [NSApplication sharedApplication];
+ NSApplication* app = [AvnApplication sharedApplication];
id delegate = [AvnAppDelegate new];
[app setDelegate:delegate];
}
diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h
index df6a7be91c..871bca086d 100644
--- a/native/Avalonia.Native/src/OSX/common.h
+++ b/native/Avalonia.Native/src/OSX/common.h
@@ -24,6 +24,7 @@ extern IAvnGlDisplay* GetGlDisplay();
extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events);
extern IAvnMenuItem* CreateAppMenuItem();
extern IAvnMenuItem* CreateAppMenuItemSeperator();
+extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent);
extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu);
extern IAvnMenu* GetAppMenu ();
extern NSMenuItem* GetAppMenuItem ();
@@ -54,4 +55,12 @@ template inline T* objc_cast(id from) {
- (void) action;
@end
+class AvnInsidePotentialDeadlock
+{
+public:
+ static bool IsInside();
+ AvnInsidePotentialDeadlock();
+ ~AvnInsidePotentialDeadlock();
+};
+
#endif
diff --git a/native/Avalonia.Native/src/OSX/controlhost.mm b/native/Avalonia.Native/src/OSX/controlhost.mm
new file mode 100644
index 0000000000..5ee2344ac7
--- /dev/null
+++ b/native/Avalonia.Native/src/OSX/controlhost.mm
@@ -0,0 +1,160 @@
+#include "common.h"
+
+
+IAvnNativeControlHostTopLevelAttachment* CreateAttachment();
+
+class AvnNativeControlHost :
+ public ComSingleObject
+{
+public:
+ FORWARD_IUNKNOWN();
+ NSView* View;
+ AvnNativeControlHost(NSView* view)
+ {
+ View = view;
+ }
+
+ virtual HRESULT CreateDefaultChild(void* parent, void** retOut) override
+ {
+ NSView* view = [NSView new];
+ [view setWantsLayer: true];
+
+ *retOut = (__bridge_retained void*)view;
+ return S_OK;
+ };
+
+ virtual IAvnNativeControlHostTopLevelAttachment* CreateAttachment() override
+ {
+ return ::CreateAttachment();
+ };
+
+ virtual void DestroyDefaultChild(void* child) override
+ {
+ // ARC will release the object for us
+ (__bridge_transfer NSView*) child;
+ }
+};
+
+class AvnNativeControlHostTopLevelAttachment :
+public ComSingleObject
+{
+ NSView* _holder;
+ NSView* _child;
+public:
+ FORWARD_IUNKNOWN();
+
+ AvnNativeControlHostTopLevelAttachment()
+ {
+ _holder = [NSView new];
+ [_holder setWantsLayer:true];
+ }
+
+ virtual ~AvnNativeControlHostTopLevelAttachment()
+ {
+ if(_child != nil && [_child superview] == _holder)
+ {
+ [_child removeFromSuperview];
+ }
+
+ if([_holder superview] != nil)
+ {
+ [_holder removeFromSuperview];
+ }
+ }
+
+ virtual void* GetParentHandle() override
+ {
+ return (__bridge void*)_holder;
+ };
+
+ virtual HRESULT InitializeWithChildHandle(void* child) override
+ {
+ if(_child != nil)
+ return E_FAIL;
+ _child = (__bridge NSView*)child;
+ if(_child == nil)
+ return E_FAIL;
+ [_holder addSubview:_child];
+ [_child setHidden: false];
+ return S_OK;
+ };
+
+ virtual HRESULT AttachTo(IAvnNativeControlHost* host) override
+ {
+ if(host == nil)
+ {
+ [_holder removeFromSuperview];
+ [_holder setHidden: true];
+ }
+ else
+ {
+ AvnNativeControlHost* chost = dynamic_cast(host);
+ if(chost == nil || chost->View == nil)
+ return E_FAIL;
+ [_holder setHidden:true];
+ [chost->View addSubview:_holder];
+ }
+ return S_OK;
+ };
+
+ virtual void ShowInBounds(float x, float y, float width, float height) override
+ {
+ if(_child == nil)
+ return;
+ if(AvnInsidePotentialDeadlock::IsInside())
+ {
+ IAvnNativeControlHostTopLevelAttachment* slf = this;
+ slf->AddRef();
+ dispatch_async(dispatch_get_main_queue(), ^{
+ slf->ShowInBounds(x, y, width, height);
+ slf->Release();
+ });
+ return;
+ }
+
+ NSRect childFrame = {0, 0, width, height};
+ NSRect holderFrame = {x, y, width, height};
+
+ [_child setFrame: childFrame];
+ [_holder setFrame: holderFrame];
+ [_holder setHidden: false];
+ if([_holder superview] != nil)
+ [[_holder superview] setNeedsDisplay:true];
+ }
+
+ virtual void HideWithSize(float width, float height) override
+ {
+ if(_child == nil)
+ return;
+ if(AvnInsidePotentialDeadlock::IsInside())
+ {
+ IAvnNativeControlHostTopLevelAttachment* slf = this;
+ slf->AddRef();
+ dispatch_async(dispatch_get_main_queue(), ^{
+ slf->HideWithSize(width, height);
+ slf->Release();
+ });
+ return;
+ }
+
+ NSRect frame = {0, 0, width, height};
+ [_holder setHidden: true];
+ [_child setFrame: frame];
+ }
+
+ virtual void ReleaseChild() override
+ {
+ [_child removeFromSuperview];
+ _child = nil;
+ }
+};
+
+IAvnNativeControlHostTopLevelAttachment* CreateAttachment()
+{
+ return new AvnNativeControlHostTopLevelAttachment();
+}
+
+extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent)
+{
+ return new AvnNativeControlHost(parent);
+}
diff --git a/native/Avalonia.Native/src/OSX/deadlock.mm b/native/Avalonia.Native/src/OSX/deadlock.mm
new file mode 100644
index 0000000000..cb1767c90f
--- /dev/null
+++ b/native/Avalonia.Native/src/OSX/deadlock.mm
@@ -0,0 +1,17 @@
+#include "common.h"
+
+static int Counter = 0;
+AvnInsidePotentialDeadlock::AvnInsidePotentialDeadlock()
+{
+ Counter++;
+}
+
+AvnInsidePotentialDeadlock::~AvnInsidePotentialDeadlock()
+{
+ Counter--;
+}
+
+bool AvnInsidePotentialDeadlock::IsInside()
+{
+ return Counter!=0;
+}
diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h
index bdf3007a28..b1f64bca88 100644
--- a/native/Avalonia.Native/src/OSX/window.h
+++ b/native/Avalonia.Native/src/OSX/window.h
@@ -3,9 +3,6 @@
class WindowBaseImpl;
-@interface AutoFitContentVisualEffectView : NSVisualEffectView
-@end
-
@interface AvnView : NSView
-(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent;
-(NSEvent* _Nonnull) lastMouseDownEvent;
@@ -15,6 +12,14 @@ class WindowBaseImpl;
-(AvnPixelSize) getPixelSize;
@end
+@interface AutoFitContentView : NSView
+-(AutoFitContentView* _Nonnull) initWithContent: (NSView* _Nonnull) content;
+-(void) ShowTitleBar: (bool) show;
+-(void) SetTitleBarHeightHint: (double) height;
+-(void) SetContent: (NSView* _Nonnull) content;
+-(void) ShowBlur: (bool) show;
+@end
+
@interface AvnWindow : NSWindow
+(void) closeAll;
-(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent;
@@ -27,6 +32,8 @@ class WindowBaseImpl;
-(void) showWindowMenuWithAppMenu;
-(void) applyMenu:(NSMenu* _Nullable)menu;
-(double) getScaling;
+-(double) getExtendedTitleBarHeight;
+-(void) setIsExtended:(bool)value;
@end
struct INSWindowHolder
diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm
index f826df4da9..dd241409c7 100644
--- a/native/Avalonia.Native/src/OSX/window.mm
+++ b/native/Avalonia.Native/src/OSX/window.mm
@@ -6,8 +6,6 @@
#include
#include "rendertarget.h"
-
-
class WindowBaseImpl : public virtual ComSingleObject, public INSWindowHolder
{
private:
@@ -20,7 +18,7 @@ public:
View = NULL;
Window = NULL;
}
- NSVisualEffectView* VisualEffect;
+ AutoFitContentView* StandardContainer;
AvnView* View;
AvnWindow* Window;
ComPtr BaseEvents;
@@ -29,6 +27,7 @@ public:
AvnPoint lastPositionSet;
NSString* _lastTitle;
IAvnMenu* _mainMenu;
+
bool _shown;
WindowBaseImpl(IAvnWindowBaseEvents* events, IAvnGlContext* gl)
@@ -39,6 +38,7 @@ public:
_glContext = gl;
renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext: gl];
View = [[AvnView alloc] initWithParent:this];
+ StandardContainer = [[AutoFitContentView new] initWithContent:View];
Window = [[AvnWindow alloc] initWithParent:this];
@@ -49,12 +49,8 @@ public:
[Window setStyleMask:NSWindowStyleMaskBorderless];
[Window setBackingType:NSBackingStoreBuffered];
- VisualEffect = [AutoFitContentVisualEffectView new];
- [VisualEffect setBlendingMode:NSVisualEffectBlendingModeBehindWindow];
- [VisualEffect setMaterial:NSVisualEffectMaterialLight];
- [VisualEffect setAutoresizesSubviews:true];
-
- [Window setContentView: View];
+ [Window setOpaque:false];
+ [Window setContentView: StandardContainer];
}
virtual HRESULT ObtainNSWindowHandle(void** ret) override
@@ -116,10 +112,15 @@ public:
{
SetPosition(lastPositionSet);
UpdateStyle();
-
- [Window makeKeyAndOrderFront:Window];
- [NSApp activateIgnoringOtherApps:YES];
-
+ if(ShouldTakeFocusOnShow())
+ {
+ [Window makeKeyAndOrderFront:Window];
+ [NSApp activateIgnoringOtherApps:YES];
+ }
+ else
+ {
+ [Window orderFront: Window];
+ }
[Window setTitle:_lastTitle];
_shown = true;
@@ -128,6 +129,11 @@ public:
}
}
+ virtual bool ShouldTakeFocusOnShow()
+ {
+ return true;
+ }
+
virtual HRESULT Hide () override
{
@autoreleasepool
@@ -225,6 +231,29 @@ public:
{
@autoreleasepool
{
+ auto maxSize = [Window maxSize];
+ auto minSize = [Window minSize];
+
+ if (x < minSize.width)
+ {
+ x = minSize.width;
+ }
+
+ if (y < minSize.height)
+ {
+ y = minSize.height;
+ }
+
+ if (x > maxSize.width)
+ {
+ x = maxSize.width;
+ }
+
+ if (y > maxSize.height)
+ {
+ y = maxSize.height;
+ }
+
[Window setContentSize:NSSize{x, y}];
return S_OK;
@@ -390,14 +419,17 @@ public:
return *ppv == nil ? E_FAIL : S_OK;
}
+ virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost** retOut) override
+ {
+ if(View == NULL)
+ return E_FAIL;
+ *retOut = ::CreateNativeControlHost(View);
+ return S_OK;
+ }
+
virtual HRESULT SetBlurEnabled (bool enable) override
{
- [Window setContentView: enable ? VisualEffect : View];
-
- if(enable)
- {
- [VisualEffect addSubview:View];
- }
+ [StandardContainer ShowBlur:enable];
return S_OK;
}
@@ -474,6 +506,8 @@ private:
bool _inSetWindowState;
NSRect _preZoomSize;
bool _transitioningWindowState;
+ bool _isClientAreaExtended;
+ AvnExtendClientAreaChromeHints _extendClientHints;
FORWARD_IUNKNOWN()
BEGIN_INTERFACE_MAP()
@@ -487,6 +521,8 @@ private:
ComPtr WindowEvents;
WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl)
{
+ _isClientAreaExtended = false;
+ _extendClientHints = AvnDefaultChrome;
_fullScreenActive = false;
_canResize = true;
_decorations = SystemDecorationsFull;
@@ -505,8 +541,20 @@ private:
if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) {
NSView *titlebarView = [subview subviews][0];
for (id button in titlebarView.subviews) {
- if ([button isKindOfClass:[NSButton class]]) {
- [button setHidden: (_decorations != SystemDecorationsFull)];
+ if ([button isKindOfClass:[NSButton class]])
+ {
+ if(_isClientAreaExtended)
+ {
+ auto wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome);
+
+ [button setHidden: !wantsChrome];
+ }
+ else
+ {
+ [button setHidden: (_decorations != SystemDecorationsFull)];
+ }
+
+ [button setWantsLayer:true];
}
}
}
@@ -582,6 +630,35 @@ private:
if(_lastWindowState != state)
{
+ if(_isClientAreaExtended)
+ {
+ if(_lastWindowState == FullScreen)
+ {
+ // we exited fs.
+ if(_extendClientHints & AvnOSXThickTitleBar)
+ {
+ Window.toolbar = [NSToolbar new];
+ Window.toolbar.showsBaselineSeparator = false;
+ }
+
+ [Window setTitlebarAppearsTransparent:true];
+
+ [StandardContainer setFrameSize: StandardContainer.frame.size];
+ }
+ else if(state == FullScreen)
+ {
+ // we entered fs.
+ if(_extendClientHints & AvnOSXThickTitleBar)
+ {
+ Window.toolbar = nullptr;
+ }
+
+ [Window setTitlebarAppearsTransparent:false];
+
+ [StandardContainer setFrameSize: StandardContainer.frame.size];
+ }
+ }
+
_lastWindowState = state;
WindowEvents->WindowStateChanged(state);
}
@@ -638,8 +715,6 @@ private:
return S_OK;
}
- auto currentFrame = [Window frame];
-
UpdateStyle();
HideOrShowTrafficLights();
@@ -766,6 +841,90 @@ private:
}
}
+ virtual HRESULT TakeFocusFromChildren () override
+ {
+ if(Window == nil)
+ return S_OK;
+ if([Window isKeyWindow])
+ [Window makeFirstResponder: View];
+
+ return S_OK;
+ }
+
+ virtual HRESULT SetExtendClientArea (bool enable) override
+ {
+ _isClientAreaExtended = enable;
+
+ if(enable)
+ {
+ Window.titleVisibility = NSWindowTitleHidden;
+
+ [Window setTitlebarAppearsTransparent:true];
+
+ auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome);
+
+ if (wantsTitleBar)
+ {
+ [StandardContainer ShowTitleBar:true];
+ }
+ else
+ {
+ [StandardContainer ShowTitleBar:false];
+ }
+
+ if(_extendClientHints & AvnOSXThickTitleBar)
+ {
+ Window.toolbar = [NSToolbar new];
+ Window.toolbar.showsBaselineSeparator = false;
+ }
+ else
+ {
+ Window.toolbar = nullptr;
+ }
+ }
+ else
+ {
+ Window.titleVisibility = NSWindowTitleVisible;
+ Window.toolbar = nullptr;
+ [Window setTitlebarAppearsTransparent:false];
+ View.layer.zPosition = 0;
+ }
+
+ [Window setIsExtended:enable];
+
+ HideOrShowTrafficLights();
+
+ UpdateStyle();
+
+ return S_OK;
+ }
+
+ virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) override
+ {
+ _extendClientHints = hints;
+
+ SetExtendClientArea(_isClientAreaExtended);
+ return S_OK;
+ }
+
+ virtual HRESULT GetExtendTitleBarHeight (double*ret) override
+ {
+ if(ret == nullptr)
+ {
+ return E_POINTER;
+ }
+
+ *ret = [Window getExtendedTitleBarHeight];
+
+ return S_OK;
+ }
+
+ virtual HRESULT SetExtendTitleBarHeight (double value) override
+ {
+ [StandardContainer SetTitleBarHeightHint:value];
+ return S_OK;
+ }
+
void EnterFullScreenMode ()
{
_fullScreenActive = true;
@@ -775,8 +934,9 @@ private:
[Window setTitlebarAppearsTransparent:NO];
[Window setTitle:_lastTitle];
- [Window setStyleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskResizable];
-
+ Window.styleMask = Window.styleMask | NSWindowStyleMaskTitled | NSWindowStyleMaskResizable;
+ Window.styleMask = Window.styleMask & ~NSWindowStyleMaskFullSizeContentView;
+
[Window toggleFullScreen:nullptr];
}
@@ -924,19 +1084,120 @@ protected:
{
s |= NSWindowStyleMaskMiniaturizable;
}
+
+ if(_isClientAreaExtended)
+ {
+ s |= NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskTexturedBackground;
+ }
return s;
}
};
NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode, NSRunLoopCommonModes, NSConnectionReplyMode, nil];
-@implementation AutoFitContentVisualEffectView
+@implementation AutoFitContentView
+{
+ NSVisualEffectView* _titleBarMaterial;
+ NSBox* _titleBarUnderline;
+ NSView* _content;
+ NSVisualEffectView* _blurBehind;
+ double _titleBarHeightHint;
+ bool _settingSize;
+}
+
+-(AutoFitContentView* _Nonnull) initWithContent:(NSView *)content
+{
+ _titleBarHeightHint = -1;
+ _content = content;
+ _settingSize = false;
+
+ [self setAutoresizesSubviews:true];
+ [self setWantsLayer:true];
+
+ _titleBarMaterial = [NSVisualEffectView new];
+ [_titleBarMaterial setBlendingMode:NSVisualEffectBlendingModeWithinWindow];
+ [_titleBarMaterial setMaterial:NSVisualEffectMaterialTitlebar];
+ [_titleBarMaterial setWantsLayer:true];
+ _titleBarMaterial.hidden = true;
+
+ _titleBarUnderline = [NSBox new];
+ _titleBarUnderline.boxType = NSBoxSeparator;
+ _titleBarUnderline.fillColor = [NSColor underPageBackgroundColor];
+ _titleBarUnderline.hidden = true;
+
+ [self addSubview:_titleBarMaterial];
+ [self addSubview:_titleBarUnderline];
+
+ _blurBehind = [NSVisualEffectView new];
+ [_blurBehind setBlendingMode:NSVisualEffectBlendingModeBehindWindow];
+ [_blurBehind setMaterial:NSVisualEffectMaterialLight];
+ [_blurBehind setWantsLayer:true];
+ _blurBehind.hidden = true;
+
+ [self addSubview:_blurBehind];
+ [self addSubview:_content];
+
+ [self setWantsLayer:true];
+ return self;
+}
+
+-(void) ShowBlur:(bool)show
+{
+ _blurBehind.hidden = !show;
+}
+
+-(void) ShowTitleBar: (bool) show
+{
+ _titleBarMaterial.hidden = !show;
+ _titleBarUnderline.hidden = !show;
+}
+
+-(void) SetTitleBarHeightHint: (double) height
+{
+ _titleBarHeightHint = height;
+
+ [self setFrameSize:self.frame.size];
+}
+
-(void)setFrameSize:(NSSize)newSize
{
- [super setFrameSize:newSize];
- if([[self subviews] count] == 0)
+ if(_settingSize)
+ {
return;
- [[self subviews][0] setFrameSize: newSize];
+ }
+
+ _settingSize = true;
+ [super setFrameSize:newSize];
+
+ [_blurBehind setFrameSize:newSize];
+ [_content setFrameSize:newSize];
+
+ auto window = objc_cast([self window]);
+
+ // TODO get actual titlebar size
+
+ double height = _titleBarHeightHint == -1 ? [window getExtendedTitleBarHeight] : _titleBarHeightHint;
+
+ NSRect tbar;
+ tbar.origin.x = 0;
+ tbar.origin.y = newSize.height - height;
+ tbar.size.width = newSize.width;
+ tbar.size.height = height;
+
+ [_titleBarMaterial setFrame:tbar];
+ tbar.size.height = height < 1 ? 0 : 1;
+ [_titleBarUnderline setFrame:tbar];
+ _settingSize = false;
+}
+
+-(void) SetContent: (NSView* _Nonnull) content
+{
+ if(content != nullptr)
+ {
+ [content removeFromSuperview];
+ [self addSubview:content];
+ _content = content;
+ }
}
@end
@@ -1054,15 +1315,20 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_parent->UpdateCursor();
auto fsize = [self convertSizeToBacking: [self frame].size];
- _lastPixelSize.Width = (int)fsize.width;
- _lastPixelSize.Height = (int)fsize.height;
- [self updateRenderTarget];
- _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height});
+
+ if(_lastPixelSize.Width != (int)fsize.width || _lastPixelSize.Height != (int)fsize.height)
+ {
+ _lastPixelSize.Width = (int)fsize.width;
+ _lastPixelSize.Height = (int)fsize.height;
+ [self updateRenderTarget];
+
+ _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height});
+ }
}
-
- (void)updateLayer
{
+ AvnInsidePotentialDeadlock deadlock;
if (_parent == nullptr)
{
return;
@@ -1142,7 +1408,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
return;
}
- [self becomeFirstResponder];
auto localPoint = [self convertPoint:[event locationInWindow] toView:self];
auto avnPoint = [self toAvnPoint:localPoint];
auto point = [self translateLocalPoint:avnPoint];
@@ -1169,7 +1434,16 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
auto timestamp = [event timestamp] * 1000;
auto modifiers = [self getModifiers:[event modifierFlags]];
- [self becomeFirstResponder];
+ if(type != AvnRawMouseEventType::Move ||
+ (
+ [self window] != nil &&
+ (
+ [[self window] firstResponder] == nil
+ || ![[[self window] firstResponder] isKindOfClass: [NSView class]]
+ )
+ )
+ )
+ [self becomeFirstResponder];
if(_parent != nullptr)
{
@@ -1179,6 +1453,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
[super mouseMoved:event];
}
+- (BOOL) resignFirstResponder
+{
+ _parent->BaseEvents->LostFocus();
+ return YES;
+}
+
- (void)mouseMoved:(NSEvent *)event
{
[self mouseEvent:event withType:Move];
@@ -1482,15 +1762,43 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
bool _canBecomeKeyAndMain;
bool _closed;
bool _isEnabled;
+ bool _isExtended;
AvnMenu* _menu;
double _lastScaling;
}
+-(void) setIsExtended:(bool)value;
+{
+ _isExtended = value;
+}
+
-(double) getScaling
{
return _lastScaling;
}
+-(double) getExtendedTitleBarHeight
+{
+ if(_isExtended)
+ {
+ for (id subview in self.contentView.superview.subviews)
+ {
+ if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")])
+ {
+ NSView *titlebarView = [subview subviews][0];
+
+ return (double)titlebarView.frame.size.height;
+ }
+ }
+
+ return -1;
+ }
+ else
+ {
+ return 0;
+ }
+}
+
+(void)closeAll
{
NSArray* windows = [NSArray arrayWithArray:[NSApp windows]];
@@ -1609,6 +1917,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
[self setOpaque:NO];
[self setBackgroundColor: [NSColor clearColor]];
[self invalidateShadow];
+ _isExtended = false;
return self;
}
@@ -1836,7 +2145,6 @@ private:
WindowEvents = events;
[Window setLevel:NSPopUpMenuWindowLevel];
}
-
protected:
virtual NSWindowStyleMask GetStyle() override
{
@@ -1854,6 +2162,11 @@ protected:
return S_OK;
}
}
+public:
+ virtual bool ShouldTakeFocusOnShow() override
+ {
+ return false;
+ }
};
extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl)
diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs
index 5460dc7720..fbfbf47e1b 100644
--- a/nukebuild/Build.cs
+++ b/nukebuild/Build.cs
@@ -101,7 +101,7 @@ partial class Build : NukeBuild
.SetProjectFile(projectFile)
// This is required for VS2019 image on Azure Pipelines
.When(Parameters.IsRunningOnWindows &&
- Parameters.IsRunningOnAzure, c => c
+ Parameters.IsRunningOnAzure, _ => _
.AddProperty("JavaSdkDirectory", GetVariable("JAVA_HOME_8_X64")))
.AddProperty("PackageVersion", Parameters.Version)
.AddProperty("iOSRoslynPathHackRequired", true)
@@ -128,7 +128,9 @@ partial class Build : NukeBuild
{
var webappDir = RootDirectory / "src" / "Avalonia.DesignerSupport" / "Remote" / "HtmlTransport" / "webapp";
- NpmTasks.NpmInstall(c => c.SetWorkingDirectory(webappDir));
+ NpmTasks.NpmInstall(c => c
+ .SetWorkingDirectory(webappDir)
+ .SetArgumentConfigurator(a => a.Add("--silent")));
NpmTasks.NpmRun(c => c
.SetWorkingDirectory(webappDir)
.SetCommand("dist"));
@@ -176,7 +178,7 @@ partial class Build : NukeBuild
.SetFramework(fw)
.EnableNoBuild()
.EnableNoRestore()
- .When(Parameters.PublishTestResults, c => c
+ .When(Parameters.PublishTestResults, _ => _
.SetLogger("trx")
.SetResultsDirectory(Parameters.TestResultsRoot)));
}
@@ -268,6 +270,8 @@ partial class Build : NukeBuild
.DependsOn(CreateIntermediateNugetPackages)
.Executes(() =>
{
+ BuildTasksPatcher.PatchBuildTasksInPackage(Parameters.NugetIntermediateRoot / "Avalonia.Build.Tasks." +
+ Parameters.Version + ".nupkg");
var config = Numerge.MergeConfiguration.LoadFile(RootDirectory / "nukebuild" / "numerge.config");
EnsureCleanDirectory(Parameters.NugetRoot);
if(!Numerge.NugetPackageMerger.Merge(Parameters.NugetIntermediateRoot, Parameters.NugetRoot, config,
diff --git a/nukebuild/BuildTasksPatcher.cs b/nukebuild/BuildTasksPatcher.cs
new file mode 100644
index 0000000000..44f01da669
--- /dev/null
+++ b/nukebuild/BuildTasksPatcher.cs
@@ -0,0 +1,76 @@
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using ILRepacking;
+using Mono.Cecil;
+
+public class BuildTasksPatcher
+{
+ public static void PatchBuildTasksInPackage(string packagePath)
+ {
+ using (var archive = new ZipArchive(File.Open(packagePath, FileMode.Open, FileAccess.ReadWrite),
+ ZipArchiveMode.Update))
+ {
+
+ foreach (var entry in archive.Entries.ToList())
+ {
+ if (entry.Name == "Avalonia.Build.Tasks.dll")
+ {
+ var temp = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".dll");
+ var output = temp + ".output";
+ var patched = new MemoryStream();
+ try
+ {
+ entry.ExtractToFile(temp, true);
+ var repack = new ILRepacking.ILRepack(new RepackOptions()
+ {
+ Internalize = true,
+ InputAssemblies = new[]
+ {
+ temp, typeof(Mono.Cecil.AssemblyDefinition).Assembly.GetModules()[0]
+ .FullyQualifiedName
+ },
+ SearchDirectories = new string[0],
+ OutputFile = output
+ });
+ repack.Repack();
+
+
+ // 'hurr-durr assembly with the same name is already loaded' prevention
+ using (var asm = AssemblyDefinition.ReadAssembly(output,
+ new ReaderParameters { ReadWrite = true, InMemory = true, }))
+ {
+ asm.Name = new AssemblyNameDefinition(
+ "Avalonia.Build.Tasks."
+ + Guid.NewGuid().ToString().Replace("-", ""),
+ new Version(0, 0, 0));
+ asm.Write(patched);
+ patched.Position = 0;
+ }
+ }
+ finally
+ {
+ try
+ {
+ if (File.Exists(temp))
+ File.Delete(temp);
+ if (File.Exists(output))
+ File.Delete(output);
+ }
+ catch
+ {
+ //ignore
+ }
+ }
+
+ var fn = entry.FullName;
+ entry.Delete();
+ var newEntry = archive.CreateEntry(fn, CompressionLevel.Optimal);
+ using (var s = newEntry.Open())
+ patched.CopyTo(s);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/nukebuild/Numerge b/nukebuild/Numerge
index 4464343aef..aef10ae67d 160000
--- a/nukebuild/Numerge
+++ b/nukebuild/Numerge
@@ -1 +1 @@
-Subproject commit 4464343aef5c8ab7a42fcb20a483a6058199f8b8
+Subproject commit aef10ae67dc55c95f49b52a505a0be33bfa297a5
diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj
index 584c36d033..4c64d4ff93 100644
--- a/nukebuild/_build.csproj
+++ b/nukebuild/_build.csproj
@@ -14,6 +14,9 @@
+
+
+
diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj
index 2c5a09bee7..cd3ce9adcd 100644
--- a/packages/Avalonia/Avalonia.csproj
+++ b/packages/Avalonia/Avalonia.csproj
@@ -41,6 +41,10 @@
true
build\
+
+ true
+ build\
+
diff --git a/packages/Avalonia/AvaloniaBuildTasks.props b/packages/Avalonia/AvaloniaBuildTasks.props
index 30bafa37ee..deea3aa391 100644
--- a/packages/Avalonia/AvaloniaBuildTasks.props
+++ b/packages/Avalonia/AvaloniaBuildTasks.props
@@ -1,3 +1,11 @@
-
+
+
+
+
+
+
+
+
+
diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets
index 537495fcad..84a62bb5c0 100644
--- a/packages/Avalonia/AvaloniaBuildTasks.targets
+++ b/packages/Avalonia/AvaloniaBuildTasks.targets
@@ -4,6 +4,20 @@
<_AvaloniaUseExternalMSBuild Condition="'$(_AvaloniaForceInternalMSBuild)' == 'true'">false
low
+
+
+
+
+ %(Filename)
+ Code
+
+
+ %(Filename)
+ Code
+
+
+
+
+ DependsOnTargets="$(BuildAvaloniaResourcesDependsOn)">
+
+
+
+
diff --git a/packages/Avalonia/AvaloniaItemSchema.xaml b/packages/Avalonia/AvaloniaItemSchema.xaml
new file mode 100644
index 0000000000..a51ea3c0be
--- /dev/null
+++ b/packages/Avalonia/AvaloniaItemSchema.xaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/readme.md b/readme.md
index a9263d4816..c1bf5bbfde 100644
--- a/readme.md
+++ b/readme.md
@@ -1,4 +1,4 @@
-[](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [](#backers) [](#sponsors) 
+[](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) []( https://aka.ms/dotnet-discord) [](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [](#backers) [](#sponsors) 
[](https://www.nuget.org/packages/Avalonia) [](https://www.nuget.org/packages/Avalonia) [](https://www.myget.org/gallery/avalonia-ci) 
@@ -16,7 +16,7 @@ To see the status of some of our features, please see our [Roadmap](https://gith
## 🚀 Getting Started
-The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starer guide see our [documentation](http://avaloniaui.net/docs/quickstart/create-new-project).
+The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starter guide see our [documentation](http://avaloniaui.net/docs/quickstart/create-new-project).
Avalonia is delivered via NuGet package manager. You can find the packages here: https://www.nuget.org/packages/Avalonia/
@@ -68,7 +68,7 @@ Avalonia is licenced under the [MIT licence](licence.md).
## Contributors
-This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing/contributing)].
+This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing)].
### Backers
diff --git a/samples/BindingDemo/MainWindow.xaml b/samples/BindingDemo/MainWindow.xaml
index 26a62ebca6..14c371efef 100644
--- a/samples/BindingDemo/MainWindow.xaml
+++ b/samples/BindingDemo/MainWindow.xaml
@@ -5,7 +5,8 @@
xmlns:local="clr-namespace:BindingDemo"
Title="AvaloniaUI Bindings Test"
Width="800"
- Height="600">
+ Height="600"
+ x:DataType="vm:MainWindowViewModel">
diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs
index a6754143a2..b0fbcc76d2 100644
--- a/samples/ControlCatalog/App.xaml.cs
+++ b/samples/ControlCatalog/App.xaml.cs
@@ -11,37 +11,37 @@ namespace ControlCatalog
{
public static Styles FluentDark = new Styles
{
- new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
+ new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
- Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseDark.xaml?assembly=Avalonia.Themes.Default")
+ Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentDark.xaml")
},
- new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
+ };
+
+ public static Styles FluentLight = new Styles
+ {
+ new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
- Source = new Uri("resm:Avalonia.Themes.Fluent.Accents.FluentDark.xaml?assembly=Avalonia.Themes.Fluent")
+ Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentLight.xaml")
},
};
- public static Styles FluentLight = new Styles
+ public static Styles DefaultLight = new Styles
{
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
- Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default")
+ Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml")
},
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
- Source = new Uri("resm:Avalonia.Themes.Fluent.Accents.FluentLight.xaml?assembly=Avalonia.Themes.Fluent")
+ Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseLight.xaml")
},
- };
-
- public static Styles DefaultLight = new Styles
- {
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
- Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default")
+ Source = new Uri("avares://Avalonia.Themes.Default/Accents/BaseLight.xaml")
},
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
- Source = new Uri("resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default")
+ Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml")
},
};
@@ -49,19 +49,27 @@ namespace ControlCatalog
{
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
- Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseDark.xaml?assembly=Avalonia.Themes.Default")
+ Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml")
+ },
+ new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
+ {
+ Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseDark.xaml")
},
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
- Source = new Uri("resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default")
+ Source = new Uri("avares://Avalonia.Themes.Default/Accents/BaseDark.xaml")
+ },
+ new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
+ {
+ Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml")
},
};
public override void Initialize()
{
- AvaloniaXamlLoader.Load(this);
-
Styles.Insert(0, FluentDark);
+
+ AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 488062f5b6..efc90357ed 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -1,10 +1,7 @@
+ x:Class="ControlCatalog.MainView">
+
@@ -29,6 +27,9 @@
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
+
+
+
@@ -52,8 +53,10 @@
+
+
@@ -62,6 +65,7 @@
+
@@ -75,7 +79,7 @@
Simple - Light
Simple - Dark
-
+
None
Transparent
Blur
diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs
index d6d4a71ad3..b0c205246e 100644
--- a/samples/ControlCatalog/MainView.xaml.cs
+++ b/samples/ControlCatalog/MainView.xaml.cs
@@ -58,13 +58,6 @@ namespace ControlCatalog
if (VisualRoot is Window window)
window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex;
};
-
- var transparencyLevels = this.Find("TransparencyLevels");
- transparencyLevels.SelectionChanged += (sender, e) =>
- {
- if (VisualRoot is Window window)
- window.TransparencyLevelHint = (WindowTransparencyLevel)transparencyLevels.SelectedIndex;
- };
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml
index 76422bc130..97bd88f5e4 100644
--- a/samples/ControlCatalog/MainWindow.xaml
+++ b/samples/ControlCatalog/MainWindow.xaml
@@ -7,7 +7,12 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ControlCatalog.ViewModels"
xmlns:v="clr-namespace:ControlCatalog.Views"
- x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" Background="{DynamicResource SystemControlPageBackgroundAltHighBrush}">
+ ExtendClientAreaToDecorationsHint="{Binding ExtendClientAreaEnabled}"
+ ExtendClientAreaChromeHints="{Binding ChromeHints}"
+ ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}"
+ TransparencyLevelHint="{Binding TransparencyLevel}"
+ x:Name="MainWindow"
+ x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}">
@@ -56,20 +61,30 @@
-
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs
index d97325ef8d..a316321cd1 100644
--- a/samples/ControlCatalog/MainWindow.xaml.cs
+++ b/samples/ControlCatalog/MainWindow.xaml.cs
@@ -63,8 +63,18 @@ namespace ControlCatalog
// TODO: iOS does not support dynamically loading assemblies
// so we must refer to this resource DLL statically. For
// now I am doing that here. But we need a better solution!!
- var theme = new Avalonia.Themes.Default.DefaultTheme();
- theme.TryGetResource("Button", out _);
+ // Note, theme swiching probably will not work in runtime for iOS.
+ if (Application.Current.Styles.Contains(App.FluentDark)
+ || Application.Current.Styles.Contains(App.FluentLight))
+ {
+ var theme = new Avalonia.Themes.Fluent.FluentTheme();
+ theme.TryGetResource("Button", out _);
+ }
+ else
+ {
+ var theme = new Avalonia.Themes.Default.DefaultTheme();
+ theme.TryGetResource("Button", out _);
+ }
AvaloniaXamlLoader.Load(this);
}
}
diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml b/samples/ControlCatalog/Pages/AcrylicPage.xaml
new file mode 100644
index 0000000000..96cfcc5288
--- /dev/null
+++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml.cs b/samples/ControlCatalog/Pages/AcrylicPage.xaml.cs
new file mode 100644
index 0000000000..44e7c4b92b
--- /dev/null
+++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml.cs
@@ -0,0 +1,19 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace ControlCatalog.Pages
+{
+ public class AcrylicPage : UserControl
+ {
+ public AcrylicPage()
+ {
+ this.InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/BorderPage.xaml b/samples/ControlCatalog/Pages/BorderPage.xaml
index 8133d0e408..bfd14cc627 100644
--- a/samples/ControlCatalog/Pages/BorderPage.xaml
+++ b/samples/ControlCatalog/Pages/BorderPage.xaml
@@ -9,27 +9,27 @@
Margin="0,16,0,0"
HorizontalAlignment="Center"
Spacing="16">
-
+
Border
-
Border and Background
-
Rounded Corners
-
Rounded Corners
-
diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml b/samples/ControlCatalog/Pages/ButtonPage.xaml
index 7e945aeaa9..be114bbbc9 100644
--- a/samples/ControlCatalog/Pages/ButtonPage.xaml
+++ b/samples/ControlCatalog/Pages/ButtonPage.xaml
@@ -9,10 +9,10 @@
Margin="0,16,0,0"
HorizontalAlignment="Center"
Spacing="16">
-
-
+
+
-
+
diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml b/samples/ControlCatalog/Pages/ComboBoxPage.xaml
index 486cc55d44..369f703718 100644
--- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml
@@ -16,7 +16,7 @@
-
+
Control Items
diff --git a/samples/ControlCatalog/Pages/ContextMenuPage.xaml b/samples/ControlCatalog/Pages/ContextMenuPage.xaml
index 8f147638b5..260162ddb9 100644
--- a/samples/ControlCatalog/Pages/ContextMenuPage.xaml
+++ b/samples/ControlCatalog/Pages/ContextMenuPage.xaml
@@ -9,18 +9,19 @@
Margin="0,16,0,0"
HorizontalAlignment="Center"
Spacing="16">
-
-
+
+
-
+
@@ -34,7 +35,7 @@
-
diff --git a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml
new file mode 100644
index 0000000000..af6b6e8605
--- /dev/null
+++ b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml
@@ -0,0 +1,123 @@
+
+
+ DatePicker and TimePicker
+
+
+
+
+ A simple DatePicker with a header
+
+
+
+
+
+
+
+
+ <DatePicker Header="Pick a date" />
+
+
+
+
+
+
+ A DatePicker with day formatted and year hidden.
+
+
+
+
+
+
+
+
+ <DatePicker DayFormat="d (ddd)" YearVisible="False" />
+
+
+
+
+
+
+
+
+ A simple TimePicker.
+
+
+
+
+
+
+
+
+ <TimePicker />
+
+
+
+
+
+
+ A TimePicker with a header and minute increments specified.
+
+
+
+
+
+
+
+
+ <TimePicker Header="Arrival time" MinuteIncrement="15" />
+
+
+
+
+
+
+ A TimePicker using a 12-hour clock.
+
+
+
+
+
+
+
+
+ <TimePicker ClockIdentifier="12HourClock" Header="12 hour clock" />
+
+
+
+
+
+
+ A TimePicker using a 24-hour clock.
+
+
+
+
+
+
+
+
+ <TimePicker ClockIdentifier="24HourClock" Header="24 hour clock" />
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs
new file mode 100644
index 0000000000..6c7ae3437e
--- /dev/null
+++ b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs
@@ -0,0 +1,30 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace ControlCatalog.Pages
+{
+ public class DateTimePickerPage : UserControl
+ {
+ public DateTimePickerPage()
+ {
+ this.InitializeComponent();
+ this.FindControl("DatePickerDesc").Text = "Use a DatePicker to let users set a date in your app, " +
+ "for example to schedule an appointment. The DatePicker displays three controls for month, day, and year. " +
+ "These controls are easy to use with touch or mouse, and they can be styled and configured in several different ways. " +
+ "Order of month, day, and year is dynamically set based on user date settings";
+
+ this.FindControl("TimePickerDesc").Text = "Use a TimePicker to let users set a time in your app, for example " +
+ "to set a reminder. The TimePicker displays three controls for hour, minute, and AM / PM(if necessary).These controls " +
+ "are easy to use with touch or mouse, and they can be styled and configured in several different ways. " +
+ "12 - hour or 24 - hour clock and visiblility of AM / PM is dynamically set based on user time settings, or can be overridden.";
+
+
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml b/samples/ControlCatalog/Pages/DragAndDropPage.xaml
index 65a798e53c..c22cf68b68 100644
--- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml
+++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml
@@ -10,15 +10,15 @@
HorizontalAlignment="Center"
Spacing="16">
-
+
Drag Me
-
+
Drag Me (custom)
-
Drop some text or files here
diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
index e600e644af..392ccb57c3 100644
--- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
+++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
@@ -1,6 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
ItemsRepeater
@@ -19,20 +43,12 @@
Scroll to Last
Scroll to Random
-
+
-
-
-
-
-
-
-
+
diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
index cce80a2d3c..9e898c4536 100644
--- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
@@ -38,6 +38,12 @@ namespace ControlCatalog.Pages
AvaloniaXamlLoader.Load(this);
}
+ public void OnSelectTemplateKey(object sender, SelectTemplateEventArgs e)
+ {
+ var item = (ItemsRepeaterPageViewModel.Item)e.DataContext;
+ e.TemplateKey = (item.Index % 2 == 0) ? "even" : "odd";
+ }
+
private void LayoutChanged(object sender, SelectionChangedEventArgs e)
{
if (_repeater == null)
@@ -79,6 +85,26 @@ namespace ControlCatalog.Pages
MinItemHeight = 200,
};
break;
+ case 4:
+ _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
+ _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
+ _repeater.Layout = new WrapLayout
+ {
+ Orientation = Orientation.Vertical,
+ HorizontalSpacing = 20,
+ VerticalSpacing = 20
+ };
+ break;
+ case 5:
+ _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled;
+ _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
+ _repeater.Layout = new WrapLayout
+ {
+ Orientation = Orientation.Horizontal,
+ HorizontalSpacing = 20,
+ VerticalSpacing = 20
+ };
+ break;
}
}
diff --git a/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml b/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml
index 446dfd7ce1..8cf3610b47 100644
--- a/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml
+++ b/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml
@@ -11,10 +11,10 @@
RowDefinitions="24,Auto,24"
HorizontalAlignment="Center"
VerticalAlignment="Center">
-
-
-
-
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml
index 47b4ce7151..edf3d41bf5 100644
--- a/samples/ControlCatalog/Pages/ListBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml
@@ -10,7 +10,12 @@
HorizontalAlignment="Center"
Spacing="16">
-
+
Add
diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs
index 0f2d4461bf..59c8fc0cbe 100644
--- a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs
@@ -1,10 +1,6 @@
-using System;
-using System.Collections.ObjectModel;
-using System.Linq;
-using System.Reactive;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
-using ReactiveUI;
+using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
@@ -13,72 +9,12 @@ namespace ControlCatalog.Pages
public ListBoxPage()
{
InitializeComponent();
- DataContext = new PageViewModel();
+ DataContext = new ListBoxPageViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
-
- private class PageViewModel : ReactiveObject
- {
- private int _counter;
- private SelectionMode _selectionMode;
-
- public PageViewModel()
- {
- Items = new ObservableCollection(Enumerable.Range(1, 10000).Select(i => GenerateItem()));
- SelectedItems = new ObservableCollection();
-
- AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem()));
-
- RemoveItemCommand = ReactiveCommand.Create(() =>
- {
- while (SelectedItems.Count > 0)
- {
- Items.Remove(SelectedItems[0]);
- }
- });
-
- SelectRandomItemCommand = ReactiveCommand.Create(() =>
- {
- var random = new Random();
-
- SelectedItem = Items[random.Next(Items.Count - 1)];
- });
- }
-
- public ObservableCollection Items { get; }
-
- private string _selectedItem;
-
- public string SelectedItem
- {
- get { return _selectedItem; }
- set { this.RaiseAndSetIfChanged(ref _selectedItem, value); }
- }
-
-
- public ObservableCollection SelectedItems { get; }
-
- public ReactiveCommand AddItemCommand { get; }
-
- public ReactiveCommand RemoveItemCommand { get; }
-
- public ReactiveCommand SelectRandomItemCommand { get; }
-
- public SelectionMode SelectionMode
- {
- get => _selectionMode;
- set
- {
- SelectedItems.Clear();
- this.RaiseAndSetIfChanged(ref _selectionMode, value);
- }
- }
-
- private string GenerateItem() => $"Item {_counter++.ToString()}";
- }
}
}
diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml b/samples/ControlCatalog/Pages/MenuPage.xaml
index de9ea34e80..2c09cb9b4d 100644
--- a/samples/ControlCatalog/Pages/MenuPage.xaml
+++ b/samples/ControlCatalog/Pages/MenuPage.xaml
@@ -16,11 +16,17 @@
Defined in XAML
+
+
+
+ Mixed
+
+
diff --git a/samples/ControlCatalog/Pages/ProgressBarPage.xaml b/samples/ControlCatalog/Pages/ProgressBarPage.xaml
index 13bae59805..2ec0b48c76 100644
--- a/samples/ControlCatalog/Pages/ProgressBarPage.xaml
+++ b/samples/ControlCatalog/Pages/ProgressBarPage.xaml
@@ -1,29 +1,19 @@
-
+
ProgressBar
A progress bar control
-
-
-
+
+
+
-
-
+
-
-
+
-
-
+
+
diff --git a/samples/ControlCatalog/Pages/RelativePanelPage.axaml b/samples/ControlCatalog/Pages/RelativePanelPage.axaml
new file mode 100644
index 0000000000..3657d52bd9
--- /dev/null
+++ b/samples/ControlCatalog/Pages/RelativePanelPage.axaml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs b/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs
new file mode 100644
index 0000000000..11d0a5152e
--- /dev/null
+++ b/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs
@@ -0,0 +1,19 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace ControlCatalog.Pages
+{
+ public class RelativePanelPage : UserControl
+ {
+ public RelativePanelPage()
+ {
+ this.InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs
index d775eb9635..c39f414b44 100644
--- a/samples/ControlCatalog/Pages/ScreenPage.cs
+++ b/samples/ControlCatalog/Pages/ScreenPage.cs
@@ -29,7 +29,8 @@ namespace ControlCatalog.Pages
var screens = w.Screens.All;
var scaling = ((IRenderRoot)w).RenderScaling;
- Pen p = new Pen(Brushes.Black);
+ var drawBrush = Brushes.Green;
+ Pen p = new Pen(drawBrush);
if (screens != null)
foreach (Screen screen in screens)
{
@@ -53,19 +54,19 @@ namespace ControlCatalog.Pages
};
text.Text = $"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}";
- context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height), text);
+ context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height), text);
text.Text = $"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}";
- context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height + 20), text);
+ context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 20), text);
text.Text = $"Scaling: {screen.PixelDensity * 100}%";
- context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height + 40), text);
+ context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 40), text);
text.Text = $"Primary: {screen.Primary}";
- context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height + 60), text);
+ context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 60), text);
text.Text = $"Current: {screen.Equals(w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))))}";
- context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height + 80), text);
+ context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 80), text);
}
context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10, w.Bounds.Width / 10, w.Bounds.Height / 10));
diff --git a/samples/ControlCatalog/Pages/SliderPage.xaml b/samples/ControlCatalog/Pages/SliderPage.xaml
index c6f5521e60..ea31ed0050 100644
--- a/samples/ControlCatalog/Pages/SliderPage.xaml
+++ b/samples/ControlCatalog/Pages/SliderPage.xaml
@@ -6,11 +6,22 @@
A control that lets the user select from a range of values by moving a Thumb control along a Track.
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Inline
+ CompactInline
+ Overlay
+ CompactOverlay
+
+
+
+
+ SystemControlBackgroundChromeMediumLowBrush
+ Red
+ Blue
+ Green
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/SplitViewPage.xaml.cs b/samples/ControlCatalog/Pages/SplitViewPage.xaml.cs
new file mode 100644
index 0000000000..cbf217c94a
--- /dev/null
+++ b/samples/ControlCatalog/Pages/SplitViewPage.xaml.cs
@@ -0,0 +1,21 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
+
+namespace ControlCatalog.Pages
+{
+ public class SplitViewPage : UserControl
+ {
+ public SplitViewPage()
+ {
+ this.InitializeComponent();
+ DataContext = new SplitViewPageViewModel();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml
index f73ef9b4fb..4a1c196917 100644
--- a/samples/ControlCatalog/Pages/TextBlockPage.xaml
+++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml
@@ -12,7 +12,7 @@
@@ -49,7 +49,7 @@
@@ -64,51 +64,42 @@
-
-
-
-
-
-
-
-
-
-
-
-
+ StrokeThicknessUnit="Pixel"
+ StrokeThickness="2">
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+ StrokeThicknessUnit="Pixel"
+ StrokeThickness="1">
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+ StrokeThicknessUnit="Pixel"
+ StrokeThickness="2">
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml b/samples/ControlCatalog/Pages/TextBoxPage.xaml
index 64118a00b4..481a459159 100644
--- a/samples/ControlCatalog/Pages/TextBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml
@@ -20,6 +20,7 @@
@@ -37,6 +38,8 @@
Text="Multiline TextBox with TextWrapping.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." />
+
+
resm fonts
diff --git a/samples/ControlCatalog/Pages/ToolTipPage.xaml b/samples/ControlCatalog/Pages/ToolTipPage.xaml
index 73d83e08f1..ec073d48a9 100644
--- a/samples/ControlCatalog/Pages/ToolTipPage.xaml
+++ b/samples/ControlCatalog/Pages/ToolTipPage.xaml
@@ -12,7 +12,7 @@
HorizontalAlignment="Center">
@@ -26,7 +26,7 @@
diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml
new file mode 100644
index 0000000000..b90f43c3b6
--- /dev/null
+++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+ None
+ Transparent
+ Blur
+ AcrylicBlur
+
+
+
diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs
new file mode 100644
index 0000000000..d8d4f3f371
--- /dev/null
+++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs
@@ -0,0 +1,19 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace ControlCatalog.Pages
+{
+ public class WindowCustomizationsPage : UserControl
+ {
+ public WindowCustomizationsPage()
+ {
+ this.InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml
index 1ee6bf7a29..7c911e91e9 100644
--- a/samples/ControlCatalog/SideBar.xaml
+++ b/samples/ControlCatalog/SideBar.xaml
@@ -12,7 +12,7 @@
diff --git a/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
index 73aaeff994..f893a6e28e 100644
--- a/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
+++ b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
@@ -62,13 +62,9 @@ namespace ControlCatalog.ViewModels
public class Item : ReactiveObject
{
private double _height = double.NaN;
- private int _index;
-
- public Item(int index)
- {
- _index = index;
- }
+ public Item(int index) => Index = index;
+ public int Index { get; }
public string Text { get; set; }
public double Height
@@ -76,8 +72,6 @@ namespace ControlCatalog.ViewModels
get => _height;
set => this.RaiseAndSetIfChanged(ref _height, value);
}
-
- public IBrush Background => ((_index % 2) == 0) ? Brushes.Yellow : Brushes.Wheat;
}
}
}
diff --git a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
new file mode 100644
index 0000000000..6bdb5c0103
--- /dev/null
+++ b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reactive;
+using Avalonia.Controls;
+using ReactiveUI;
+
+namespace ControlCatalog.ViewModels
+{
+ public class ListBoxPageViewModel : ReactiveObject
+ {
+ private int _counter;
+ private SelectionMode _selectionMode;
+
+ public ListBoxPageViewModel()
+ {
+ Items = new ObservableCollection(Enumerable.Range(1, 10000).Select(i => GenerateItem()));
+ Selection = new SelectionModel();
+ Selection.Select(1);
+
+ AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem()));
+
+ RemoveItemCommand = ReactiveCommand.Create(() =>
+ {
+ while (Selection.SelectedItems.Count > 0)
+ {
+ Items.Remove((string)Selection.SelectedItems.First());
+ }
+ });
+
+ SelectRandomItemCommand = ReactiveCommand.Create(() =>
+ {
+ var random = new Random();
+
+ using (Selection.Update())
+ {
+ Selection.ClearSelection();
+ Selection.Select(random.Next(Items.Count - 1));
+ }
+ });
+ }
+
+ public ObservableCollection Items { get; }
+
+ public SelectionModel Selection { get; }
+
+ public ReactiveCommand AddItemCommand { get; }
+
+ public ReactiveCommand RemoveItemCommand { get; }
+
+ public ReactiveCommand SelectRandomItemCommand { get; }
+
+ public SelectionMode SelectionMode
+ {
+ get => _selectionMode;
+ set
+ {
+ Selection.ClearSelection();
+ this.RaiseAndSetIfChanged(ref _selectionMode, value);
+ }
+ }
+
+ private string GenerateItem() => $"Item {_counter++.ToString()}";
+ }
+}
diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
index 0257b4ce66..4356a032fa 100644
--- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
+++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
@@ -3,6 +3,8 @@ using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Notifications;
using Avalonia.Dialogs;
+using Avalonia.Platform;
+using System;
using ReactiveUI;
namespace ControlCatalog.ViewModels
@@ -14,6 +16,12 @@ namespace ControlCatalog.ViewModels
private bool _isMenuItemChecked = true;
private WindowState _windowState;
private WindowState[] _windowStates;
+ private int _transparencyLevel;
+ private ExtendClientAreaChromeHints _chromeHints;
+ private bool _extendClientAreaEnabled;
+ private bool _systemTitleBarEnabled;
+ private bool _preferSystemChromeEnabled;
+ private double _titleBarHeight;
public MainWindowViewModel(IManagedNotificationManager notificationManager)
{
@@ -62,6 +70,63 @@ namespace ControlCatalog.ViewModels
WindowState.Maximized,
WindowState.FullScreen,
};
+
+ this.WhenAnyValue(x => x.SystemTitleBarEnabled, x=>x.PreferSystemChromeEnabled)
+ .Subscribe(x =>
+ {
+ var hints = ExtendClientAreaChromeHints.NoChrome | ExtendClientAreaChromeHints.OSXThickTitleBar;
+
+ if(x.Item1)
+ {
+ hints |= ExtendClientAreaChromeHints.SystemChrome;
+ }
+
+ if(x.Item2)
+ {
+ hints |= ExtendClientAreaChromeHints.PreferSystemChrome;
+ }
+
+ ChromeHints = hints;
+ });
+
+ SystemTitleBarEnabled = true;
+ TitleBarHeight = -1;
+ }
+
+ public int TransparencyLevel
+ {
+ get { return _transparencyLevel; }
+ set { this.RaiseAndSetIfChanged(ref _transparencyLevel, value); }
+ }
+
+ public ExtendClientAreaChromeHints ChromeHints
+ {
+ get { return _chromeHints; }
+ set { this.RaiseAndSetIfChanged(ref _chromeHints, value); }
+ }
+
+ public bool ExtendClientAreaEnabled
+ {
+ get { return _extendClientAreaEnabled; }
+ set { this.RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value); }
+ }
+
+ public bool SystemTitleBarEnabled
+ {
+ get { return _systemTitleBarEnabled; }
+ set { this.RaiseAndSetIfChanged(ref _systemTitleBarEnabled, value); }
+ }
+
+ public bool PreferSystemChromeEnabled
+ {
+ get { return _preferSystemChromeEnabled; }
+ set { this.RaiseAndSetIfChanged(ref _preferSystemChromeEnabled, value); }
+ }
+
+ public double TitleBarHeight
+ {
+ get { return _titleBarHeight; }
+ set { this.RaiseAndSetIfChanged(ref _titleBarHeight, value); }
}
public WindowState WindowState
diff --git a/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs b/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs
index dc9c4a8f49..9e7ae8b716 100644
--- a/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs
+++ b/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs
@@ -17,6 +17,23 @@ namespace ControlCatalog.ViewModels
SaveCommand = ReactiveCommand.Create(Save, Observable.Return(false));
OpenRecentCommand = ReactiveCommand.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
@@ -24,27 +41,13 @@ namespace ControlCatalog.ViewModels
Header = "_File",
Items = new[]
{
- new MenuItemViewModel { Header = "_Open...", Command = OpenCommand },
+ new MenuItemViewModel { Header = "O_pen...", 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"
- },
- }
+ Items = recentItems
},
}
},
@@ -61,6 +64,7 @@ namespace ControlCatalog.ViewModels
}
public IReadOnlyList MenuItems { get; set; }
+ public IReadOnlyList RecentItems { get; set; }
public ReactiveCommand OpenCommand { get; }
public ReactiveCommand SaveCommand { get; }
public ReactiveCommand OpenRecentCommand { get; }
diff --git a/samples/ControlCatalog/ViewModels/SplitViewPageViewModel.cs b/samples/ControlCatalog/ViewModels/SplitViewPageViewModel.cs
new file mode 100644
index 0000000000..f27f605a8b
--- /dev/null
+++ b/samples/ControlCatalog/ViewModels/SplitViewPageViewModel.cs
@@ -0,0 +1,46 @@
+using System;
+using Avalonia.Controls;
+using ReactiveUI;
+
+namespace ControlCatalog.ViewModels
+{
+ public class SplitViewPageViewModel : ReactiveObject
+ {
+ private bool _isLeft = true;
+ private int _displayMode = 3; //CompactOverlay
+
+ public bool IsLeft
+ {
+ get => _isLeft;
+ set
+ {
+ this.RaiseAndSetIfChanged(ref _isLeft, value);
+ this.RaisePropertyChanged(nameof(PanePlacement));
+ }
+ }
+
+ public int DisplayMode
+ {
+ get => _displayMode;
+ set
+ {
+ this.RaiseAndSetIfChanged(ref _displayMode, value);
+ this.RaisePropertyChanged(nameof(CurrentDisplayMode));
+ }
+ }
+
+ public SplitViewPanePlacement PanePlacement => _isLeft ? SplitViewPanePlacement.Left : SplitViewPanePlacement.Right;
+
+ public SplitViewDisplayMode CurrentDisplayMode
+ {
+ get
+ {
+ if (Enum.IsDefined(typeof(SplitViewDisplayMode), _displayMode))
+ {
+ return (SplitViewDisplayMode)_displayMode;
+ }
+ return SplitViewDisplayMode.CompactOverlay;
+ }
+ }
+ }
+}
diff --git a/samples/RenderDemo/App.xaml b/samples/RenderDemo/App.xaml
index ccc3f54cc0..61e4d2385b 100644
--- a/samples/RenderDemo/App.xaml
+++ b/samples/RenderDemo/App.xaml
@@ -3,8 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="RenderDemo.App">
-
-
+
-
\ No newline at end of file
+
diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml
index 14ccc82043..770960d7c4 100644
--- a/samples/RenderDemo/MainWindow.xaml
+++ b/samples/RenderDemo/MainWindow.xaml
@@ -44,6 +44,9 @@
+
+
+
diff --git a/samples/RenderDemo/Pages/ClippingPage.xaml b/samples/RenderDemo/Pages/ClippingPage.xaml
index 8920b8a123..10225f7c49 100644
--- a/samples/RenderDemo/Pages/ClippingPage.xaml
+++ b/samples/RenderDemo/Pages/ClippingPage.xaml
@@ -43,7 +43,7 @@
C 72.078834 28.113269 74.047517 25.960974 74.931641 23.777344
C 78.93827 14.586564 73.049722 2.8815081 63.248047 0.67382812
C 61.721916 0.22817968 60.165597 0.038541919 58.625 0.07421875 z ">
-
+
@@ -53,4 +53,4 @@
Apply Geometry Clip
-
\ No newline at end of file
+
diff --git a/samples/RenderDemo/Pages/WriteableBitmapPage.cs b/samples/RenderDemo/Pages/WriteableBitmapPage.cs
new file mode 100644
index 0000000000..850e398a93
--- /dev/null
+++ b/samples/RenderDemo/Pages/WriteableBitmapPage.cs
@@ -0,0 +1,92 @@
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.LogicalTree;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Avalonia.Media.Immutable;
+using Avalonia.Platform;
+using Avalonia.Threading;
+
+namespace RenderDemo.Pages
+{
+ public class WriteableBitmapPage : Control
+ {
+ private WriteableBitmap _unpremulBitmap;
+ private WriteableBitmap _premulBitmap;
+ private readonly Stopwatch _st = Stopwatch.StartNew();
+
+ protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
+ {
+ _unpremulBitmap = new WriteableBitmap(new PixelSize(256, 256), new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Unpremul);
+ _premulBitmap = new WriteableBitmap(new PixelSize(256, 256), new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Premul);
+
+ base.OnAttachedToLogicalTree(e);
+ }
+
+ protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
+ {
+ base.OnDetachedFromLogicalTree(e);
+
+ _unpremulBitmap?.Dispose();
+ _unpremulBitmap = null;
+
+ _premulBitmap?.Dispose();
+ _unpremulBitmap = null;
+ }
+
+ public override void Render(DrawingContext context)
+ {
+ void FillPixels(WriteableBitmap bitmap, byte fillAlpha, bool premul)
+ {
+ using (var fb = bitmap.Lock())
+ {
+ var data = new int[fb.Size.Width * fb.Size.Height];
+
+ for (int y = 0; y < fb.Size.Height; y++)
+ {
+ for (int x = 0; x < fb.Size.Width; x++)
+ {
+ var color = new Color(fillAlpha, 0, 255, 0);
+
+ if (premul)
+ {
+ byte r = (byte) (color.R * color.A / 255);
+ byte g = (byte) (color.G * color.A / 255);
+ byte b = (byte) (color.B * color.A / 255);
+
+ color = new Color(fillAlpha, r, g, b);
+ }
+
+ data[y * fb.Size.Width + x] = (int) color.ToUint32();
+ }
+ }
+
+ Marshal.Copy(data, 0, fb.Address, fb.Size.Width * fb.Size.Height);
+ }
+ }
+
+ base.Render(context);
+
+ byte alpha = (byte)((_st.ElapsedMilliseconds / 10) % 256);
+
+ FillPixels(_unpremulBitmap, alpha, false);
+ FillPixels(_premulBitmap, alpha, true);
+
+ context.FillRectangle(Brushes.Red, new Rect(0, 0, 256 * 3, 256));
+
+ context.DrawImage(_unpremulBitmap,
+ new Rect(0, 0, 256, 256),
+ new Rect(0, 0, 256, 256));
+
+ context.DrawImage(_premulBitmap,
+ new Rect(0, 0, 256, 256),
+ new Rect(256, 0, 256, 256));
+
+ context.FillRectangle(new ImmutableSolidColorBrush(Colors.Lime, alpha / 255d), new Rect(512, 0, 256, 256));
+
+ Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
+ }
+ }
+}
diff --git a/samples/RenderDemo/SideBar.xaml b/samples/RenderDemo/SideBar.xaml
index d6bde0b146..fd23067f61 100644
--- a/samples/RenderDemo/SideBar.xaml
+++ b/samples/RenderDemo/SideBar.xaml
@@ -1,65 +1,68 @@
-
+
-
-
-
-
-
+
+
+
+
+
+
diff --git a/samples/interop/NativeEmbedSample/App.xaml b/samples/interop/NativeEmbedSample/App.xaml
new file mode 100644
index 0000000000..e35ade4087
--- /dev/null
+++ b/samples/interop/NativeEmbedSample/App.xaml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/samples/interop/NativeEmbedSample/App.xaml.cs b/samples/interop/NativeEmbedSample/App.xaml.cs
new file mode 100644
index 0000000000..cb17cfc35d
--- /dev/null
+++ b/samples/interop/NativeEmbedSample/App.xaml.cs
@@ -0,0 +1,22 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+
+namespace NativeEmbedSample
+{
+ public class App : Application
+ {
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
+ desktopLifetime.MainWindow = new MainWindow();
+
+ base.OnFrameworkInitializationCompleted();
+ }
+ }
+}
diff --git a/samples/interop/NativeEmbedSample/EmbedSample.cs b/samples/interop/NativeEmbedSample/EmbedSample.cs
new file mode 100644
index 0000000000..ab9df11e19
--- /dev/null
+++ b/samples/interop/NativeEmbedSample/EmbedSample.cs
@@ -0,0 +1,121 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+using Avalonia.Controls;
+using Avalonia.Platform;
+using Avalonia.Threading;
+using MonoMac.AppKit;
+using MonoMac.Foundation;
+using MonoMac.WebKit;
+using Encoding = SharpDX.Text.Encoding;
+
+namespace NativeEmbedSample
+{
+ public class EmbedSample : NativeControlHost
+ {
+ public bool IsSecond { get; set; }
+ private Process _mplayer;
+
+ IPlatformHandle CreateLinux(IPlatformHandle parent)
+ {
+ if (IsSecond)
+ {
+ var chooser = GtkHelper.CreateGtkFileChooser(parent.Handle);
+ if (chooser != null)
+ return chooser;
+ }
+
+ var control = base.CreateNativeControlCore(parent);
+ var nodes = Path.GetFullPath(Path.Combine(typeof(EmbedSample).Assembly.GetModules()[0].FullyQualifiedName,
+ "..",
+ "nodes.mp4"));
+ _mplayer = Process.Start(new ProcessStartInfo("mplayer",
+ $"-vo x11 -zoom -loop 0 -wid {control.Handle.ToInt64()} \"{nodes}\"")
+ {
+ UseShellExecute = false,
+
+ });
+ return control;
+ }
+
+ void DestroyLinux(IPlatformHandle handle)
+ {
+ _mplayer?.Kill();
+ _mplayer = null;
+ base.DestroyNativeControlCore(handle);
+ }
+
+ private const string RichText =
+ @"{\rtf1\ansi\ansicpg1251\deff0\nouicompat\deflang1049{\fonttbl{\f0\fnil\fcharset0 Calibri;}}
+{\colortbl ;\red255\green0\blue0;\red0\green77\blue187;\red0\green176\blue80;\red155\green0\blue211;\red247\green150\blue70;\red75\green172\blue198;}
+{\*\generator Riched20 6.3.9600}\viewkind4\uc1
+\pard\sa200\sl276\slmult1\f0\fs22\lang9 I \i am\i0 a \cf1\b Rich Text \cf0\b0\fs24 control\cf2\fs28 !\cf3\fs32 !\cf4\fs36 !\cf1\fs40 !\cf5\fs44 !\cf6\fs48 !\cf0\fs44\par
+}";
+
+ IPlatformHandle CreateWin32(IPlatformHandle parent)
+ {
+ WinApi.LoadLibrary("Msftedit.dll");
+ var handle = WinApi.CreateWindowEx(0, "RICHEDIT50W",
+ @"Rich Edit",
+ 0x800000 | 0x10000000 | 0x40000000 | 0x800000 | 0x10000 | 0x0004, 0, 0, 1, 1, parent.Handle,
+ IntPtr.Zero, WinApi.GetModuleHandle(null), IntPtr.Zero);
+ var st = new WinApi.SETTEXTEX { Codepage = 65001, Flags = 0x00000008 };
+ var text = RichText.Replace("", IsSecond ? "\\qr " : "");
+ var bytes = Encoding.UTF8.GetBytes(text);
+ WinApi.SendMessage(handle, 0x0400 + 97, ref st, bytes);
+ return new PlatformHandle(handle, "HWND");
+
+ }
+
+ void DestroyWin32(IPlatformHandle handle)
+ {
+ WinApi.DestroyWindow(handle.Handle);
+ }
+
+ IPlatformHandle CreateOSX(IPlatformHandle parent)
+ {
+ // Note: We are using MonoMac for example purposes
+ // It shouldn't be used in production apps
+ MacHelper.EnsureInitialized();
+
+ var webView = new WebView();
+ Dispatcher.UIThread.Post(() =>
+ {
+ webView.MainFrame.LoadRequest(new NSUrlRequest(new NSUrl(
+ IsSecond ? "https://bing.com": "https://google.com/")));
+ });
+ return new MacOSViewHandle(webView);
+
+ }
+
+ void DestroyOSX(IPlatformHandle handle)
+ {
+ ((MacOSViewHandle)handle).Dispose();
+ }
+
+ protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ return CreateLinux(parent);
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return CreateWin32(parent);
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ return CreateOSX(parent);
+ return base.CreateNativeControlCore(parent);
+ }
+
+ protected override void DestroyNativeControlCore(IPlatformHandle control)
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ DestroyLinux(control);
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ DestroyWin32(control);
+ else if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ DestroyOSX(control);
+ else
+ base.DestroyNativeControlCore(control);
+ }
+ }
+}
diff --git a/samples/interop/NativeEmbedSample/GtkHelper.cs b/samples/interop/NativeEmbedSample/GtkHelper.cs
new file mode 100644
index 0000000000..e389a51ef5
--- /dev/null
+++ b/samples/interop/NativeEmbedSample/GtkHelper.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Threading.Tasks;
+using Avalonia.Controls.Platform;
+using Avalonia.Platform;
+using Avalonia.Platform.Interop;
+using Avalonia.X11.NativeDialogs;
+using static Avalonia.X11.NativeDialogs.Gtk;
+using static Avalonia.X11.NativeDialogs.Glib;
+namespace NativeEmbedSample
+{
+ public class GtkHelper
+ {
+ private static Task s_gtkTask;
+ class FileChooser : INativeControlHostDestroyableControlHandle
+ {
+ private readonly IntPtr _widget;
+
+ public FileChooser(IntPtr widget, IntPtr xid)
+ {
+ _widget = widget;
+ Handle = xid;
+ }
+
+ public IntPtr Handle { get; }
+ public string HandleDescriptor => "XID";
+ public void Destroy()
+ {
+ RunOnGlibThread(() =>
+ {
+ gtk_widget_destroy(_widget);
+ return 0;
+ }).Wait();
+ }
+ }
+
+
+
+ public static IPlatformHandle CreateGtkFileChooser(IntPtr parentXid)
+ {
+ if (s_gtkTask == null)
+ s_gtkTask = StartGtk();
+ if (!s_gtkTask.Result)
+ return null;
+ return RunOnGlibThread(() =>
+ {
+ using (var title = new Utf8Buffer("Embedded"))
+ {
+ var widget = gtk_file_chooser_dialog_new(title, IntPtr.Zero, GtkFileChooserAction.SelectFolder,
+ IntPtr.Zero);
+ gtk_widget_realize(widget);
+ var xid = gdk_x11_window_get_xid(gtk_widget_get_window(widget));
+ gtk_window_present(widget);
+ return new FileChooser(widget, xid);
+ }
+ }).Result;
+ }
+ }
+}
diff --git a/samples/interop/NativeEmbedSample/MacHelper.cs b/samples/interop/NativeEmbedSample/MacHelper.cs
new file mode 100644
index 0000000000..74a06a0a0c
--- /dev/null
+++ b/samples/interop/NativeEmbedSample/MacHelper.cs
@@ -0,0 +1,39 @@
+using System;
+using Avalonia.Platform;
+using MonoMac.AppKit;
+
+namespace NativeEmbedSample
+{
+ public class MacHelper
+ {
+ private static bool _isInitialized;
+
+ public static void EnsureInitialized()
+ {
+ if (_isInitialized)
+ return;
+ _isInitialized = true;
+ NSApplication.Init();
+ }
+ }
+
+ class MacOSViewHandle : IPlatformHandle, IDisposable
+ {
+ private NSView _view;
+
+ public MacOSViewHandle(NSView view)
+ {
+ _view = view;
+ }
+
+ public IntPtr Handle => _view?.Handle ?? IntPtr.Zero;
+ public string HandleDescriptor => "NSView";
+
+ public void Dispose()
+ {
+ _view.Dispose();
+ _view = null;
+ }
+ }
+
+}
diff --git a/samples/interop/NativeEmbedSample/MainWindow.xaml b/samples/interop/NativeEmbedSample/MainWindow.xaml
new file mode 100644
index 0000000000..f2161a1bea
--- /dev/null
+++ b/samples/interop/NativeEmbedSample/MainWindow.xaml
@@ -0,0 +1,52 @@
+
+
+
+
+ Show popup (delay)
+ Show popup
+
+
+
+ Text
+
+
+ Tooltip
+
+
+
+
+
+
+
+
+ Visible
+
+
+
+
+
+
+
+ Visible
+
+
+
+
+
+
diff --git a/samples/interop/NativeEmbedSample/MainWindow.xaml.cs b/samples/interop/NativeEmbedSample/MainWindow.xaml.cs
new file mode 100644
index 0000000000..4324aa2762
--- /dev/null
+++ b/samples/interop/NativeEmbedSample/MainWindow.xaml.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+
+namespace NativeEmbedSample
+{
+ public class MainWindow : Window
+ {
+ public MainWindow()
+ {
+ AvaloniaXamlLoader.Load(this);
+ this.AttachDevTools();
+ }
+
+ 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 List
+ {
+ new MenuItem() { Header = "Test" }, new MenuItem() { Header = "Test" }
+ }
+ }.Open((Control)sender);
+ }
+ }
+}
diff --git a/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj b/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj
new file mode 100644
index 0000000000..cc831ef8ae
--- /dev/null
+++ b/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj
@@ -0,0 +1,31 @@
+
+
+
+ Exe
+ netcoreapp2.0
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+ Designer
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
diff --git a/samples/interop/NativeEmbedSample/Program.cs b/samples/interop/NativeEmbedSample/Program.cs
new file mode 100644
index 0000000000..baa7837667
--- /dev/null
+++ b/samples/interop/NativeEmbedSample/Program.cs
@@ -0,0 +1,17 @@
+using Avalonia;
+
+namespace NativeEmbedSample
+{
+ class Program
+ {
+ static int Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
+
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .With(new AvaloniaNativePlatformOptions()
+ {
+ })
+ .UsePlatformDetect();
+
+ }
+}
diff --git a/samples/interop/NativeEmbedSample/WinApi.cs b/samples/interop/NativeEmbedSample/WinApi.cs
new file mode 100644
index 0000000000..8e5bcdf49e
--- /dev/null
+++ b/samples/interop/NativeEmbedSample/WinApi.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace NativeEmbedSample
+{
+ public unsafe class WinApi
+ {
+ public enum CommonControls : uint
+ {
+ ICC_LISTVIEW_CLASSES = 0x00000001, // listview, header
+ ICC_TREEVIEW_CLASSES = 0x00000002, // treeview, tooltips
+ ICC_BAR_CLASSES = 0x00000004, // toolbar, statusbar, trackbar, tooltips
+ ICC_TAB_CLASSES = 0x00000008, // tab, tooltips
+ ICC_UPDOWN_CLASS = 0x00000010, // updown
+ ICC_PROGRESS_CLASS = 0x00000020, // progress
+ ICC_HOTKEY_CLASS = 0x00000040, // hotkey
+ ICC_ANIMATE_CLASS = 0x00000080, // animate
+ ICC_WIN95_CLASSES = 0x000000FF,
+ ICC_DATE_CLASSES = 0x00000100, // month picker, date picker, time picker, updown
+ ICC_USEREX_CLASSES = 0x00000200, // comboex
+ ICC_COOL_CLASSES = 0x00000400, // rebar (coolbar) control
+ ICC_INTERNET_CLASSES = 0x00000800,
+ ICC_PAGESCROLLER_CLASS = 0x00001000, // page scroller
+ ICC_NATIVEFNTCTL_CLASS = 0x00002000, // native font control
+ ICC_STANDARD_CLASSES = 0x00004000,
+ ICC_LINK_CLASS = 0x00008000
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct INITCOMMONCONTROLSEX
+ {
+ public int dwSize;
+ public uint dwICC;
+ }
+
+ [DllImport("Comctl32.dll")]
+ public static extern void InitCommonControlsEx(ref INITCOMMONCONTROLSEX init);
+
+ [DllImport("user32.dll", SetLastError = true)]
+ public static extern bool DestroyWindow(IntPtr hwnd);
+
+ [DllImport("kernel32.dll")]
+ public static extern IntPtr LoadLibrary(string lib);
+
+
+ [DllImport("kernel32.dll")]
+ public static extern IntPtr GetModuleHandle(string lpModuleName);
+
+ [DllImport("user32.dll", SetLastError = true)]
+ public static extern IntPtr CreateWindowEx(
+ int dwExStyle,
+ string lpClassName,
+ string lpWindowName,
+ uint dwStyle,
+ int x,
+ int y,
+ int nWidth,
+ int nHeight,
+ IntPtr hWndParent,
+ IntPtr hMenu,
+ IntPtr hInstance,
+ IntPtr lpParam);
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SETTEXTEX
+ {
+ public uint Flags;
+ public uint Codepage;
+ }
+
+ [DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "SendMessageW")]
+ public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, ref SETTEXTEX wParam, byte[] lParam);
+ }
+}
diff --git a/samples/interop/NativeEmbedSample/nodes-license.md b/samples/interop/NativeEmbedSample/nodes-license.md
new file mode 100644
index 0000000000..ab165bb9b6
--- /dev/null
+++ b/samples/interop/NativeEmbedSample/nodes-license.md
@@ -0,0 +1 @@
+nodes.mp4 by beeple is licensed under the creative commons license, downloaded from https://vimeo.com/9936271
diff --git a/samples/interop/NativeEmbedSample/nodes.mp4 b/samples/interop/NativeEmbedSample/nodes.mp4
new file mode 100644
index 0000000000..5afe23488a
Binary files /dev/null and b/samples/interop/NativeEmbedSample/nodes.mp4 differ
diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs
index c11cadfbac..2e6f4a67c3 100644
--- a/src/Android/Avalonia.Android/AndroidPlatform.cs
+++ b/src/Android/Avalonia.Android/AndroidPlatform.cs
@@ -67,7 +67,7 @@ namespace Avalonia.Android
throw new NotSupportedException();
}
- public IEmbeddableWindowImpl CreateEmbeddableWindow()
+ public IWindowImpl CreateEmbeddableWindow()
{
throw new NotSupportedException();
}
diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs
index fbe027db00..72732a1f95 100644
--- a/src/Android/Avalonia.Android/AvaloniaView.cs
+++ b/src/Android/Avalonia.Android/AvaloniaView.cs
@@ -33,10 +33,8 @@ namespace Avalonia.Android
return _view.View.DispatchKeyEvent(e);
}
- class ViewImpl : TopLevelImpl, IEmbeddableWindowImpl
+ class ViewImpl : TopLevelImpl
{
- public event Action LostFocus;
-
public ViewImpl(Context context) : base(context)
{
View.Focusable = true;
diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
index 71706676d6..71dce93ce7 100644
--- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
+++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
@@ -126,7 +126,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_view.Visibility = ViewStates.Visible;
}
- public double Scaling => 1;
+ public double RenderScaling => 1;
void Draw()
{
@@ -194,6 +194,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
}
public IPopupImpl CreatePopup() => null;
+
+ public Action LostFocus { get; set; }
ILockedFramebuffer IFramebufferPlatformSurface.Lock()=>new AndroidFramebuffer(_view.Holder.Surface);
}
diff --git a/src/Avalonia.Animation/Avalonia.Animation.csproj b/src/Avalonia.Animation/Avalonia.Animation.csproj
index 73c619942e..ee91acf45e 100644
--- a/src/Avalonia.Animation/Avalonia.Animation.csproj
+++ b/src/Avalonia.Animation/Avalonia.Animation.csproj
@@ -1,9 +1,10 @@
- netstandard2.0
+ netstandard2.0
+
diff --git a/src/Avalonia.Animation/Easing/Easing.cs b/src/Avalonia.Animation/Easing/Easing.cs
index 5b0dea6c60..e006459652 100644
--- a/src/Avalonia.Animation/Easing/Easing.cs
+++ b/src/Avalonia.Animation/Easing/Easing.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Globalization;
using System.Linq;
namespace Avalonia.Animation.Easings
@@ -25,6 +26,11 @@ namespace Avalonia.Animation.Easings
/// Returns the instance of the parsed type.
public static Easing Parse(string e)
{
+ if (e.Contains(','))
+ {
+ return new SplineEasing(KeySpline.Parse(e, CultureInfo.InvariantCulture));
+ }
+
if (_easingTypes == null)
{
_easingTypes = new Dictionary();
diff --git a/src/Avalonia.Animation/Easing/SplineEasing.cs b/src/Avalonia.Animation/Easing/SplineEasing.cs
new file mode 100644
index 0000000000..975fcc4746
--- /dev/null
+++ b/src/Avalonia.Animation/Easing/SplineEasing.cs
@@ -0,0 +1,85 @@
+namespace Avalonia.Animation.Easings
+{
+ ///
+ /// Eases a value
+ /// using a user-defined cubic bezier curve.
+ /// Good for custom easing functions that doesn't quite
+ /// fit with the built-in ones.
+ ///
+ public class SplineEasing : Easing
+ {
+ ///
+ /// X coordinate of the first control point
+ ///
+ public double X1
+ {
+ get => _internalKeySpline.ControlPointX1;
+ set
+ {
+ _internalKeySpline.ControlPointX1 = value;
+ }
+ }
+
+ ///
+ /// Y coordinate of the first control point
+ ///
+ public double Y1
+ {
+ get => _internalKeySpline.ControlPointY1;
+ set
+ {
+ _internalKeySpline.ControlPointY1 = value;
+ }
+ }
+
+ ///
+ /// X coordinate of the second control point
+ ///
+ public double X2
+ {
+ get => _internalKeySpline.ControlPointX2;
+ set
+ {
+ _internalKeySpline.ControlPointX2 = value;
+ }
+ }
+
+ ///
+ /// Y coordinate of the second control point
+ ///
+ public double Y2
+ {
+ get => _internalKeySpline.ControlPointY2;
+ set
+ {
+ _internalKeySpline.ControlPointY2 = value;
+ }
+ }
+
+ private readonly KeySpline _internalKeySpline;
+
+ public SplineEasing(double x1 = 0d, double y1 = 0d, double x2 = 1d, double y2 = 1d)
+ {
+ _internalKeySpline = new KeySpline();
+
+ this.X1 = x1;
+ this.Y1 = y1;
+ this.X2 = x2;
+ this.Y1 = y2;
+ }
+
+ public SplineEasing(KeySpline keySpline)
+ {
+ _internalKeySpline = keySpline;
+ }
+
+ public SplineEasing()
+ {
+ _internalKeySpline = new KeySpline();
+ }
+
+ ///
+ public override double Ease(double progress) =>
+ _internalKeySpline.GetSplineProgress(progress);
+ }
+}
diff --git a/src/Avalonia.Animation/KeySpline.cs b/src/Avalonia.Animation/KeySpline.cs
index 5a4f7a15a3..a6e9769186 100644
--- a/src/Avalonia.Animation/KeySpline.cs
+++ b/src/Avalonia.Animation/KeySpline.cs
@@ -81,7 +81,10 @@ namespace Avalonia.Animation
/// A with the appropriate values set
public static KeySpline Parse(string value, CultureInfo culture)
{
- using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid KeySpline."))
+ if (culture is null)
+ culture = CultureInfo.InvariantCulture;
+
+ using (var tokenizer = new StringTokenizer((string)value, culture, exceptionMessage: $"Invalid KeySpline string: \"{value}\"."))
{
return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble());
}
@@ -98,6 +101,7 @@ namespace Avalonia.Animation
if (IsValidXValue(value))
{
_controlPointX1 = value;
+ _isDirty = true;
}
else
{
@@ -112,7 +116,11 @@ namespace Avalonia.Animation
public double ControlPointY1
{
get => _controlPointY1;
- set => _controlPointY1 = value;
+ set
+ {
+ _controlPointY1 = value;
+ _isDirty = true;
+ }
}
///
@@ -126,6 +134,7 @@ namespace Avalonia.Animation
if (IsValidXValue(value))
{
_controlPointX2 = value;
+ _isDirty = true;
}
else
{
@@ -140,7 +149,11 @@ namespace Avalonia.Animation
public double ControlPointY2
{
get => _controlPointY2;
- set => _controlPointY2 = value;
+ set
+ {
+ _controlPointY2 = value;
+ _isDirty = true;
+ }
}
///
@@ -330,20 +343,4 @@ namespace Avalonia.Animation
}
}
}
-
- ///
- /// Converts string values to values
- ///
- public class KeySplineTypeConverter : TypeConverter
- {
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- return sourceType == typeof(string);
- }
-
- public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
- {
- return KeySpline.Parse((string)value, culture);
- }
- }
}
diff --git a/src/Avalonia.Animation/KeySplineTypeConverter.cs b/src/Avalonia.Animation/KeySplineTypeConverter.cs
new file mode 100644
index 0000000000..cd7427a37d
--- /dev/null
+++ b/src/Avalonia.Animation/KeySplineTypeConverter.cs
@@ -0,0 +1,25 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+
+// Ported from WPF open-source code.
+// https://github.com/dotnet/wpf/blob/ae1790531c3b993b56eba8b1f0dd395a3ed7de75/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Animation/KeySpline.cs
+
+namespace Avalonia.Animation
+{
+ ///
+ /// Converts string values to values
+ ///
+ public class KeySplineTypeConverter : TypeConverter
+ {
+ public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+ {
+ return sourceType == typeof(string);
+ }
+
+ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+ {
+ return KeySpline.Parse((string)value, culture);
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj
index cbcf7a7386..9bfd387ca9 100644
--- a/src/Avalonia.Base/Avalonia.Base.csproj
+++ b/src/Avalonia.Base/Avalonia.Base.csproj
@@ -3,7 +3,7 @@
netstandard2.0
Avalonia.Base
Avalonia
- True
+ True
@@ -13,4 +13,5 @@
+
diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs
index d18f0b3f94..65233f9230 100644
--- a/src/Avalonia.Base/AvaloniaObject.cs
+++ b/src/Avalonia.Base/AvaloniaObject.cs
@@ -806,7 +806,9 @@ namespace Avalonia
break;
}
- if (p.IsDataValidationEnabled)
+ var metadata = p.GetMetadata(GetType());
+
+ if (metadata.EnableDataValidation == true)
{
UpdateDataValidation(property, value);
}
diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs
index daa7191cc5..39391490b0 100644
--- a/src/Avalonia.Base/AvaloniaProperty.cs
+++ b/src/Avalonia.Base/AvaloniaProperty.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Reactive.Subjects;
using Avalonia.Data;
+using Avalonia.Data.Core;
using Avalonia.Utilities;
namespace Avalonia
@@ -9,7 +10,7 @@ namespace Avalonia
///
/// Base class for avalonia properties.
///
- public abstract class AvaloniaProperty : IEquatable
+ public abstract class AvaloniaProperty : IEquatable, IPropertyInfo
{
///
/// Represents an unset property value.
@@ -159,8 +160,6 @@ namespace Avalonia
///
internal int Id { get; }
- internal bool HasChangedSubscriptions => _changed?.HasObservers ?? false;
-
///
/// Provides access to a property's binding via the
/// indexer.
@@ -370,14 +369,14 @@ namespace Avalonia
var metadata = new DirectPropertyMetadata(
unsetValue: unsetValue,
- defaultBindingMode: defaultBindingMode);
+ defaultBindingMode: defaultBindingMode,
+ enableDataValidation: enableDataValidation);
var result = new DirectProperty(
name,
getter,
setter,
- metadata,
- enableDataValidation);
+ metadata);
AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result);
return result;
}
@@ -512,7 +511,7 @@ namespace Avalonia
///
/// An if setting the property can be undone, otherwise null.
///
- internal abstract IDisposable? RouteSetValue(
+ internal abstract IDisposable RouteSetValue(
IAvaloniaObject o,
object value,
BindingPriority priority);
@@ -584,6 +583,11 @@ namespace Avalonia
return _defaultMetadata;
}
+
+ bool IPropertyInfo.CanGet => true;
+ bool IPropertyInfo.CanSet => true;
+ object IPropertyInfo.Get(object target) => ((AvaloniaObject)target).GetValue(this);
+ void IPropertyInfo.Set(object target, object value) => ((AvaloniaObject)target).SetValue(this, value);
}
///
diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs
index 4a3b104f2a..4cde965400 100644
--- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs
+++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs
@@ -362,7 +362,7 @@ namespace Avalonia
/// The property.
///
/// You won't usually want to call this method directly, instead use the
- ///
+ ///
/// method.
///
public void Register(Type type, AvaloniaProperty property)
@@ -413,7 +413,7 @@ namespace Avalonia
/// The property.
///
/// You won't usually want to call this method directly, instead use the
- ///
+ ///
/// method.
///
public void RegisterAttached(Type type, AvaloniaProperty property)
diff --git a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs
index d915887e4c..b52829a60f 100644
--- a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs
+++ b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs
@@ -140,6 +140,7 @@ namespace Avalonia.Collections
}
}
+ [Obsolete("Causes memory leaks. Use DynamicData or similar instead.")]
public static IAvaloniaReadOnlyList CreateDerivedList(
this IAvaloniaReadOnlyList collection,
Func select)
diff --git a/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs b/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs
index 9bc3609dc5..7a233a62ab 100644
--- a/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs
+++ b/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs
@@ -13,9 +13,11 @@ namespace Avalonia.Collections.Pooled
public interface IReadOnlyPooledList : IReadOnlyList
{
+#pragma warning disable CS0419
///
/// Gets a for the items currently in the collection.
///
+#pragma warning restore CS0419
ReadOnlySpan Span { get; }
}
}
diff --git a/src/Avalonia.Base/Collections/Pooled/PooledList.cs b/src/Avalonia.Base/Collections/Pooled/PooledList.cs
index f0d6b292cc..e50e100d32 100644
--- a/src/Avalonia.Base/Collections/Pooled/PooledList.cs
+++ b/src/Avalonia.Base/Collections/Pooled/PooledList.cs
@@ -138,7 +138,6 @@ namespace Avalonia.Collections.Pooled
/// initially empty, but will have room for the given number of elements
/// before any reallocations are required.
///
- /// If true, Count of list equals capacity. Depending on ClearMode, rented items may or may not hold dirty values.
public PooledList(int capacity, ClearMode clearMode, ArrayPool customPool, bool sizeToCapacity)
{
if (capacity < 0)
@@ -499,11 +498,13 @@ namespace Avalonia.Collections.Pooled
public void AddRange(T[] array)
=> AddRange(array.AsSpan());
+#pragma warning disable CS0419
///
/// Adds the elements of the given to the end of this list. If
/// required, the capacity of the list is increased to twice the previous
/// capacity or the new size, whichever is larger.
///
+#pragma warning restore CS0419
public void AddRange(ReadOnlySpan span)
{
var newSpan = InsertSpan(_size, span.Length, false);
diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs
index 9aac1bacba..6e3c9ae67b 100644
--- a/src/Avalonia.Base/Data/BindingValue.cs
+++ b/src/Avalonia.Base/Data/BindingValue.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using Avalonia.Utilities;
#nullable enable
@@ -81,14 +82,14 @@ namespace Avalonia.Data
///
public readonly struct BindingValue
{
- private readonly T _value;
+ [AllowNull] private readonly T _value;
///
/// Initializes a new instance of the struct with a type of
///
///
/// The value.
- public BindingValue(T value)
+ public BindingValue([AllowNull] T value)
{
ValidateValue(value);
_value = value;
@@ -96,7 +97,7 @@ namespace Avalonia.Data
Error = null;
}
- private BindingValue(BindingValueType type, T value, Exception? error)
+ private BindingValue(BindingValueType type, [AllowNull] T value, Exception? error)
{
_value = value;
Type = type;
@@ -154,7 +155,7 @@ namespace Avalonia.Data
BindingValueType.UnsetValue => AvaloniaProperty.UnsetValue,
BindingValueType.DoNothing => BindingOperations.DoNothing,
BindingValueType.Value => _value,
- BindingValueType.BindingError =>
+ BindingValueType.BindingError =>
new BindingNotification(Error, BindingErrorType.Error),
BindingValueType.BindingErrorWithFallback =>
new BindingNotification(Error, BindingErrorType.Error, Value),
@@ -175,7 +176,7 @@ namespace Avalonia.Data
/// The binding type is or
/// .
///
- public BindingValue WithValue(T value)
+ public BindingValue WithValue([AllowNull] T value)
{
if (Type == BindingValueType.DoNothing)
{
@@ -190,6 +191,7 @@ namespace Avalonia.Data
/// Gets the value of the binding value if present, otherwise the default value.
///
/// The value.
+ [return: MaybeNull]
public T GetValueOrDefault() => HasValue ? _value : default;
///
@@ -206,6 +208,7 @@ namespace Avalonia.Data
/// The value if present and of the correct type, `default(TResult)` if the value is
/// not present or of an incorrect type.
///
+ [return: MaybeNull]
public TResult GetValueOrDefault()
{
return HasValue ?
@@ -222,7 +225,8 @@ namespace Avalonia.Data
/// present but not of the correct type or null, or if the
/// value is not present.
///
- public TResult GetValueOrDefault(TResult defaultValue)
+ [return: MaybeNull]
+ public TResult GetValueOrDefault([AllowNull] TResult defaultValue)
{
return HasValue ?
_value is TResult result ? result : default
@@ -242,7 +246,7 @@ namespace Avalonia.Data
UnsetValueType _ => Unset,
DoNothingType _ => DoNothing,
BindingNotification n => n.ToBindingValue().Cast(),
- _ => (T)value
+ _ => new BindingValue((T)value)
};
}
@@ -250,7 +254,7 @@ namespace Avalonia.Data
/// Creates a binding value from an instance of the underlying value type.
///
/// The value.
- public static implicit operator BindingValue(T value) => new BindingValue(value);
+ public static implicit operator BindingValue([AllowNull] T value) => new BindingValue(value);
///
/// Creates a binding value from an .
@@ -278,7 +282,7 @@ namespace Avalonia.Data
/// The binding error.
public static BindingValue BindingError(Exception e)
{
- e = e ?? throw new ArgumentNullException("e");
+ e = e ?? throw new ArgumentNullException(nameof(e));
return new BindingValue(BindingValueType.BindingError, default, e);
}
@@ -290,7 +294,7 @@ namespace Avalonia.Data
/// The fallback value.
public static BindingValue BindingError(Exception e, T fallbackValue)
{
- e = e ?? throw new ArgumentNullException("e");
+ e = e ?? throw new ArgumentNullException(nameof(e));
return new BindingValue(BindingValueType.BindingErrorWithFallback, fallbackValue, e);
}
@@ -303,7 +307,7 @@ namespace Avalonia.Data
/// The fallback value.
public static BindingValue BindingError(Exception e, Optional fallbackValue)
{
- e = e ?? throw new ArgumentNullException("e");
+ e = e ?? throw new ArgumentNullException(nameof(e));
return new BindingValue(
fallbackValue.HasValue ?
@@ -319,7 +323,7 @@ namespace Avalonia.Data
/// The data validation error.
public static BindingValue DataValidationError(Exception e)
{
- e = e ?? throw new ArgumentNullException("e");
+ e = e ?? throw new ArgumentNullException(nameof(e));
return new BindingValue(BindingValueType.DataValidationError, default, e);
}
@@ -331,7 +335,7 @@ namespace Avalonia.Data
/// The fallback value.
public static BindingValue DataValidationError(Exception e, T fallbackValue)
{
- e = e ?? throw new ArgumentNullException("e");
+ e = e ?? throw new ArgumentNullException(nameof(e));
return new BindingValue(BindingValueType.DataValidationErrorWithFallback, fallbackValue, e);
}
@@ -344,7 +348,7 @@ namespace Avalonia.Data
/// The fallback value.
public static BindingValue DataValidationError(Exception e, Optional fallbackValue)
{
- e = e ?? throw new ArgumentNullException("e");
+ e = e ?? throw new ArgumentNullException(nameof(e));
return new BindingValue(
fallbackValue.HasValue ?
@@ -354,7 +358,7 @@ namespace Avalonia.Data
e);
}
- private static void ValidateValue(T value)
+ private static void ValidateValue([AllowNull] T value)
{
if (value is UnsetValueType)
{
diff --git a/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs b/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs
new file mode 100644
index 0000000000..f66411c2c2
--- /dev/null
+++ b/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace Avalonia.Data.Core
+{
+ public class ClrPropertyInfo : IPropertyInfo
+ {
+ private readonly Func
-
-
-
-
- $(MSBuildThisFileDirectory)bin\$(Configuration)\$(TargetFramework)
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs
index 406abe6f99..ae2bf99d1e 100644
--- a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs
+++ b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs
@@ -107,7 +107,7 @@ namespace Avalonia.Build.Tasks
foreach (var s in sources.ToList())
{
- if (s.Path.ToLowerInvariant().EndsWith(".xaml") || s.Path.ToLowerInvariant().EndsWith(".paml"))
+ if (s.Path.ToLowerInvariant().EndsWith(".xaml") || s.Path.ToLowerInvariant().EndsWith(".paml") || s.Path.ToLowerInvariant().EndsWith(".axaml"))
{
XamlFileInfo info;
try
@@ -150,7 +150,7 @@ namespace Avalonia.Build.Tasks
BuildEngine.LogMessage($"GenerateAvaloniaResourcesTask -> Root: {Root}, {Resources?.Count()} resources, Output:{Output}", _reportImportance < MessageImportance.Low ? MessageImportance.High : _reportImportance);
- foreach (var r in EmbeddedResources.Where(r => r.ItemSpec.EndsWith(".xaml") || r.ItemSpec.EndsWith(".paml")))
+ foreach (var r in EmbeddedResources.Where(r => r.ItemSpec.EndsWith(".xaml") || r.ItemSpec.EndsWith(".paml") || r.ItemSpec.EndsWith(".axaml")))
BuildEngine.LogWarning(BuildEngineErrorCode.LegacyResmScheme, r.ItemSpec,
"XAML file is packed using legacy EmbeddedResource/resm scheme, relative URIs won't work");
var resources = BuildResourceSources();
diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs
index f94f10f792..5a2c74e16f 100644
--- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs
+++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs
@@ -5,7 +5,7 @@ using Avalonia.Utilities;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Collections.Generic;
-using XamlIl.TypeSystem;
+using XamlX.TypeSystem;
namespace Avalonia.Build.Tasks
{
diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
index 3b69109e68..c9c9c562bd 100644
--- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
+++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
@@ -7,17 +7,18 @@ using System.Text;
using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions;
using Microsoft.Build.Framework;
using Mono.Cecil;
-using XamlIl.TypeSystem;
using Avalonia.Utilities;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;
-using XamlIl;
-using XamlIl.Ast;
-using XamlIl.Parsers;
-using XamlIl.Transform;
+using XamlX;
+using XamlX.Ast;
+using XamlX.Parsers;
+using XamlX.Transform;
+using XamlX.TypeSystem;
using FieldAttributes = Mono.Cecil.FieldAttributes;
using MethodAttributes = Mono.Cecil.MethodAttributes;
using TypeAttributes = Mono.Cecil.TypeAttributes;
+using XamlX.IL;
namespace Avalonia.Build.Tasks
{
@@ -25,7 +26,8 @@ namespace Avalonia.Build.Tasks
public static partial class XamlCompilerTaskExecutor
{
static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml")
- || r.Name.ToLowerInvariant().EndsWith(".paml");
+ || r.Name.ToLowerInvariant().EndsWith(".paml")
+ || r.Name.ToLowerInvariant().EndsWith(".axaml");
public class CompileResult
{
@@ -49,23 +51,32 @@ namespace Avalonia.Build.Tasks
if (avares.Resources.Count(CheckXamlName) == 0 && emres.Resources.Count(CheckXamlName) == 0)
// Nothing to do
return new CompileResult(true);
-
- var xamlLanguage = AvaloniaXamlIlLanguage.Configure(typeSystem);
- var compilerConfig = new XamlIlTransformerConfiguration(typeSystem,
+
+ var clrPropertiesDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlHelpers",
+ TypeAttributes.Class, asm.MainModule.TypeSystem.Object);
+ asm.MainModule.Types.Add(clrPropertiesDef);
+ var indexerAccessorClosure = new TypeDefinition("CompiledAvaloniaXaml", "!IndexerAccessorFactoryClosure",
+ TypeAttributes.Class, asm.MainModule.TypeSystem.Object);
+ asm.MainModule.Types.Add(indexerAccessorClosure);
+
+ var (xamlLanguage , emitConfig) = AvaloniaXamlIlLanguage.Configure(typeSystem);
+ var compilerConfig = new AvaloniaXamlIlCompilerConfiguration(typeSystem,
typeSystem.TargetAssembly,
xamlLanguage,
- XamlIlXmlnsMappings.Resolve(typeSystem, xamlLanguage),
- AvaloniaXamlIlLanguage.CustomValueConverter);
+ XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage),
+ AvaloniaXamlIlLanguage.CustomValueConverter,
+ new XamlIlClrPropertyInfoEmitter(typeSystem.CreateTypeBuilder(clrPropertiesDef)),
+ new XamlIlPropertyInfoAccessorFactoryEmitter(typeSystem.CreateTypeBuilder(indexerAccessorClosure)));
var contextDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlContext",
TypeAttributes.Class, asm.MainModule.TypeSystem.Object);
asm.MainModule.Types.Add(contextDef);
- var contextClass = XamlIlContextDefinition.GenerateContextClass(typeSystem.CreateTypeBuilder(contextDef), typeSystem,
- xamlLanguage);
+ var contextClass = XamlILContextDefinition.GenerateContextClass(typeSystem.CreateTypeBuilder(contextDef), typeSystem,
+ xamlLanguage, emitConfig);
- var compiler = new AvaloniaXamlIlCompiler(compilerConfig, contextClass) { EnableIlVerification = verifyIl };
+ var compiler = new AvaloniaXamlIlCompiler(compilerConfig, emitConfig, contextClass) { EnableIlVerification = verifyIl };
var editorBrowsableAttribute = typeSystem
.GetTypeReference(typeSystem.FindType("System.ComponentModel.EditorBrowsableAttribute"))
@@ -117,7 +128,7 @@ namespace Avalonia.Build.Tasks
asm.MainModule.Types.Add(typeDef);
var builder = typeSystem.CreateTypeBuilder(typeDef);
- foreach (var res in group.Resources.Where(CheckXamlName))
+ foreach (var res in group.Resources.Where(CheckXamlName).OrderBy(x=>x.FilePath.ToLowerInvariant()))
{
try
{
@@ -125,35 +136,35 @@ namespace Avalonia.Build.Tasks
// StreamReader is needed here to handle BOM
var xaml = new StreamReader(new MemoryStream(res.FileContents)).ReadToEnd();
- var parsed = XDocumentXamlIlParser.Parse(xaml);
+ var parsed = XDocumentXamlParser.Parse(xaml);
- var initialRoot = (XamlIlAstObjectNode)parsed.Root;
+ var initialRoot = (XamlAstObjectNode)parsed.Root;
- var precompileDirective = initialRoot.Children.OfType()
+ var precompileDirective = initialRoot.Children.OfType()
.FirstOrDefault(d => d.Namespace == XamlNamespaces.Xaml2006 && d.Name == "Precompile");
if (precompileDirective != null)
{
- var precompileText = (precompileDirective.Values[0] as XamlIlAstTextNode)?.Text.Trim()
+ var precompileText = (precompileDirective.Values[0] as XamlAstTextNode)?.Text.Trim()
.ToLowerInvariant();
if (precompileText == "false")
continue;
if (precompileText != "true")
- throw new XamlIlParseException("Invalid value for x:Precompile", precompileDirective);
+ throw new XamlParseException("Invalid value for x:Precompile", precompileDirective);
}
- var classDirective = initialRoot.Children.OfType()
+ var classDirective = initialRoot.Children.OfType()
.FirstOrDefault(d => d.Namespace == XamlNamespaces.Xaml2006 && d.Name == "Class");
- IXamlIlType classType = null;
+ IXamlType classType = null;
if (classDirective != null)
{
- if (classDirective.Values.Count != 1 || !(classDirective.Values[0] is XamlIlAstTextNode tn))
- throw new XamlIlParseException("x:Class should have a string value", classDirective);
+ if (classDirective.Values.Count != 1 || !(classDirective.Values[0] is XamlAstTextNode tn))
+ throw new XamlParseException("x:Class should have a string value", classDirective);
classType = typeSystem.TargetAssembly.FindType(tn.Text);
if (classType == null)
- throw new XamlIlParseException($"Unable to find type `{tn.Text}`", classDirective);
+ throw new XamlParseException($"Unable to find type `{tn.Text}`", classDirective);
compiler.OverrideRootType(parsed,
- new XamlIlAstClrTypeReference(classDirective, classType, false));
+ new XamlAstClrTypeReference(classDirective, classType, false));
initialRoot.Children.Remove(classDirective);
}
@@ -322,7 +333,7 @@ namespace Avalonia.Build.Tasks
catch (Exception e)
{
int lineNumber = 0, linePosition = 0;
- if (e is XamlIlParseException xe)
+ if (e is XamlParseException xe)
{
lineNumber = xe.LineNumber;
linePosition = xe.LinePosition;
diff --git a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj
index 27853a1540..6975642db2 100644
--- a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj
+++ b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj
@@ -22,4 +22,5 @@
+
diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs
index 7e893d131e..7e921944ea 100644
--- a/src/Avalonia.Controls.DataGrid/DataGrid.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs
@@ -3920,6 +3920,14 @@ namespace Avalonia.Controls
dataGridColumn: CurrentColumn,
dataGridRow: EditingRow,
dataGridCell: editingCell);
+
+ EditingRow.InvalidateDesiredHeight();
+ var column = editingCell.OwningColumn;
+ if (column.Width.IsSizeToCells || column.Width.IsAuto)
+ {// Invalidate desired width and force recalculation
+ column.SetWidthDesiredValue(0);
+ EditingRow.OwningGrid.AutoSizeColumn(column, editingCell.DesiredSize.Width);
+ }
}
// We're done, so raise the CellEditEnded event
diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs
index 830eff1102..d5115c983a 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs
@@ -767,6 +767,11 @@ namespace Avalonia.Controls
}
}
+ internal void InvalidateDesiredHeight()
+ {
+ _cellsElement?.InvalidateDesiredHeight();
+ }
+
internal void ResetGridLine()
{
_bottomGridLine = null;
diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs
index 6e0703c90f..c5fe9f0cb2 100644
--- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs
+++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs
@@ -299,6 +299,11 @@ namespace Avalonia.Controls.Primitives
DesiredHeight = 0;
}
+ internal void InvalidateDesiredHeight()
+ {
+ DesiredHeight = 0;
+ }
+
private bool ShouldDisplayCell(DataGridColumn column, double frozenLeftEdge, double scrollingLeftEdge)
{
if (!column.IsVisible)
diff --git a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml
index 17e7ecba43..1d4d88825f 100644
--- a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml
+++ b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml
@@ -28,7 +28,7 @@
-
+
@@ -196,9 +196,9 @@
-
-
-
+
+
+
diff --git a/src/Avalonia.Controls/AcrylicPlatformCompensationLevels.cs b/src/Avalonia.Controls/AcrylicPlatformCompensationLevels.cs
new file mode 100644
index 0000000000..e1f1c4029e
--- /dev/null
+++ b/src/Avalonia.Controls/AcrylicPlatformCompensationLevels.cs
@@ -0,0 +1,23 @@
+namespace Avalonia.Controls
+{
+ ///
+ /// Defines compensation levels for the platform depending on the transparency level.
+ /// It controls the base opacity level of the 'tracing paper' layer that compensates
+ /// for low blur radius.
+ ///
+ public struct AcrylicPlatformCompensationLevels
+ {
+ public AcrylicPlatformCompensationLevels(double transparent, double blurred, double acrylic)
+ {
+ TransparentLevel = transparent;
+ BlurLevel = blurred;
+ AcrylicBlurLevel = acrylic;
+ }
+
+ public double TransparentLevel { get; }
+
+ public double BlurLevel { get; }
+
+ public double AcrylicBlurLevel { get; }
+ }
+}
diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs
index 31101dc0f1..c164f282e8 100644
--- a/src/Avalonia.Controls/AutoCompleteBox.cs
+++ b/src/Avalonia.Controls/AutoCompleteBox.cs
@@ -1647,7 +1647,7 @@ namespace Avalonia.Controls
///
/// The source object.
/// The event data.
- private void DropDownPopup_Closed(object sender, PopupClosedEventArgs e)
+ private void DropDownPopup_Closed(object sender, EventArgs e)
{
// Force the drop down dependency property to be false.
if (IsDropDownOpen)
@@ -1655,11 +1655,6 @@ namespace Avalonia.Controls
IsDropDownOpen = false;
}
- if (e.CloseEvent is PointerEventArgs pointerEvent)
- {
- pointerEvent.Handled = true;
- }
-
// Fire the DropDownClosed event
if (_popupHasOpened)
{
diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj
index 32331d29ab..480dcfcb85 100644
--- a/src/Avalonia.Controls/Avalonia.Controls.csproj
+++ b/src/Avalonia.Controls/Avalonia.Controls.csproj
@@ -1,6 +1,6 @@
- netstandard2.0
+ netstandard2.0
@@ -14,4 +14,5 @@
+
diff --git a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs
index b987f065be..046b55d49a 100644
--- a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs
+++ b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs
@@ -889,17 +889,12 @@ namespace Avalonia.Controls
_ignoreButtonClick = false;
}
}
- private void PopUp_Closed(object sender, PopupClosedEventArgs e)
+ private void PopUp_Closed(object sender, EventArgs e)
{
IsDropDownOpen = false;
if(!_isPopupClosing)
{
- if (e.CloseEvent is PointerEventArgs pointerEvent)
- {
- pointerEvent.Handled = true;
- }
-
_isPopupClosing = true;
Threading.Dispatcher.UIThread.InvokeAsync(() => _isPopupClosing = false);
}
diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs
new file mode 100644
index 0000000000..a86cbc271b
--- /dev/null
+++ b/src/Avalonia.Controls/Chrome/CaptionButtons.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Reactive.Disposables;
+using Avalonia.Controls.Primitives;
+
+#nullable enable
+
+namespace Avalonia.Controls.Chrome
+{
+ ///
+ /// Draws window minimize / maximize / close buttons in a when managed client decorations are enabled.
+ ///
+ public class CaptionButtons : TemplatedControl
+ {
+ private CompositeDisposable? _disposables;
+ private Window? _hostWindow;
+
+ public void Attach(Window hostWindow)
+ {
+ if (_disposables == null)
+ {
+ _hostWindow = hostWindow;
+
+ _disposables = new CompositeDisposable
+ {
+ _hostWindow.GetObservable(Window.WindowStateProperty)
+ .Subscribe(x =>
+ {
+ PseudoClasses.Set(":minimized", x == WindowState.Minimized);
+ PseudoClasses.Set(":normal", x == WindowState.Normal);
+ PseudoClasses.Set(":maximized", x == WindowState.Maximized);
+ PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen);
+ })
+ };
+ }
+ }
+
+ public void Detach()
+ {
+ if (_disposables != null)
+ {
+ _disposables.Dispose();
+ _disposables = null;
+
+ _hostWindow = null;
+ }
+ }
+
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ base.OnApplyTemplate(e);
+
+ var closeButton = e.NameScope.Get("PART_CloseButton");
+ var restoreButton = e.NameScope.Get("PART_RestoreButton");
+ var minimiseButton = e.NameScope.Get("PART_MinimiseButton");
+ var fullScreenButton = e.NameScope.Get("PART_FullScreenButton");
+
+ closeButton.PointerReleased += (sender, e) => _hostWindow?.Close();
+
+ restoreButton.PointerReleased += (sender, e) =>
+ {
+ if (_hostWindow != null)
+ {
+ _hostWindow.WindowState = _hostWindow.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
+ }
+ };
+
+ minimiseButton.PointerReleased += (sender, e) =>
+ {
+ if (_hostWindow != null)
+ {
+ _hostWindow.WindowState = WindowState.Minimized;
+ }
+ };
+
+ fullScreenButton.PointerReleased += (sender, e) =>
+ {
+ if (_hostWindow != null)
+ {
+ _hostWindow.WindowState = _hostWindow.WindowState == WindowState.FullScreen ? WindowState.Normal : WindowState.FullScreen;
+ }
+ };
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs
new file mode 100644
index 0000000000..c0c8076dd8
--- /dev/null
+++ b/src/Avalonia.Controls/Chrome/TitleBar.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Reactive.Disposables;
+using Avalonia.Controls.Primitives;
+
+#nullable enable
+
+namespace Avalonia.Controls.Chrome
+{
+ ///
+ /// Draws a titlebar when managed client decorations are enabled.
+ ///
+ public class TitleBar : TemplatedControl
+ {
+ private CompositeDisposable? _disposables;
+ private CaptionButtons? _captionButtons;
+
+ private void UpdateSize(Window window)
+ {
+ if (window != null)
+ {
+ Margin = new Thickness(
+ window.OffScreenMargin.Left,
+ window.OffScreenMargin.Top,
+ window.OffScreenMargin.Right,
+ window.OffScreenMargin.Bottom);
+
+ if (window.WindowState != WindowState.FullScreen)
+ {
+ Height = window.WindowDecorationMargin.Top;
+
+ if (_captionButtons != null)
+ {
+ _captionButtons.Height = Height;
+ }
+ }
+
+ IsVisible = window.PlatformImpl.NeedsManagedDecorations;
+ }
+ }
+
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ base.OnApplyTemplate(e);
+
+ _captionButtons?.Detach();
+
+ _captionButtons = e.NameScope.Get("PART_CaptionButtons");
+
+ if (VisualRoot is Window window)
+ {
+ _captionButtons?.Attach(window);
+
+ UpdateSize(window);
+ }
+ }
+
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+
+ if (VisualRoot is Window window)
+ {
+ _disposables = new CompositeDisposable
+ {
+ window.GetObservable(Window.WindowDecorationMarginProperty)
+ .Subscribe(x => UpdateSize(window)),
+ window.GetObservable(Window.ExtendClientAreaTitleBarHeightHintProperty)
+ .Subscribe(x => UpdateSize(window)),
+ window.GetObservable(Window.OffScreenMarginProperty)
+ .Subscribe(x => UpdateSize(window)),
+ window.GetObservable(Window.ExtendClientAreaChromeHintsProperty)
+ .Subscribe(x => UpdateSize(window)),
+ window.GetObservable(Window.WindowStateProperty)
+ .Subscribe(x =>
+ {
+ PseudoClasses.Set(":minimized", x == WindowState.Minimized);
+ PseudoClasses.Set(":normal", x == WindowState.Normal);
+ PseudoClasses.Set(":maximized", x == WindowState.Maximized);
+ PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen);
+ }),
+ window.GetObservable(Window.IsExtendedIntoWindowDecorationsProperty)
+ .Subscribe(x => UpdateSize(window))
+ };
+ }
+ }
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnDetachedFromVisualTree(e);
+
+ _disposables?.Dispose();
+
+ _captionButtons?.Detach();
+ _captionButtons = null;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs
index 6557346b1f..27313b0b4c 100644
--- a/src/Avalonia.Controls/ComboBox.cs
+++ b/src/Avalonia.Controls/ComboBox.cs
@@ -7,6 +7,7 @@ using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
+using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.VisualTree;
@@ -63,6 +64,18 @@ namespace Avalonia.Controls
public static readonly StyledProperty PlaceholderForegroundProperty =
AvaloniaProperty.Register(nameof(PlaceholderForeground));
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty HorizontalContentAlignmentProperty =
+ ContentControl.HorizontalContentAlignmentProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty VerticalContentAlignmentProperty =
+ ContentControl.VerticalContentAlignmentProperty.AddOwner();
+
private bool _isDropDownOpen;
private Popup _popup;
private object _selectionBoxItem;
@@ -133,6 +146,24 @@ namespace Avalonia.Controls
set { SetValue(VirtualizationModeProperty, value); }
}
+ ///
+ /// Gets or sets the horizontal alignment of the content within the control.
+ ///
+ public HorizontalAlignment HorizontalContentAlignment
+ {
+ get { return GetValue(HorizontalContentAlignmentProperty); }
+ set { SetValue(HorizontalContentAlignmentProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the vertical alignment of the content within the control.
+ ///
+ public VerticalAlignment VerticalContentAlignment
+ {
+ get { return GetValue(VerticalContentAlignmentProperty); }
+ set { SetValue(VerticalContentAlignmentProperty, value); }
+ }
+
///
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
@@ -259,24 +290,6 @@ namespace Avalonia.Controls
_popup = e.NameScope.Get("PART_Popup");
_popup.Opened += PopupOpened;
- _popup.Closed += PopupClosed;
- }
-
- ///
- /// Called when the ComboBox popup is closed, with the
- /// that caused the popup to close.
- ///
- /// The event args.
- ///
- /// This method can be overridden to control whether the event that caused the popup to close
- /// is swallowed or passed through.
- ///
- protected virtual void PopupClosedOverride(PopupClosedEventArgs e)
- {
- if (e.CloseEvent is PointerEventArgs pointerEvent)
- {
- pointerEvent.Handled = true;
- }
}
internal void ItemFocused(ComboBoxItem dropDownItem)
@@ -287,13 +300,11 @@ namespace Avalonia.Controls
}
}
- private void PopupClosed(object sender, PopupClosedEventArgs e)
+ private void PopupClosed(object sender, EventArgs e)
{
_subscriptionsOnOpen?.Dispose();
_subscriptionsOnOpen = null;
- PopupClosedOverride(e);
-
if (CanFocus(this))
{
Focus();
diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs
index 5929dd39d4..9bb4cc4816 100644
--- a/src/Avalonia.Controls/ContextMenu.cs
+++ b/src/Avalonia.Controls/ContextMenu.cs
@@ -265,7 +265,8 @@ namespace Avalonia.Controls
PlacementMode = PlacementMode,
PlacementRect = PlacementRect,
PlacementTarget = PlacementTarget ?? control,
- StaysOpen = false
+ IsLightDismissEnabled = true,
+ OverlayDismissEventPassThrough = true,
};
_popup.Opened += PopupOpened;
@@ -279,9 +280,8 @@ namespace Avalonia.Controls
}
_popup.Child = this;
- _popup.IsOpen = true;
-
IsOpen = true;
+ _popup.IsOpen = true;
RaiseEvent(new RoutedEventArgs
{
diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs
new file mode 100644
index 0000000000..a41c159980
--- /dev/null
+++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs
@@ -0,0 +1,416 @@
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Shapes;
+using Avalonia.Controls.Templates;
+using Avalonia.Interactivity;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// A control to allow the user to select a date
+ ///
+ public class DatePicker : TemplatedControl
+ {
+ ///
+ /// Define the Property
+ ///
+ public static readonly DirectProperty DayFormatProperty =
+ AvaloniaProperty.RegisterDirect(nameof(DayFormat),
+ x => x.DayFormat, (x, v) => x.DayFormat = v);
+
+ ///
+ /// Defines the Property
+ ///
+ public static readonly DirectProperty DayVisibleProperty =
+ AvaloniaProperty.RegisterDirect(nameof(DayVisible),
+ x => x.DayVisible, (x, v) => x.DayVisible = v);
+
+ ///
+ /// Defines the Property
+ ///
+ public static readonly StyledProperty HeaderProperty =
+ AvaloniaProperty.Register(nameof(Header));
+
+ ///
+ /// Defines the Property
+ ///
+ public static readonly StyledProperty HeaderTemplateProperty =
+ AvaloniaProperty.Register(nameof(HeaderTemplate));
+
+ ///
+ /// Defines the Property
+ ///
+ public static readonly DirectProperty MaxYearProperty =
+ AvaloniaProperty.RegisterDirect(nameof(MaxYear),
+ x => x.MaxYear, (x, v) => x.MaxYear = v);
+
+ ///
+ /// Defines the Property
+ ///
+ public static readonly DirectProperty MinYearProperty =
+ AvaloniaProperty.RegisterDirect(nameof(MinYear),
+ x => x.MinYear, (x, v) => x.MinYear = v);
+
+ ///
+ /// Defines the Property
+ ///
+ public static readonly DirectProperty MonthFormatProperty =
+ AvaloniaProperty.RegisterDirect(nameof(MonthFormat),
+ x => x.MonthFormat, (x, v) => x.MonthFormat = v);
+
+ ///
+ /// Defines the Property
+ ///
+ public static readonly DirectProperty MonthVisibleProperty =
+ AvaloniaProperty.RegisterDirect(nameof(MonthVisible),
+ x => x.MonthVisible, (x, v) => x.MonthVisible = v);
+
+ ///
+ /// Defiens the Property
+ ///
+ public static readonly DirectProperty YearFormatProperty =
+ AvaloniaProperty.RegisterDirect(nameof(YearFormat),
+ x => x.YearFormat, (x, v) => x.YearFormat = v);
+
+ ///
+ /// Defines the Property
+ ///
+ public static readonly DirectProperty YearVisibleProperty =
+ AvaloniaProperty.RegisterDirect(nameof(YearVisible),
+ x => x.YearVisible, (x, v) => x.YearVisible = v);
+
+ ///
+ /// Defines the Property
+ ///
+ public static readonly DirectProperty SelectedDateProperty =
+ AvaloniaProperty.RegisterDirect(nameof(SelectedDate),
+ x => x.SelectedDate, (x, v) => x.SelectedDate = v);
+
+ // Template Items
+ private Button _flyoutButton;
+ private TextBlock _dayText;
+ private TextBlock _monthText;
+ private TextBlock _yearText;
+ private Grid _container;
+ private Rectangle _spacer1;
+ private Rectangle _spacer2;
+ private Popup _popup;
+ private DatePickerPresenter _presenter;
+
+ private bool _areControlsAvailable;
+
+ private string _dayFormat = "%d";
+ private bool _dayVisible = true;
+ private DateTimeOffset _maxYear;
+ private DateTimeOffset _minYear;
+ private string _monthFormat = "MMMM";
+ private bool _monthVisible = true;
+ private string _yearFormat = "yyyy";
+ private bool _yearVisible = true;
+ private DateTimeOffset? _selectedDate;
+
+ public DatePicker()
+ {
+ PseudoClasses.Set(":hasnodate", true);
+ var now = DateTimeOffset.Now;
+ _minYear = new DateTimeOffset(now.Date.Year - 100, 1, 1, 0, 0, 0, now.Offset);
+ _maxYear = new DateTimeOffset(now.Date.Year + 100, 12, 31, 0, 0, 0, now.Offset);
+ }
+
+ public string DayFormat
+ {
+ get => _dayFormat;
+ set => SetAndRaise(DayFormatProperty, ref _dayFormat, value);
+ }
+
+ ///
+ /// Gets or sets whether the day is visible
+ ///
+ public bool DayVisible
+ {
+ get => _dayVisible;
+ set
+ {
+ SetAndRaise(DayVisibleProperty, ref _dayVisible, value);
+ SetGrid();
+ }
+ }
+
+ ///
+ /// Gets or sets the DatePicker header
+ ///
+ public object Header
+ {
+ get => GetValue(HeaderProperty);
+ set => SetValue(HeaderProperty, value);
+ }
+
+ ///
+ /// Gets or sets the header template
+ ///
+ public IDataTemplate HeaderTemplate
+ {
+ get => GetValue(HeaderTemplateProperty);
+ set => SetValue(HeaderTemplateProperty, value);
+ }
+
+ ///
+ /// Gets or sets the maximum year for the picker
+ ///
+ public DateTimeOffset MaxYear
+ {
+ get => _maxYear;
+ set
+ {
+ if (value < MinYear)
+ throw new InvalidOperationException("MaxDate cannot be less than MinDate");
+ SetAndRaise(MaxYearProperty, ref _maxYear, value);
+
+ if (SelectedDate.HasValue && SelectedDate.Value > value)
+ SelectedDate = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the minimum year for the picker
+ ///
+ public DateTimeOffset MinYear
+ {
+ get => _minYear;
+ set
+ {
+ if (value > MaxYear)
+ throw new InvalidOperationException("MinDate cannot be greater than MaxDate");
+ SetAndRaise(MinYearProperty, ref _minYear, value);
+
+ if (SelectedDate.HasValue && SelectedDate.Value < value)
+ SelectedDate = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the month format
+ ///
+ public string MonthFormat
+ {
+ get => _monthFormat;
+ set => SetAndRaise(MonthFormatProperty, ref _monthFormat, value);
+ }
+
+ ///
+ /// Gets or sets whether the month is visible
+ ///
+ public bool MonthVisible
+ {
+ get => _monthVisible;
+ set
+ {
+ SetAndRaise(MonthVisibleProperty, ref _monthVisible, value);
+ SetGrid();
+ }
+ }
+
+ ///
+ /// Gets or sets the year format
+ ///
+ public string YearFormat
+ {
+ get => _yearFormat;
+ set => SetAndRaise(YearFormatProperty, ref _yearFormat, value);
+ }
+
+ ///
+ /// Gets or sets whether the year is visible
+ ///
+ public bool YearVisible
+ {
+ get => _yearVisible;
+ set
+ {
+ SetAndRaise(YearVisibleProperty, ref _yearVisible, value);
+ SetGrid();
+ }
+ }
+
+ ///
+ /// Gets or sets the Selected Date for the picker, can be null
+ ///
+ public DateTimeOffset? SelectedDate
+ {
+ get => _selectedDate;
+ set
+ {
+ var old = _selectedDate;
+ SetAndRaise(SelectedDateProperty, ref _selectedDate, value);
+ SetSelectedDateText();
+ OnSelectedDateChanged(this, new DatePickerSelectedValueChangedEventArgs(old, value));
+ }
+ }
+
+ ///
+ /// Raised when the changes
+ ///
+ public event EventHandler SelectedDateChanged;
+
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ _areControlsAvailable = false;
+ if (_flyoutButton != null)
+ _flyoutButton.Click -= OnFlyoutButtonClicked;
+ if (_presenter != null)
+ {
+ _presenter.Confirmed -= OnConfirmed;
+ _presenter.Dismissed -= OnDismissPicker;
+ }
+
+ base.OnApplyTemplate(e);
+ _flyoutButton = e.NameScope.Find("FlyoutButton");
+ _dayText = e.NameScope.Find("DayText");
+ _monthText = e.NameScope.Find("MonthText");
+ _yearText = e.NameScope.Find("YearText");
+ _container = e.NameScope.Find("ButtonContentGrid");
+ _spacer1 = e.NameScope.Find("FirstSpacer");
+ _spacer2 = e.NameScope.Find("SecondSpacer");
+ _popup = e.NameScope.Find("Popup");
+ _presenter = e.NameScope.Find("PickerPresenter");
+
+ _areControlsAvailable = true;
+
+ SetGrid();
+ SetSelectedDateText();
+
+ if (_flyoutButton != null)
+ _flyoutButton.Click += OnFlyoutButtonClicked;
+
+ if (_presenter != null)
+ {
+ _presenter.Confirmed += OnConfirmed;
+ _presenter.Dismissed += OnDismissPicker;
+
+ _presenter[!DatePickerPresenter.MaxYearProperty] = this[!MaxYearProperty];
+ _presenter[!DatePickerPresenter.MinYearProperty] = this[!MinYearProperty];
+
+ _presenter[!DatePickerPresenter.MonthVisibleProperty] = this[!MonthVisibleProperty];
+ _presenter[!DatePickerPresenter.MonthFormatProperty] = this[!MonthFormatProperty];
+
+ _presenter[!DatePickerPresenter.DayVisibleProperty] = this[!DayVisibleProperty];
+ _presenter[!DatePickerPresenter.DayFormatProperty] = this[!DayFormatProperty];
+
+ _presenter[!DatePickerPresenter.YearVisibleProperty] = this[!YearVisibleProperty];
+ _presenter[!DatePickerPresenter.YearFormatProperty] = this[!YearFormatProperty];
+ }
+ }
+
+ private void OnDismissPicker(object sender, EventArgs e)
+ {
+ _popup.Close();
+ Focus();
+ }
+
+ private void OnConfirmed(object sender, EventArgs e)
+ {
+ _popup.Close();
+ SelectedDate = _presenter.Date;
+ }
+
+ private void SetGrid()
+ {
+ if (_container == null)
+ return;
+
+ var fmt = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
+ var columns = new List<(TextBlock, int)>
+ {
+ (_monthText, MonthVisible ? fmt.IndexOf("m", StringComparison.OrdinalIgnoreCase) : -1),
+ (_yearText, YearVisible ? fmt.IndexOf("y", StringComparison.OrdinalIgnoreCase) : -1),
+ (_dayText, DayVisible ? fmt.IndexOf("d", StringComparison.OrdinalIgnoreCase) : -1),
+ };
+
+ columns.Sort((x, y) => x.Item2 - y.Item2);
+ _container.ColumnDefinitions.Clear();
+
+ var columnIndex = 0;
+
+ foreach (var column in columns)
+ {
+ if (column.Item1 is null)
+ continue;
+
+ column.Item1.IsVisible = column.Item2 != -1;
+
+ if (column.Item2 != -1)
+ {
+ if (columnIndex > 0)
+ {
+ _container.ColumnDefinitions.Add(new ColumnDefinition(0, GridUnitType.Auto));
+ }
+
+ _container.ColumnDefinitions.Add(
+ new ColumnDefinition(column.Item1 == _monthText ? 138 : 78, GridUnitType.Star));
+
+ if (column.Item1.Parent is null)
+ {
+ _container.Children.Add(column.Item1);
+ }
+
+ Grid.SetColumn(column.Item1, (columnIndex++ * 2));
+ }
+ }
+
+ var isSpacer1Visible = columnIndex > 1;
+ var isSpacer2Visible = columnIndex > 2;
+ // ternary conditional operator is used to make sure grid cells will be validated
+ Grid.SetColumn(_spacer1, isSpacer1Visible ? 1 : 0);
+ Grid.SetColumn(_spacer2, isSpacer2Visible ? 3 : 0);
+
+ _spacer1.IsVisible = isSpacer1Visible;
+ _spacer2.IsVisible = isSpacer2Visible;
+ }
+
+ private void SetSelectedDateText()
+ {
+ if (!_areControlsAvailable)
+ return;
+
+ if (SelectedDate.HasValue)
+ {
+ PseudoClasses.Set(":hasnodate", false);
+ var selDate = SelectedDate.Value;
+ _monthText.Text = selDate.ToString(MonthFormat);
+ _yearText.Text = selDate.ToString(YearFormat);
+ _dayText.Text = selDate.ToString(DayFormat);
+ }
+ else
+ {
+ PseudoClasses.Set(":hasnodate", true);
+ _monthText.Text = "month";
+ _yearText.Text = "year";
+ _dayText.Text = "day";
+ }
+ }
+
+ private void OnFlyoutButtonClicked(object sender, RoutedEventArgs e)
+ {
+ if (_presenter == null)
+ throw new InvalidOperationException("No DatePickerPresenter found");
+
+ _presenter.Date = SelectedDate ?? DateTimeOffset.Now;
+
+ _popup.IsOpen = true;
+
+ var deltaY = _presenter.GetOffsetForPopup();
+
+ // The extra 5 px I think is related to default popup placement behavior
+ _popup.Host.ConfigurePosition(_popup.PlacementTarget, PlacementMode.AnchorAndGravity, new Point(0, deltaY + 5),
+ Primitives.PopupPositioning.PopupAnchor.Bottom, Primitives.PopupPositioning.PopupGravity.Bottom,
+ Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY);
+ }
+
+ protected virtual void OnSelectedDateChanged(object sender, DatePickerSelectedValueChangedEventArgs e)
+ {
+ SelectedDateChanged?.Invoke(sender, e);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs
new file mode 100644
index 0000000000..eec4615736
--- /dev/null
+++ b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs
@@ -0,0 +1,541 @@
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Shapes;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Defines the presenter used for selecting a date for a
+ ///
+ ///
+ public class DatePickerPresenter : PickerPresenterBase
+ {
+ ///
+ /// Defines the Property
+ ///
+ public static readonly DirectProperty DateProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Date),
+ x => x.Date, (x, v) => x.Date = v);
+
+ ///
+ /// Defines the Property
+ ///
+ public static readonly DirectProperty DayFormatProperty =
+ DatePicker.DayFormatProperty.AddOwner(x =>
+ x.DayFormat, (x, v) => x.DayFormat = v);
+
+ ///
+ /// Defines the Property
+ ///
+ public static readonly DirectProperty DayVisibleProperty =
+ DatePicker.DayVisibleProperty.AddOwner(x =>
+ x.DayVisible, (x, v) => x.DayVisible = v);
+
+ ///
+ /// Defines the Property
+ ///
+ public static readonly DirectProperty MaxYearProperty =
+ DatePicker.MaxYearProperty.AddOwner(x =>
+ x.MaxYear, (x, v) => x.MaxYear = v);
+
+ ///
+ /// Defines the Property
+ ///
+ public static readonly DirectProperty MinYearProperty =
+ DatePicker.MinYearProperty.AddOwner(x =>
+ x.MinYear, (x, v) => x.MinYear = v);
+
+ ///
+ /// Defines the Property
+ ///
+ public static readonly DirectProperty MonthFormatProperty =
+ DatePicker.MonthFormatProperty.AddOwner(x =>
+ x.MonthFormat, (x, v) => x.MonthFormat = v);
+
+ ///
+ /// Defines the Property
+ ///
+ public static readonly DirectProperty MonthVisibleProperty =
+ DatePicker.MonthVisibleProperty.AddOwner(x =>
+ x.MonthVisible, (x, v) => x.MonthVisible = v);
+
+ ///
+ /// Defines the Property
+ ///
+ public static readonly DirectProperty YearFormatProperty =
+ DatePicker.YearFormatProperty.AddOwner(x =>
+ x.YearFormat, (x, v) => x.YearFormat = v);
+
+ ///
+ /// Defines the Property
+ ///
+ public static readonly DirectProperty YearVisibleProperty =
+ DatePicker.YearVisibleProperty.AddOwner(x =>
+ x.YearVisible, (x, v) => x.YearVisible = v);
+
+ // Template Items
+ private Grid _pickerContainer;
+ private Button _acceptButton;
+ private Button _dismissButton;
+ private Rectangle _spacer1;
+ private Rectangle _spacer2;
+ private Panel _monthHost;
+ private Panel _yearHost;
+ private Panel _dayHost;
+ private DateTimePickerPanel _monthSelector;
+ private DateTimePickerPanel _yearSelector;
+ private DateTimePickerPanel _daySelector;
+ private Button _monthUpButton;
+ private Button _dayUpButton;
+ private Button _yearUpButton;
+ private Button _monthDownButton;
+ private Button _dayDownButton;
+ private Button _yearDownButton;
+
+ private DateTimeOffset _date;
+ private string _dayFormat = "%d";
+ private bool _dayVisible = true;
+ private DateTimeOffset _maxYear;
+ private DateTimeOffset _minYear;
+ private string _monthFormat = "MMMM";
+ private bool _monthVisible = true;
+ private string _yearFormat = "yyyy";
+ private bool _yearVisible = true;
+ private DateTimeOffset _syncDate;
+
+ private readonly GregorianCalendar _calendar;
+ private bool _suppressUpdateSelection;
+
+ public DatePickerPresenter()
+ {
+ var now = DateTimeOffset.Now;
+ _minYear = new DateTimeOffset(now.Year - 100, 1, 1, 0, 0, 0, now.Offset);
+ _maxYear = new DateTimeOffset(now.Year + 100, 12, 31, 0, 0, 0, now.Offset);
+ _date = now;
+ _calendar = new GregorianCalendar();
+ KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle);
+ }
+
+ ///
+ /// Gets or sets the current Date for the picker
+ ///
+ public DateTimeOffset Date
+ {
+ get => _date;
+ set
+ {
+ SetAndRaise(DateProperty, ref _date, value);
+ _syncDate = Date;
+ InitPicker();
+ }
+ }
+
+ ///
+ /// Gets or sets the DayFormat
+ ///
+ public string DayFormat
+ {
+ get => _dayFormat;
+ set => SetAndRaise(DayFormatProperty, ref _dayFormat, value);
+ }
+
+ ///
+ /// Get or sets whether the Day selector is visible
+ ///
+ public bool DayVisible
+ {
+ get => _dayVisible;
+ set
+ {
+ SetAndRaise(DayVisibleProperty, ref _dayVisible, value);
+ }
+ }
+
+ ///
+ /// Gets or sets the maximum pickable year
+ ///
+ public DateTimeOffset MaxYear
+ {
+ get => _maxYear;
+ set
+ {
+ if (value < MinYear)
+ throw new InvalidOperationException("MaxDate cannot be less than MinDate");
+ SetAndRaise(MaxYearProperty, ref _maxYear, value);
+
+ if (Date > value)
+ Date = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the minimum pickable year
+ ///
+ public DateTimeOffset MinYear
+ {
+ get => _minYear;
+ set
+ {
+ if (value > MaxYear)
+ throw new InvalidOperationException("MinDate cannot be greater than MaxDate");
+ SetAndRaise(MinYearProperty, ref _minYear, value);
+
+ if (Date < value)
+ Date = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the month format
+ ///
+ public string MonthFormat
+ {
+ get => _monthFormat;
+ set => SetAndRaise(MonthFormatProperty, ref _monthFormat, value);
+ }
+
+ ///
+ /// Gets or sets whether the month selector is visible
+ ///
+ public bool MonthVisible
+ {
+ get => _monthVisible;
+ set
+ {
+ SetAndRaise(MonthVisibleProperty, ref _monthVisible, value);
+ }
+ }
+
+ ///
+ /// Gets or sets the year format
+ ///
+ public string YearFormat
+ {
+ get => _yearFormat;
+ set => SetAndRaise(YearFormatProperty, ref _yearFormat, value);
+ }
+
+ ///
+ /// Gets or sets whether the year selector is visible
+ ///
+ public bool YearVisible
+ {
+ get => _yearVisible;
+ set
+ {
+ SetAndRaise(YearVisibleProperty, ref _yearVisible, value);
+ }
+ }
+
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ base.OnApplyTemplate(e);
+ // These are requirements, so throw if not found
+ _pickerContainer = e.NameScope.Get("PickerContainer");
+ _monthHost = e.NameScope.Get("MonthHost");
+ _dayHost = e.NameScope.Get("DayHost");
+ _yearHost = e.NameScope.Get("YearHost");
+
+ _monthSelector = e.NameScope.Get("MonthSelector");
+ _monthSelector.SelectionChanged += OnMonthChanged;
+
+ _daySelector = e.NameScope.Get("DaySelector");
+ _daySelector.SelectionChanged += OnDayChanged;
+
+ _yearSelector = e.NameScope.Get("YearSelector");
+ _yearSelector.SelectionChanged += OnYearChanged;
+
+ _acceptButton = e.NameScope.Get("AcceptButton");
+
+ _monthUpButton = e.NameScope.Find("MonthUpButton");
+ if (_monthUpButton != null)
+ {
+ _monthUpButton.Click += OnSelectorButtonClick;
+ }
+ _monthDownButton = e.NameScope.Find("MonthDownButton");
+ if (_monthDownButton != null)
+ {
+ _monthDownButton.Click += OnSelectorButtonClick;
+ }
+
+ _dayUpButton = e.NameScope.Find("DayUpButton");
+ if (_dayUpButton != null)
+ {
+ _dayUpButton.Click += OnSelectorButtonClick;
+ }
+ _dayDownButton = e.NameScope.Find("DayDownButton");
+ if (_dayDownButton != null)
+ {
+ _dayDownButton.Click += OnSelectorButtonClick;
+ }
+
+ _yearUpButton = e.NameScope.Find("YearUpButton");
+ if (_yearUpButton != null)
+ {
+ _yearUpButton.Click += OnSelectorButtonClick;
+ }
+ _yearDownButton = e.NameScope.Find("YearDownButton");
+ if (_yearDownButton != null)
+ {
+ _yearDownButton.Click += OnSelectorButtonClick;
+ }
+
+ _dismissButton = e.NameScope.Find("DismissButton");
+ _spacer1 = e.NameScope.Find("FirstSpacer");
+ _spacer2 = e.NameScope.Find("SecondSpacer");
+
+ if (_acceptButton != null)
+ {
+ _acceptButton.Click += OnAcceptButtonClicked;
+ }
+ if (_dismissButton != null)
+ {
+ _dismissButton.Click += OnDismissButtonClicked;
+ }
+ InitPicker();
+ }
+
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ switch (e.Key)
+ {
+ case Key.Escape:
+ OnDismiss();
+ e.Handled = true;
+ break;
+ case Key.Tab:
+ var nextFocus = KeyboardNavigationHandler.GetNext(FocusManager.Instance.Current, NavigationDirection.Next);
+ KeyboardDevice.Instance?.SetFocusedElement(nextFocus, NavigationMethod.Tab, KeyModifiers.None);
+ e.Handled = true;
+ break;
+ case Key.Enter:
+ Date = _syncDate;
+ OnConfirmed();
+ e.Handled = true;
+ break;
+ }
+ base.OnKeyDown(e);
+ }
+
+ ///
+ /// Initializes the picker selectors.
+ ///
+ private void InitPicker()
+ {
+ // OnApplyTemplate must've been called before we can init here...
+ if (_pickerContainer == null)
+ return;
+
+ _suppressUpdateSelection = true;
+
+ _monthSelector.MaximumValue = 12;
+ _monthSelector.MinimumValue = 1;
+ _monthSelector.ItemFormat = MonthFormat;
+
+ _daySelector.ItemFormat = DayFormat;
+
+ _yearSelector.MaximumValue = MaxYear.Year;
+ _yearSelector.MinimumValue = MinYear.Year;
+ _yearSelector.ItemFormat = YearFormat;
+
+ SetGrid();
+
+ // Date should've been set when we reach this point
+ var dt = Date;
+ if (DayVisible)
+ {
+ var maxDays = _calendar.GetDaysInMonth(dt.Year, dt.Month);
+ _daySelector.MaximumValue = maxDays;
+ _daySelector.MinimumValue = 1;
+ _daySelector.SelectedValue = dt.Day;
+ _daySelector.FormatDate = dt.Date;
+ }
+
+ if (MonthVisible)
+ {
+ _monthSelector.SelectedValue = dt.Month;
+ _monthSelector.FormatDate = dt.Date;
+ }
+
+ if (YearVisible)
+ {
+ _yearSelector.SelectedValue = dt.Year;
+ _yearSelector.FormatDate = dt.Date;
+ }
+
+ _suppressUpdateSelection = false;
+
+ SetInitialFocus();
+ }
+
+ private void SetGrid()
+ {
+ var fmt = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
+ var columns = new List<(Panel, int)>
+ {
+ (_monthHost, MonthVisible ? fmt.IndexOf("m", StringComparison.OrdinalIgnoreCase) : -1),
+ (_yearHost, YearVisible ? fmt.IndexOf("y", StringComparison.OrdinalIgnoreCase) : -1),
+ (_dayHost, DayVisible ? fmt.IndexOf("d", StringComparison.OrdinalIgnoreCase) : -1),
+ };
+
+ columns.Sort((x, y) => x.Item2 - y.Item2);
+ _pickerContainer.ColumnDefinitions.Clear();
+
+ var columnIndex = 0;
+
+ foreach (var column in columns)
+ {
+ if (column.Item1 is null)
+ continue;
+
+ column.Item1.IsVisible = column.Item2 != -1;
+
+ if (column.Item2 != -1)
+ {
+ if (columnIndex > 0)
+ {
+ _pickerContainer.ColumnDefinitions.Add(new ColumnDefinition(0, GridUnitType.Auto));
+ }
+
+ _pickerContainer.ColumnDefinitions.Add(
+ new ColumnDefinition(column.Item1 == _monthHost ? 138 : 78, GridUnitType.Star));
+
+ if (column.Item1.Parent is null)
+ {
+ _pickerContainer.Children.Add(column.Item1);
+ }
+
+ Grid.SetColumn(column.Item1, (columnIndex++ * 2));
+ }
+ }
+
+ var isSpacer1Visible = columnIndex > 1;
+ var isSpacer2Visible = columnIndex > 2;
+ // ternary conditional operator is used to make sure grid cells will be validated
+ Grid.SetColumn(_spacer1, isSpacer1Visible ? 1 : 0);
+ Grid.SetColumn(_spacer2, isSpacer2Visible ? 3 : 0);
+
+ _spacer1.IsVisible = isSpacer1Visible;
+ _spacer2.IsVisible = isSpacer2Visible;
+ }
+
+ private void SetInitialFocus()
+ {
+ int monthCol = MonthVisible ? Grid.GetColumn(_monthHost) : int.MaxValue;
+ int dayCol = DayVisible ? Grid.GetColumn(_dayHost) : int.MaxValue;
+ int yearCol = YearVisible ? Grid.GetColumn(_yearHost) : int.MaxValue;
+
+ if (monthCol < dayCol && monthCol < yearCol)
+ {
+ KeyboardDevice.Instance?.SetFocusedElement(_monthSelector, NavigationMethod.Pointer, KeyModifiers.None);
+ }
+ else if (dayCol < monthCol && dayCol < yearCol)
+ {
+ KeyboardDevice.Instance?.SetFocusedElement(_daySelector, NavigationMethod.Pointer, KeyModifiers.None);
+ }
+ else if (yearCol < monthCol && yearCol < dayCol)
+ {
+ KeyboardDevice.Instance?.SetFocusedElement(_yearSelector, NavigationMethod.Pointer, KeyModifiers.None);
+ }
+ }
+
+ private void OnDismissButtonClicked(object sender, RoutedEventArgs e)
+ {
+ OnDismiss();
+ }
+
+ private void OnAcceptButtonClicked(object sender, RoutedEventArgs e)
+ {
+ Date = _syncDate;
+ OnConfirmed();
+ }
+
+ private void OnSelectorButtonClick(object sender, RoutedEventArgs e)
+ {
+ if (sender == _monthUpButton)
+ _monthSelector.ScrollUp();
+ else if (sender == _monthDownButton)
+ _monthSelector.ScrollDown();
+ else if (sender == _yearUpButton)
+ _yearSelector.ScrollUp();
+ else if (sender == _yearDownButton)
+ _yearSelector.ScrollDown();
+ else if (sender == _dayUpButton)
+ _daySelector.ScrollUp();
+ else if (sender == _dayDownButton)
+ _daySelector.ScrollDown();
+ }
+
+ private void OnYearChanged(object sender, EventArgs e)
+ {
+ if (_suppressUpdateSelection)
+ return;
+
+ int maxDays = _calendar.GetDaysInMonth(_yearSelector.SelectedValue, _syncDate.Month);
+ var newDate = new DateTimeOffset(_yearSelector.SelectedValue, _syncDate.Month,
+ _syncDate.Day > maxDays ? maxDays : _syncDate.Day, 0, 0, 0, _syncDate.Offset);
+
+ _syncDate = newDate;
+
+ // We don't need to update the days if not displaying day, not february
+ if (!DayVisible || _syncDate.Month != 2)
+ return;
+
+ _suppressUpdateSelection = true;
+
+ _daySelector.FormatDate = newDate.Date;
+
+ if (_daySelector.MaximumValue != maxDays)
+ _daySelector.MaximumValue = maxDays;
+ else
+ _daySelector.RefreshItems();
+
+ _suppressUpdateSelection = false;
+ }
+
+ private void OnDayChanged(object sender, EventArgs e)
+ {
+ if (_suppressUpdateSelection)
+ return;
+ _syncDate = new DateTimeOffset(_syncDate.Year, _syncDate.Month, _daySelector.SelectedValue, 0, 0, 0, _syncDate.Offset);
+ }
+
+ private void OnMonthChanged(object sender, EventArgs e)
+ {
+ if (_suppressUpdateSelection)
+ return;
+
+ int maxDays = _calendar.GetDaysInMonth(_syncDate.Year, _monthSelector.SelectedValue);
+ var newDate = new DateTimeOffset(_syncDate.Year, _monthSelector.SelectedValue,
+ _syncDate.Day > maxDays ? maxDays : _syncDate.Day, 0, 0, 0, _syncDate.Offset);
+
+ if (!DayVisible)
+ {
+ _syncDate = newDate;
+ return;
+ }
+
+ _suppressUpdateSelection = true;
+
+ _daySelector.FormatDate = newDate.Date;
+ _syncDate = newDate;
+
+ if (_daySelector.MaximumValue != maxDays)
+ _daySelector.MaximumValue = maxDays;
+ else
+ _daySelector.RefreshItems();
+
+ _suppressUpdateSelection = false;
+ }
+
+ internal double GetOffsetForPopup()
+ {
+ var acceptDismissButtonHeight = _acceptButton != null ? _acceptButton.Bounds.Height : 41;
+ return -(MaxHeight - acceptDismissButtonHeight) / 2 - (_monthSelector.ItemHeight / 2);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/DateTimePickers/DatePickerSelectedValueChangedEventArgs.cs b/src/Avalonia.Controls/DateTimePickers/DatePickerSelectedValueChangedEventArgs.cs
new file mode 100644
index 0000000000..6c4c53b623
--- /dev/null
+++ b/src/Avalonia.Controls/DateTimePickers/DatePickerSelectedValueChangedEventArgs.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Defines the argument passed when the SelectedDate changes
+ ///
+ public class DatePickerSelectedValueChangedEventArgs
+ {
+ public DateTimeOffset? NewDate { get; }
+ public DateTimeOffset? OldDate { get; }
+
+ public DatePickerSelectedValueChangedEventArgs(DateTimeOffset? oldDate, DateTimeOffset? newDate)
+ {
+ NewDate = newDate;
+ OldDate = oldDate;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs
new file mode 100644
index 0000000000..daee7968eb
--- /dev/null
+++ b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs
@@ -0,0 +1,566 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using Avalonia.Input;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls.Primitives
+{
+ public enum DateTimePickerPanelType
+ {
+ Year,
+ Month,
+ Day,
+ Hour,
+ Minute,
+ TimePeriod //AM or PM
+ }
+
+ public class DateTimePickerPanel : Panel, ILogicalScrollable
+ {
+ ///
+ /// Defines the property
+ ///
+ public static readonly StyledProperty ItemHeightProperty =
+ AvaloniaProperty.Register(nameof(ItemHeight), 40.0);
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly StyledProperty PanelTypeProperty =
+ AvaloniaProperty.Register(nameof(PanelType));
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly StyledProperty ItemFormatProperty =
+ AvaloniaProperty.Register(nameof(ItemFormat), "yyyy");
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly StyledProperty ShouldLoopProperty =
+ AvaloniaProperty.Register(nameof(ShouldLoop));
+
+ //Backing fields for properties
+ private int _minimumValue = 1;
+ private int _maximumValue = 2;
+ private int _selectedValue = 1;
+ private int _increment = 1;
+
+ //Helper fields
+ private int _selectedIndex = 0;
+ private int _totalItems;
+ private int _numItemsAboveBelowSelected;
+ private int _range;
+ private double _extentOne;
+ private Size _extent;
+ private Vector _offset;
+ private bool _hasInit;
+ private bool _suppressUpdateOffset;
+ private ListBoxItem _pressedItem;
+
+ public DateTimePickerPanel()
+ {
+ FormatDate = DateTime.Now;
+ AddHandler(ListBoxItem.PointerPressedEvent, OnItemPointerDown, Avalonia.Interactivity.RoutingStrategies.Bubble);
+ AddHandler(ListBoxItem.PointerReleasedEvent, OnItemPointerUp, Avalonia.Interactivity.RoutingStrategies.Bubble);
+ }
+
+ static DateTimePickerPanel()
+ {
+ FocusableProperty.OverrideDefaultValue(true);
+ AffectsMeasure(ItemHeightProperty);
+ }
+
+ ///
+ /// Gets or sets what this panel displays in date or time units
+ ///
+ public DateTimePickerPanelType PanelType
+ {
+ get => GetValue(PanelTypeProperty);
+ set => SetValue(PanelTypeProperty, value);
+ }
+
+ ///
+ /// Gets or sets the height of each item
+ ///
+ public double ItemHeight
+ {
+ get => GetValue(ItemHeightProperty);
+ set => SetValue(ItemHeightProperty, value);
+ }
+
+ ///
+ /// Gets or sets the string format for the items, using standard
+ /// .net DateTime or TimeSpan formatting. Format must match panel type
+ ///
+ public string ItemFormat
+ {
+ get => GetValue(ItemFormatProperty);
+ set => SetValue(ItemFormatProperty, value);
+ }
+
+ ///
+ /// Gets or sets whether the panel should loop
+ ///
+ public bool ShouldLoop
+ {
+ get => GetValue(ShouldLoopProperty);
+ set => SetValue(ShouldLoopProperty, value);
+ }
+
+ ///
+ /// Gets or sets the minimum value
+ ///
+ public int MinimumValue
+ {
+ get => _minimumValue;
+ set
+ {
+ if (value > MaximumValue)
+ throw new InvalidOperationException("Minimum cannot be greater than Maximum");
+ _minimumValue = value;
+ UpdateHelperInfo();
+ var sel = CoerceSelected(SelectedValue);
+ if (sel != SelectedValue)
+ SelectedValue = sel;
+ UpdateItems();
+ InvalidateArrange();
+ RaiseScrollInvalidated(EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// Gets or sets the maximum value
+ ///
+ public int MaximumValue
+ {
+ get => _maximumValue;
+ set
+ {
+ if (value < MinimumValue)
+ throw new InvalidOperationException("Maximum cannot be less than Minimum");
+ _maximumValue = value;
+ UpdateHelperInfo();
+ var sel = CoerceSelected(SelectedValue);
+ if (sel != SelectedValue)
+ SelectedValue = sel;
+ UpdateItems();
+ InvalidateArrange();
+ RaiseScrollInvalidated(EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// Gets or sets the selected value
+ ///
+ public int SelectedValue
+ {
+ get => _selectedValue;
+ set
+ {
+ if (value > MaximumValue || value < MinimumValue)
+ throw new ArgumentOutOfRangeException("SelectedValue");
+
+ var sel = CoerceSelected(value);
+ _selectedValue = sel;
+ _selectedIndex = (value - MinimumValue) / Increment;
+
+ if (!ShouldLoop)
+ CreateOrDestroyItems(Children);
+
+ if (!_suppressUpdateOffset)
+ _offset = new Vector(0, ShouldLoop ? _selectedIndex * ItemHeight + (_extentOne * 50) :
+ _selectedIndex * ItemHeight);
+
+ UpdateItems();
+ InvalidateArrange();
+ RaiseScrollInvalidated(EventArgs.Empty);
+
+ SelectionChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// Gets or sets the increment
+ ///
+ public int Increment
+ {
+ get => _increment;
+ set
+ {
+ if (value <= 0 || value > _range)
+ throw new ArgumentOutOfRangeException("Increment");
+ _increment = value;
+ UpdateHelperInfo();
+ var sel = CoerceSelected(SelectedValue);
+ if (sel != SelectedValue)
+ SelectedValue = sel;
+ UpdateItems();
+ InvalidateArrange();
+ RaiseScrollInvalidated(EventArgs.Empty);
+ }
+ }
+
+ //Used to help format the date (if applicable), for ex.,
+ //if we're want to display the day of week, we need context
+ //for the month/year, this is our context
+ internal DateTime FormatDate { get; set; }
+
+ public Vector Offset
+ {
+ get => _offset;
+ set
+ {
+ var old = _offset;
+ _offset = value;
+ var dy = _offset.Y - old.Y;
+ var children = Children;
+
+ if (dy > 0) // Scroll Down
+ {
+ int numContsToMove = 0;
+ for (int i = 0; i < children.Count; i++)
+ {
+ if (children[i].Bounds.Bottom - dy < 0)
+ numContsToMove++;
+ else
+ break;
+ }
+ children.MoveRange(0, numContsToMove, children.Count);
+
+ var scrollHeight = _extent.Height - Viewport.Height;
+ if (ShouldLoop && value.Y >= scrollHeight - _extentOne)
+ _offset = new Vector(0, value.Y - (_extentOne * 50));
+ }
+ else if (dy < 0) // Scroll Up
+ {
+ int numContsToMove = 0;
+ for (int i = children.Count - 1; i >= 0; i--)
+ {
+ if (children[i].Bounds.Top - dy > Bounds.Height)
+ numContsToMove++;
+ else
+ break;
+ }
+ children.MoveRange(children.Count - numContsToMove, numContsToMove, 0);
+ if (ShouldLoop && value.Y < _extentOne)
+ _offset = new Vector(0, value.Y + (_extentOne * 50));
+ }
+
+ //Setting selection will handle all invalidation
+ var newSel = (Offset.Y / ItemHeight) % _totalItems;
+ _suppressUpdateOffset = true;
+ SelectedValue = (int)newSel * Increment + MinimumValue;
+ _suppressUpdateOffset = false;
+ }
+ }
+
+ public bool CanHorizontallyScroll { get => false; set { } }
+
+ public bool CanVerticallyScroll { get => true; set { } }
+
+ public bool IsLogicalScrollEnabled => true;
+
+ public Size ScrollSize => new Size(0, ItemHeight);
+
+ public Size PageScrollSize => new Size(0, ItemHeight * 4);
+
+ public Size Extent => _extent;
+
+ public Size Viewport => new Size(0, ItemHeight);
+
+ public event EventHandler ScrollInvalidated;
+
+ public event EventHandler SelectionChanged;
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ if (double.IsInfinity(availableSize.Width) ||
+ double.IsInfinity(availableSize.Height))
+ throw new InvalidOperationException("Panel must have finite height");
+
+ if (!_hasInit)
+ UpdateHelperInfo();
+
+ double initY = (availableSize.Height / 2.0) - (ItemHeight / 2.0);
+ _numItemsAboveBelowSelected = (int)Math.Ceiling(initY / ItemHeight) + 1;
+
+ var children = Children;
+
+ CreateOrDestroyItems(children);
+
+ for (int i = 0; i < children.Count; i++)
+ children[i].Measure(availableSize);
+
+ if (!_hasInit)
+ {
+ UpdateItems();
+ RaiseScrollInvalidated(EventArgs.Empty);
+ _hasInit = true;
+ }
+
+ return availableSize;
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ if (Children.Count == 0)
+ return base.ArrangeOverride(finalSize);
+
+ var itemHgt = ItemHeight;
+ var children = Children;
+ Rect rc;
+ double initY = (finalSize.Height / 2.0) - (itemHgt / 2.0);
+
+ if (ShouldLoop)
+ {
+ var currentSet = Math.Truncate(Offset.Y / _extentOne);
+ initY += (_extentOne * currentSet) + (_selectedIndex - _numItemsAboveBelowSelected) * ItemHeight;
+
+ for (int i = 0; i < children.Count; i++)
+ {
+ rc = new Rect(0, initY - Offset.Y, finalSize.Width, itemHgt);
+ children[i].Arrange(rc);
+ initY += itemHgt;
+ }
+ }
+ else
+ {
+ var first = Math.Max(0, (_selectedIndex - _numItemsAboveBelowSelected));
+ for (int i = 0; i < children.Count; i++)
+ {
+ rc = new Rect(0, (initY + first * itemHgt) - Offset.Y, finalSize.Width, itemHgt);
+ children[i].Arrange(rc);
+ initY += itemHgt;
+ }
+ }
+
+ return finalSize;
+ }
+
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ switch (e.Key)
+ {
+ case Key.Up:
+ ScrollUp();
+ e.Handled = true;
+ break;
+ case Key.Down:
+ ScrollDown();
+ e.Handled = true;
+ break;
+ case Key.PageUp:
+ ScrollUp(4);
+ e.Handled = true;
+ break;
+ case Key.PageDown:
+ ScrollDown(4);
+ e.Handled = true;
+ break;
+ }
+ base.OnKeyDown(e);
+ }
+
+ ///
+ /// Refreshes the content of the visible items
+ ///
+ public void RefreshItems()
+ {
+ UpdateItems();
+ }
+
+ ///
+ /// Scrolls up the specified number of items
+ ///
+ public void ScrollUp(int numItems = 1)
+ {
+ var newY = Math.Max(Offset.Y - (numItems * ItemHeight), 0);
+ Offset = new Vector(0, newY);
+ }
+
+ ///
+ /// Scrolls down the specified number of items
+ ///
+ public void ScrollDown(int numItems = 1)
+ {
+ var scrollHeight = _extent.Height - Viewport.Height;
+ var newY = Math.Min(Offset.Y + (numItems * ItemHeight), scrollHeight);
+ Offset = new Vector(0, newY);
+ }
+
+ ///
+ /// Updates helper fields used in various calculations
+ ///
+ private void UpdateHelperInfo()
+ {
+ _range = _maximumValue - _minimumValue + 1;
+ _totalItems = (int)Math.Ceiling((double)_range / _increment);
+
+ var itemHgt = ItemHeight;
+ //If looping, measure 100x as many items as we actually have
+ _extent = new Size(0, ShouldLoop ? _totalItems * itemHgt * 100 : _totalItems * itemHgt);
+
+ //Height of 1 "set" of items
+ _extentOne = _totalItems * itemHgt;
+ _offset = new Vector(0, ShouldLoop ? _extentOne * 50 + _selectedIndex * itemHgt : _selectedIndex * itemHgt);
+
+ }
+
+ ///
+ /// Ensures enough containers are visible in the viewport
+ ///
+ ///
+ private void CreateOrDestroyItems(Controls children)
+ {
+ int totalItemsInViewport = _numItemsAboveBelowSelected * 2 + 1;
+
+ if (!ShouldLoop)
+ {
+ int numItemAboveSelect = _numItemsAboveBelowSelected;
+ if (_selectedIndex - _numItemsAboveBelowSelected < 0)
+ numItemAboveSelect = _selectedIndex;
+ int numItemBelowSelect = _numItemsAboveBelowSelected;
+ if (_selectedIndex + _numItemsAboveBelowSelected >= _totalItems)
+ numItemBelowSelect = _totalItems - _selectedIndex - 1;
+
+ totalItemsInViewport = numItemBelowSelect + numItemAboveSelect + 1;
+ }
+
+ while (children.Count < totalItemsInViewport)
+ {
+ children.Add(new ListBoxItem
+ {
+ Height = ItemHeight,
+ Classes = new Classes("DateTimePickerItem", $"{PanelType}Item"),
+ VerticalContentAlignment = Avalonia.Layout.VerticalAlignment.Center,
+ Focusable = false
+ });
+ }
+ if (children.Count > totalItemsInViewport)
+ {
+ var numToRemove = children.Count - totalItemsInViewport;
+ children.RemoveRange(children.Count - numToRemove, numToRemove);
+ }
+ }
+
+ ///
+ /// Updates item content based on the current selection
+ /// and the panel type
+ ///
+ private void UpdateItems()
+ {
+ var children = Children;
+ var min = MinimumValue;
+ var panelType = PanelType;
+ var selected = SelectedValue;
+ var max = MaximumValue;
+
+ int first;
+ if (ShouldLoop)
+ {
+ first = (_selectedIndex - _numItemsAboveBelowSelected) % _totalItems;
+ first = first < 0 ? min + (first + _totalItems) * Increment : min + first * Increment;
+ }
+ else
+ {
+ first = min + Math.Max(0, _selectedIndex - _numItemsAboveBelowSelected) * Increment;
+ }
+
+ for (int i = 0; i < children.Count; i++)
+ {
+ ListBoxItem item = (ListBoxItem)children[i];
+ item.Content = FormatContent(first, panelType);
+ item.Tag = first;
+ item.IsSelected = first == selected;
+ first += Increment;
+ if (first > max)
+ first = min;
+ }
+ }
+
+ private string FormatContent(int value, DateTimePickerPanelType panelType)
+ {
+ switch (panelType)
+ {
+ case DateTimePickerPanelType.Year:
+ return new DateTime(value, FormatDate.Month, FormatDate.Day).ToString(ItemFormat);
+ case DateTimePickerPanelType.Month:
+ return new DateTime(FormatDate.Year, value, FormatDate.Day).ToString(ItemFormat);
+ case DateTimePickerPanelType.Day:
+ return new DateTime(FormatDate.Year, FormatDate.Month, value).ToString(ItemFormat);
+ case DateTimePickerPanelType.Hour:
+ return new TimeSpan(value, 0, 0).ToString(ItemFormat);
+ case DateTimePickerPanelType.Minute:
+ return new TimeSpan(0, value, 0).ToString(ItemFormat);
+ case DateTimePickerPanelType.TimePeriod:
+ return value == MinimumValue ? CultureInfo.CurrentCulture.DateTimeFormat.AMDesignator :
+ CultureInfo.CurrentCulture.DateTimeFormat.PMDesignator;
+ default:
+ return "";
+ }
+ }
+
+ ///
+ /// Ensures the is within the bounds and
+ /// follows the current Increment
+ ///
+ private int CoerceSelected(int newValue)
+ {
+ if (newValue < MinimumValue)
+ return MinimumValue;
+ if (newValue > MaximumValue)
+ return MaximumValue;
+
+ if (newValue % Increment != 0)
+ {
+ var items = Enumerable.Range(MinimumValue, MaximumValue + 1).Where(i => i % Increment == 0).ToList();
+ var nearest = items.Aggregate((x, y) => Math.Abs(x - newValue) > Math.Abs(y - newValue) ? y : x);
+ return items.IndexOf(nearest) * Increment;
+ }
+ return newValue;
+ }
+
+ private void OnItemPointerDown(object sender, PointerPressedEventArgs e)
+ {
+ if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
+ {
+ _pressedItem = GetItemFromSource((IVisual)e.Source);
+ e.Handled = true;
+ }
+ }
+
+ private void OnItemPointerUp(object sender, PointerReleasedEventArgs e)
+ {
+ if (e.GetCurrentPoint(this).Properties.PointerUpdateKind == PointerUpdateKind.LeftButtonReleased &&
+ _pressedItem != null)
+ {
+ SelectedValue = (int)GetItemFromSource((IVisual)e.Source).Tag;
+ _pressedItem = null;
+ e.Handled = true;
+ }
+ }
+
+ //Helper to get ListBoxItem from pointerevent source
+ private ListBoxItem GetItemFromSource(IVisual src)
+ {
+ var item = src;
+ while (item != null && !(item is ListBoxItem))
+ {
+ item = item.VisualParent;
+ }
+ return (ListBoxItem)item;
+ }
+
+ public bool BringIntoView(IControl target, Rect targetRect) { return false; }
+
+ public IControl GetControlInDirection(NavigationDirection direction, IControl from) { return null; }
+
+ public void RaiseScrollInvalidated(EventArgs e)
+ {
+ ScrollInvalidated?.Invoke(this, e);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/DateTimePickers/PickerPresenterBase.cs b/src/Avalonia.Controls/DateTimePickers/PickerPresenterBase.cs
new file mode 100644
index 0000000000..639b187ad8
--- /dev/null
+++ b/src/Avalonia.Controls/DateTimePickers/PickerPresenterBase.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Avalonia.Controls.Primitives
+{
+ ///
+ /// Defines the base class for Date and Time PickerPresenters
+ ///
+ public abstract class PickerPresenterBase : TemplatedControl
+ {
+ protected virtual void OnConfirmed()
+ {
+ Confirmed?.Invoke(this, EventArgs.Empty);
+ }
+
+ protected virtual void OnDismiss()
+ {
+ Dismissed.Invoke(this, EventArgs.Empty);
+ }
+
+ public event EventHandler Confirmed;
+ public event EventHandler Dismissed;
+ }
+}
diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs
new file mode 100644
index 0000000000..e54da1fb3a
--- /dev/null
+++ b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs
@@ -0,0 +1,277 @@
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Shapes;
+using Avalonia.Controls.Templates;
+using System;
+using System.Globalization;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// A control to allow the user to select a time
+ ///
+ public class TimePicker : TemplatedControl
+ {
+ ///
+ /// Defines the property
+ ///
+ public static readonly DirectProperty MinuteIncrementProperty =
+ AvaloniaProperty.RegisterDirect(nameof(MinuteIncrement),
+ x => x.MinuteIncrement, (x, v) => x.MinuteIncrement = v);
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly StyledProperty HeaderProperty =
+ AvaloniaProperty.Register(nameof(Header));
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly StyledProperty HeaderTemplateProperty =
+ AvaloniaProperty.Register(nameof(HeaderTemplate));
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly DirectProperty ClockIdentifierProperty =
+ AvaloniaProperty.RegisterDirect(nameof(ClockIdentifier),
+ x => x.ClockIdentifier, (x, v) => x.ClockIdentifier = v);
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly DirectProperty SelectedTimeProperty =
+ AvaloniaProperty.RegisterDirect(nameof(SelectedTime),
+ x => x.SelectedTime, (x, v) => x.SelectedTime = v);
+
+ // Template Items
+ private TimePickerPresenter _presenter;
+ private Button _flyoutButton;
+ private Border _firstPickerHost;
+ private Border _secondPickerHost;
+ private Border _thirdPickerHost;
+ private TextBlock _hourText;
+ private TextBlock _minuteText;
+ private TextBlock _periodText;
+ private Rectangle _firstSplitter;
+ private Rectangle _secondSplitter;
+ private Grid _contentGrid;
+ private Popup _popup;
+
+ private TimeSpan? _selectedTime;
+ private int _minuteIncrement = 1;
+ private string _clockIdentifier = "12HourClock";
+
+ public TimePicker()
+ {
+ PseudoClasses.Set(":hasnotime", true);
+
+ var timePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern;
+ if (timePattern.IndexOf("H") != -1)
+ _clockIdentifier = "24HourClock";
+ }
+
+ ///
+ /// Gets or sets the minute increment in the picker
+ ///
+ public int MinuteIncrement
+ {
+ get => _minuteIncrement;
+ set
+ {
+ if (value < 1 || value > 59)
+ throw new ArgumentOutOfRangeException("1 >= MinuteIncrement <= 59");
+ SetAndRaise(MinuteIncrementProperty, ref _minuteIncrement, value);
+ SetSelectedTimeText();
+ }
+ }
+
+ ///
+ /// Gets or sets the header
+ ///
+ public object Header
+ {
+ get => GetValue(HeaderProperty);
+ set => SetValue(HeaderProperty, value);
+ }
+
+ ///
+ /// Gets or sets the header template
+ ///
+ public IDataTemplate HeaderTemplate
+ {
+ get => GetValue(HeaderTemplateProperty);
+ set => SetValue(HeaderTemplateProperty, value);
+ }
+
+ ///
+ /// Gets or sets the clock identifier, either 12HourClock or 24HourClock
+ ///
+ public string ClockIdentifier
+ {
+ get => _clockIdentifier;
+ set
+ {
+ if (!(string.IsNullOrEmpty(value) || value == "" || value == "12HourClock" || value == "24HourClock"))
+ throw new ArgumentException("Invalid ClockIdentifier");
+ SetAndRaise(ClockIdentifierProperty, ref _clockIdentifier, value);
+ SetGrid();
+ SetSelectedTimeText();
+ }
+ }
+
+ ///
+ /// Gets or sets the selected time. Can be null.
+ ///
+ public TimeSpan? SelectedTime
+ {
+ get => _selectedTime;
+ set
+ {
+ var old = _selectedTime;
+ SetAndRaise(SelectedTimeProperty, ref _selectedTime, value);
+ OnSelectedTimeChanged(old, value);
+ SetSelectedTimeText();
+ }
+ }
+
+ ///
+ /// Raised when the property changes
+ ///
+ public event EventHandler SelectedTimeChanged;
+
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ if (_flyoutButton != null)
+ _flyoutButton.Click -= OnFlyoutButtonClicked;
+
+ if (_presenter != null)
+ {
+ _presenter.Confirmed -= OnConfirmed;
+ _presenter.Dismissed -= OnDismissPicker;
+ }
+ base.OnApplyTemplate(e);
+
+ _flyoutButton = e.NameScope.Find("FlyoutButton");
+
+ _firstPickerHost = e.NameScope.Find("FirstPickerHost");
+ _secondPickerHost = e.NameScope.Find("SecondPickerHost");
+ _thirdPickerHost = e.NameScope.Find("ThirdPickerHost");
+
+ _hourText = e.NameScope.Find("HourTextBlock");
+ _minuteText = e.NameScope.Find("MinuteTextBlock");
+ _periodText = e.NameScope.Find("PeriodTextBlock");
+
+ _firstSplitter = e.NameScope.Find("FirstColumnDivider");
+ _secondSplitter = e.NameScope.Find("SecondColumnDivider");
+
+ _contentGrid = e.NameScope.Find("FlyoutButtonContentGrid");
+
+ _popup = e.NameScope.Find("Popup");
+ _presenter = e.NameScope.Find("PickerPresenter");
+
+ if (_flyoutButton != null)
+ _flyoutButton.Click += OnFlyoutButtonClicked;
+
+ SetGrid();
+ SetSelectedTimeText();
+
+ if (_presenter != null)
+ {
+ _presenter.Confirmed += OnConfirmed;
+ _presenter.Dismissed += OnDismissPicker;
+
+ _presenter[!TimePickerPresenter.MinuteIncrementProperty] = this[!MinuteIncrementProperty];
+ _presenter[!TimePickerPresenter.ClockIdentifierProperty] = this[!ClockIdentifierProperty];
+ }
+ }
+
+ private void SetGrid()
+ {
+ if (_contentGrid == null)
+ return;
+
+ bool use24HourClock = ClockIdentifier == "24HourClock";
+
+ var columnsD = use24HourClock ? "*, Auto, *" : "*, Auto, *, Auto, *";
+ _contentGrid.ColumnDefinitions = new ColumnDefinitions(columnsD);
+
+ _thirdPickerHost.IsVisible = !use24HourClock;
+ _secondSplitter.IsVisible = !use24HourClock;
+
+ Grid.SetColumn(_firstPickerHost, 0);
+ Grid.SetColumn(_secondPickerHost, 2);
+
+ Grid.SetColumn(_thirdPickerHost, use24HourClock ? 0 : 4);
+
+ Grid.SetColumn(_firstSplitter, 1);
+ Grid.SetColumn(_secondSplitter, use24HourClock ? 0 : 3);
+ }
+
+ private void SetSelectedTimeText()
+ {
+ if (_hourText == null || _minuteText == null || _periodText == null)
+ return;
+
+ var time = SelectedTime;
+ if (time.HasValue)
+ {
+ var newTime = SelectedTime.Value;
+
+ if (ClockIdentifier == "12HourClock")
+ {
+ var hr = newTime.Hours;
+ hr = hr > 12 ? hr - 12 : hr == 0 ? 12 : hr;
+ newTime = new TimeSpan(hr, newTime.Minutes, 0);
+ }
+
+ _hourText.Text = newTime.ToString("%h");
+ _minuteText.Text = newTime.ToString("mm");
+ PseudoClasses.Set(":hasnotime", false);
+
+ _periodText.Text = time.Value.Hours >= 12 ? CultureInfo.CurrentCulture.DateTimeFormat.PMDesignator :
+ CultureInfo.CurrentCulture.DateTimeFormat.AMDesignator;
+ }
+ else
+ {
+ _hourText.Text = "hour";
+ _minuteText.Text = "minute";
+ PseudoClasses.Set(":hasnotime", true);
+
+ _periodText.Text = DateTime.Now.Hour >= 12 ? CultureInfo.CurrentCulture.DateTimeFormat.PMDesignator :
+ CultureInfo.CurrentCulture.DateTimeFormat.AMDesignator;
+ }
+ }
+
+ protected virtual void OnSelectedTimeChanged(TimeSpan? oldTime, TimeSpan? newTime)
+ {
+ SelectedTimeChanged?.Invoke(this, new TimePickerSelectedValueChangedEventArgs(oldTime, newTime));
+ }
+
+ private void OnFlyoutButtonClicked(object sender, Interactivity.RoutedEventArgs e)
+ {
+ _presenter.Time = SelectedTime ?? DateTime.Now.TimeOfDay;
+
+ _popup.IsOpen = true;
+
+ var deltaY = _presenter.GetOffsetForPopup();
+
+ // The extra 5 px I think is related to default popup placement behavior
+ _popup.Host.ConfigurePosition(_popup.PlacementTarget, PlacementMode.AnchorAndGravity, new Point(0, deltaY + 5),
+ Primitives.PopupPositioning.PopupAnchor.Bottom, Primitives.PopupPositioning.PopupGravity.Bottom,
+ Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY);
+ }
+
+ private void OnDismissPicker(object sender, EventArgs e)
+ {
+ _popup.Close();
+ Focus();
+ }
+
+ private void OnConfirmed(object sender, EventArgs e)
+ {
+ _popup.Close();
+ SelectedTime = _presenter.Time;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs
new file mode 100644
index 0000000000..920eeeb406
--- /dev/null
+++ b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs
@@ -0,0 +1,260 @@
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Shapes;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using System;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Defines the presenter used for selecting a time. Intended for use with
+ /// but can be used independently
+ ///
+ public class TimePickerPresenter : PickerPresenterBase
+ {
+ ///
+ /// Defines the property
+ ///
+ public static readonly DirectProperty MinuteIncrementProperty =
+ TimePicker.MinuteIncrementProperty.AddOwner(x => x.MinuteIncrement,
+ (x, v) => x.MinuteIncrement = v);
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly DirectProperty ClockIdentifierProperty =
+ TimePicker.ClockIdentifierProperty.AddOwner(x => x.ClockIdentifier,
+ (x, v) => x.ClockIdentifier = v);
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly DirectProperty TimeProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Time),
+ x => x.Time, (x, v) => x.Time = v);
+
+ public TimePickerPresenter()
+ {
+ Time = DateTime.Now.TimeOfDay;
+ KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle);
+ }
+
+ // TemplateItems
+ private Grid _pickerContainer;
+ private Button _acceptButton;
+ private Button _dismissButton;
+ private Rectangle _spacer2;
+ private Panel _periodHost;
+ private DateTimePickerPanel _hourSelector;
+ private DateTimePickerPanel _minuteSelector;
+ private DateTimePickerPanel _periodSelector;
+ private Button _hourUpButton;
+ private Button _minuteUpButton;
+ private Button _periodUpButton;
+ private Button _hourDownButton;
+ private Button _minuteDownButton;
+ private Button _periodDownButton;
+
+ // Backing Fields
+ private TimeSpan _time;
+ private int _minuteIncrement = 1;
+ private string _clockIdentifier = "12HourClock";
+
+ ///
+ /// Gets or sets the minute increment in the selector
+ ///
+ public int MinuteIncrement
+ {
+ get => _minuteIncrement;
+ set
+ {
+ if (value < 1 || value > 59)
+ throw new ArgumentOutOfRangeException("1 >= MinuteIncrement <= 59");
+ SetAndRaise(MinuteIncrementProperty, ref _minuteIncrement, value);
+ InitPicker();
+ }
+ }
+
+ ///
+ /// Gets or sets the current clock identifier, either 12HourClock or 24HourClock
+ ///
+ public string ClockIdentifier
+ {
+ get => _clockIdentifier;
+ set
+ {
+ if (string.IsNullOrEmpty(value) || !(value == "12HourClock" || value == "24HourClock"))
+ throw new ArgumentException("Invalid ClockIdentifier");
+ SetAndRaise(ClockIdentifierProperty, ref _clockIdentifier, value);
+ InitPicker();
+ }
+ }
+
+ ///
+ /// Gets or sets the current time
+ ///
+ public TimeSpan Time
+ {
+ get => _time;
+ set
+ {
+ SetAndRaise(TimeProperty, ref _time, value);
+ InitPicker();
+ }
+ }
+
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ base.OnApplyTemplate(e);
+
+ _pickerContainer = e.NameScope.Get("PickerContainer");
+ _periodHost = e.NameScope.Get("PeriodHost");
+
+ _hourSelector = e.NameScope.Get("HourSelector");
+ _minuteSelector = e.NameScope.Get("MinuteSelector");
+ _periodSelector = e.NameScope.Get("PeriodSelector");
+
+ _spacer2 = e.NameScope.Get("SecondSpacer");
+
+ _acceptButton = e.NameScope.Get("AcceptButton");
+ _acceptButton.Click += OnAcceptButtonClicked;
+
+ _hourUpButton = e.NameScope.Find("HourUpButton");
+ if (_hourUpButton != null)
+ _hourUpButton.Click += OnSelectorButtonClick;
+ _hourDownButton = e.NameScope.Find("HourDownButton");
+ if (_hourDownButton != null)
+ _hourDownButton.Click += OnSelectorButtonClick;
+
+ _minuteUpButton = e.NameScope.Find("MinuteUpButton");
+ if (_minuteUpButton != null)
+ _minuteUpButton.Click += OnSelectorButtonClick;
+ _minuteDownButton = e.NameScope.Find("MinuteDownButton");
+ if (_minuteDownButton != null)
+ _minuteDownButton.Click += OnSelectorButtonClick;
+
+ _periodUpButton = e.NameScope.Find("PeriodUpButton");
+ if (_periodUpButton != null)
+ _periodUpButton.Click += OnSelectorButtonClick;
+ _periodDownButton = e.NameScope.Find("PeriodDownButton");
+ if (_periodDownButton != null)
+ _periodDownButton.Click += OnSelectorButtonClick;
+
+ _dismissButton = e.NameScope.Find("DismissButton");
+ if (_dismissButton != null)
+ _dismissButton.Click += OnDismissButtonClicked;
+
+ InitPicker();
+ }
+
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ switch (e.Key)
+ {
+ case Key.Escape:
+ OnDismiss();
+ e.Handled = true;
+ break;
+ case Key.Tab:
+ var nextFocus = KeyboardNavigationHandler.GetNext(FocusManager.Instance.Current, NavigationDirection.Next);
+ KeyboardDevice.Instance?.SetFocusedElement(nextFocus, NavigationMethod.Tab, KeyModifiers.None);
+ e.Handled = true;
+ break;
+ case Key.Enter:
+ OnConfirmed();
+ e.Handled = true;
+ break;
+ }
+ base.OnKeyDown(e);
+ }
+
+ protected override void OnConfirmed()
+ {
+ var hr = _hourSelector.SelectedValue;
+ var min = _minuteSelector.SelectedValue;
+ var per = _periodSelector.SelectedValue;
+
+ if (ClockIdentifier == "12HourClock")
+ {
+ hr = per == 1 ? hr + 12 : per == 0 && hr == 12 ? 0 : hr;
+ }
+
+ Time = new TimeSpan(hr, min, 0);
+
+ base.OnConfirmed();
+ }
+
+ private void InitPicker()
+ {
+ if (_pickerContainer == null)
+ return;
+
+ bool clock12 = ClockIdentifier == "12HourClock";
+ _hourSelector.MaximumValue = clock12 ? 12 : 23;
+ _hourSelector.MinimumValue = clock12 ? 1 : 0;
+ _hourSelector.ItemFormat = "%h";
+ var hr = Time.Hours;
+ _hourSelector.SelectedValue = !clock12 ? hr :
+ hr > 12 ? hr - 12 : hr == 0 ? 12 : hr;
+
+ _minuteSelector.MaximumValue = 59;
+ _minuteSelector.MinimumValue = 0;
+ _minuteSelector.Increment = MinuteIncrement;
+ _minuteSelector.SelectedValue = Time.Minutes;
+ _minuteSelector.ItemFormat = "mm";
+
+ _periodSelector.MaximumValue = 1;
+ _periodSelector.MinimumValue = 0;
+ _periodSelector.SelectedValue = hr >= 12 ? 1 : 0;
+
+ SetGrid();
+ KeyboardDevice.Instance?.SetFocusedElement(_hourSelector, NavigationMethod.Pointer, KeyModifiers.None);
+ }
+
+ private void SetGrid()
+ {
+ bool use24HourClock = ClockIdentifier == "24HourClock";
+
+ var columnsD = use24HourClock ? "*, Auto, *" : "*, Auto, *, Auto, *";
+ _pickerContainer.ColumnDefinitions = new ColumnDefinitions(columnsD);
+
+ _spacer2.IsVisible = !use24HourClock;
+ _periodHost.IsVisible = !use24HourClock;
+
+ Grid.SetColumn(_spacer2, use24HourClock ? 0 : 3);
+ Grid.SetColumn(_periodHost, use24HourClock ? 0 : 4);
+ }
+
+ private void OnDismissButtonClicked(object sender, RoutedEventArgs e)
+ {
+ OnDismiss();
+ }
+
+ private void OnAcceptButtonClicked(object sender, RoutedEventArgs e)
+ {
+ OnConfirmed();
+ }
+
+ private void OnSelectorButtonClick(object sender, RoutedEventArgs e)
+ {
+ if (sender == _hourUpButton)
+ _hourSelector.ScrollUp();
+ else if (sender == _hourDownButton)
+ _hourSelector.ScrollDown();
+ else if (sender == _minuteUpButton)
+ _minuteSelector.ScrollUp();
+ else if (sender == _minuteDownButton)
+ _minuteSelector.ScrollDown();
+ else if (sender == _periodUpButton)
+ _periodSelector.ScrollUp();
+ else if (sender == _periodDownButton)
+ _periodSelector.ScrollDown();
+ }
+
+ internal double GetOffsetForPopup()
+ {
+ var acceptDismissButtonHeight = _acceptButton != null ? _acceptButton.Bounds.Height : 41;
+ return -(MaxHeight - acceptDismissButtonHeight) / 2 - (_hourSelector.ItemHeight / 2);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/DateTimePickers/TimePickerSelectedValueChangedEventArgs.cs b/src/Avalonia.Controls/DateTimePickers/TimePickerSelectedValueChangedEventArgs.cs
new file mode 100644
index 0000000000..c2954ba449
--- /dev/null
+++ b/src/Avalonia.Controls/DateTimePickers/TimePickerSelectedValueChangedEventArgs.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace Avalonia.Controls
+{
+ public class TimePickerSelectedValueChangedEventArgs
+ {
+ public TimeSpan? OldTime { get; }
+ public TimeSpan? NewTime { get; }
+ public TimePickerSelectedValueChangedEventArgs(TimeSpan? old, TimeSpan? newT)
+ {
+ OldTime = old;
+ NewTime = newT;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs
index 2d48a7d33b..5ff0fd1feb 100644
--- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs
+++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs
@@ -10,7 +10,7 @@ namespace Avalonia.Controls.Embedding
{
public class EmbeddableControlRoot : TopLevel, IStyleable, IFocusScope, IDisposable
{
- public EmbeddableControlRoot(IEmbeddableWindowImpl impl) : base(impl)
+ public EmbeddableControlRoot(ITopLevelImpl impl) : base(impl)
{
}
@@ -19,16 +19,13 @@ namespace Avalonia.Controls.Embedding
{
}
- [CanBeNull]
- public new IEmbeddableWindowImpl PlatformImpl => (IEmbeddableWindowImpl) base.PlatformImpl;
-
protected bool EnforceClientSize { get; set; } = true;
public void Prepare()
{
EnsureInitialized();
ApplyTemplate();
- LayoutManager.ExecuteInitialLayoutPass(this);
+ LayoutManager.ExecuteInitialLayoutPass();
}
private void EnsureInitialized()
diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs
index d326ab5734..b037dd9901 100644
--- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs
+++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs
@@ -18,7 +18,7 @@ namespace Avalonia.Controls.Embedding.Offscreen
{
EnsureInitialized();
ApplyTemplate();
- LayoutManager.ExecuteInitialLayoutPass(this);
+ LayoutManager.ExecuteInitialLayoutPass();
}
private void EnsureInitialized()
diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
index 09f9ed7c1b..522103c7bd 100644
--- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
+++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
@@ -35,7 +35,7 @@ namespace Avalonia.Controls.Embedding.Offscreen
}
}
- public double Scaling
+ public double RenderScaling
{
get { return _scaling; }
set
@@ -52,6 +52,9 @@ namespace Avalonia.Controls.Embedding.Offscreen
public Action TransparencyLevelChanged { get; set; }
+ ///
+ public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1);
+
public void SetInputRoot(IInputRoot inputRoot) => InputRoot = inputRoot;
public virtual Point PointToClient(PixelPoint point) => point.ToPoint(1);
@@ -63,6 +66,7 @@ namespace Avalonia.Controls.Embedding.Offscreen
}
public Action Closed { get; set; }
+ public Action LostFocus { get; set; }
public abstract IMouseDevice MouseDevice { get; }
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { }
diff --git a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs
new file mode 100644
index 0000000000..0a01767a07
--- /dev/null
+++ b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs
@@ -0,0 +1,117 @@
+using Avalonia.Controls.Utils;
+using Avalonia.Layout;
+using Avalonia.Media;
+using Avalonia.Platform;
+using System;
+
+namespace Avalonia.Controls
+{
+ public class ExperimentalAcrylicBorder : Decorator
+ {
+ public static readonly StyledProperty CornerRadiusProperty =
+ Border.CornerRadiusProperty.AddOwner();
+
+ public static readonly StyledProperty MaterialProperty =
+ AvaloniaProperty.Register(nameof(Material));
+
+ private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
+
+ private IDisposable _subscription;
+
+ static ExperimentalAcrylicBorder()
+ {
+ AffectsRender(
+ MaterialProperty,
+ CornerRadiusProperty);
+ }
+
+
+ ///
+ /// Gets or sets the radius of the border rounded corners.
+ ///
+ public CornerRadius CornerRadius
+ {
+ get { return GetValue(CornerRadiusProperty); }
+ set { SetValue(CornerRadiusProperty, value); }
+ }
+
+ public ExperimentalAcrylicMaterial Material
+ {
+ get => GetValue(MaterialProperty);
+ set => SetValue(MaterialProperty, value);
+ }
+
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+
+ var tl = (e.Root as TopLevel);
+
+ _subscription = tl.GetObservable(TopLevel.ActualTransparencyLevelProperty)
+ .Subscribe(x =>
+ {
+ switch (x)
+ {
+ case WindowTransparencyLevel.Transparent:
+ case WindowTransparencyLevel.None:
+ Material.PlatformTransparencyCompensationLevel = tl.PlatformImpl.AcrylicCompensationLevels.TransparentLevel;
+ break;
+
+ case WindowTransparencyLevel.Blur:
+ Material.PlatformTransparencyCompensationLevel = tl.PlatformImpl.AcrylicCompensationLevels.BlurLevel;
+ break;
+
+ case WindowTransparencyLevel.AcrylicBlur:
+ Material.PlatformTransparencyCompensationLevel = tl.PlatformImpl.AcrylicCompensationLevels.AcrylicBlurLevel;
+ break;
+ }
+ });
+ }
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnDetachedFromVisualTree(e);
+
+ _subscription?.Dispose();
+ }
+
+ public override void Render(DrawingContext context)
+ {
+ if (context.PlatformImpl is IDrawingContextWithAcrylicLikeSupport idc)
+ {
+ var cornerRadius = CornerRadius;
+
+ idc.DrawRectangle(
+ Material,
+ new RoundedRect(
+ new Rect(Bounds.Size),
+ cornerRadius.TopLeft, cornerRadius.TopRight,
+ cornerRadius.BottomRight, cornerRadius.BottomLeft));
+ }
+ else
+ {
+ _borderRenderHelper.Render(context, Bounds.Size, new Thickness(), CornerRadius, new SolidColorBrush(Material.FallbackColor), null, default);
+ }
+ }
+
+ ///
+ /// Measures the control.
+ ///
+ /// The available size.
+ /// The desired size of the control.
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ return LayoutHelper.MeasureChild(Child, availableSize, Padding);
+ }
+
+ ///
+ /// Arranges the control's child.
+ ///
+ /// The size allocated to the control.
+ /// The space taken.
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ return LayoutHelper.ArrangeChild(Child, finalSize, Padding);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
index cd1ce3deae..9e65ef5f81 100644
--- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
+++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
@@ -142,7 +142,6 @@ namespace Avalonia.Controls.Generators
private readonly IDataTemplate _inner;
public WrapperTreeDataTemplate(IDataTemplate inner) => _inner = inner;
public IControl Build(object param) => _inner.Build(param);
- public bool SupportsRecycling => _inner.SupportsRecycling;
public bool Match(object data) => _inner.Match(data);
public InstancedBinding ItemsSelector(object item) => null;
}
diff --git a/src/Avalonia.Controls/IMenuElement.cs b/src/Avalonia.Controls/IMenuElement.cs
index ee9d0fd6b6..426f265084 100644
--- a/src/Avalonia.Controls/IMenuElement.cs
+++ b/src/Avalonia.Controls/IMenuElement.cs
@@ -1,6 +1,8 @@
using System.Collections.Generic;
using Avalonia.Input;
+#nullable enable
+
namespace Avalonia.Controls
{
///
@@ -11,7 +13,7 @@ namespace Avalonia.Controls
///
/// Gets or sets the currently selected submenu item.
///
- IMenuItem SelectedItem { get; set; }
+ IMenuItem? SelectedItem { get; set; }
///
/// Gets the submenu items.
diff --git a/src/Avalonia.Controls/IMenuItem.cs b/src/Avalonia.Controls/IMenuItem.cs
index 132d565cb7..94d761f725 100644
--- a/src/Avalonia.Controls/IMenuItem.cs
+++ b/src/Avalonia.Controls/IMenuItem.cs
@@ -1,4 +1,6 @@
-namespace Avalonia.Controls
+#nullable enable
+
+namespace Avalonia.Controls
{
///
/// Represents a .
@@ -29,7 +31,7 @@
///
/// Gets the parent .
///
- new IMenuElement Parent { get; }
+ new IMenuElement? Parent { get; }
///
/// Raises a click event on the menu item.
diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs
index 6e0ad66699..1aa7945901 100644
--- a/src/Avalonia.Controls/ItemsControl.cs
+++ b/src/Avalonia.Controls/ItemsControl.cs
@@ -295,7 +295,7 @@ namespace Avalonia.Controls
if (next != null)
{
- focus.Focus(next, NavigationMethod.Directional);
+ focus.Focus(next, NavigationMethod.Directional, e.KeyModifiers);
e.Handled = true;
}
diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs
index 3c21cd2c38..a085bfb6bc 100644
--- a/src/Avalonia.Controls/ListBox.cs
+++ b/src/Avalonia.Controls/ListBox.cs
@@ -136,7 +136,8 @@ namespace Avalonia.Controls
e.Handled = UpdateSelectionFromEventSource(
e.Source,
true,
- (e.KeyModifiers & KeyModifiers.Shift) != 0);
+ (e.KeyModifiers & KeyModifiers.Shift) != 0,
+ (e.KeyModifiers & KeyModifiers.Control) != 0);
}
}
diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs
index 7205af0e75..4da044fec1 100644
--- a/src/Avalonia.Controls/Menu.cs
+++ b/src/Avalonia.Controls/Menu.cs
@@ -1,9 +1,12 @@
using Avalonia.Controls.Platform;
+using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
+#nullable enable
+
namespace Avalonia.Controls
{
///
@@ -14,6 +17,8 @@ namespace Avalonia.Controls
private static readonly ITemplate DefaultPanel =
new FuncTemplate(() => new StackPanel { Orientation = Orientation.Horizontal });
+ private LightDismissOverlayLayer? _overlay;
+
///
/// Initializes a new instance of the class.
///
diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs
index 4554cb2bcf..0434928280 100644
--- a/src/Avalonia.Controls/MenuBase.cs
+++ b/src/Avalonia.Controls/MenuBase.cs
@@ -8,6 +8,8 @@ using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
+#nullable enable
+
namespace Avalonia.Controls
{
///
@@ -51,9 +53,7 @@ namespace Avalonia.Controls
/// The menu interaction handler.
public MenuBase(IMenuInteractionHandler interactionHandler)
{
- Contract.Requires(interactionHandler != null);
-
- InteractionHandler = interactionHandler;
+ InteractionHandler = interactionHandler ?? throw new ArgumentNullException(nameof(interactionHandler));
}
///
@@ -77,7 +77,7 @@ namespace Avalonia.Controls
IMenuInteractionHandler IMenu.InteractionHandler => InteractionHandler;
///
- IMenuItem IMenuElement.SelectedItem
+ IMenuItem? IMenuElement.SelectedItem
{
get
{
diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs
index 912abc6de3..b4d3272471 100644
--- a/src/Avalonia.Controls/MenuItem.cs
+++ b/src/Avalonia.Controls/MenuItem.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Reactive.Linq;
using System.Windows.Input;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Mixins;
@@ -12,6 +13,8 @@ using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.VisualTree;
+#nullable enable
+
namespace Avalonia.Controls
{
///
@@ -22,7 +25,7 @@ namespace Avalonia.Controls
///
/// Defines the property.
///
- public static readonly DirectProperty CommandProperty =
+ public static readonly DirectProperty CommandProperty =
Button.CommandProperty.AddOwner(
menuItem => menuItem.Command,
(menuItem, command) => menuItem.Command = command,
@@ -94,10 +97,9 @@ namespace Avalonia.Controls
private static readonly ITemplate DefaultPanel =
new FuncTemplate(() => new StackPanel());
- private ICommand _command;
+ private ICommand? _command;
private bool _commandCanExecute = true;
private Popup _popup;
- private IDisposable _gridHack;
///
/// Initializes static members of the class.
@@ -119,6 +121,32 @@ namespace Avalonia.Controls
public MenuItem()
{
+ // HACK: This nasty but it's all WPF's fault. Grid uses an inherited attached
+ // property to store SharedSizeGroup state, except property inheritance is done
+ // down the logical tree. In this case, the control which is setting
+ // Grid.IsSharedSizeScope="True" is not in the logical tree. Instead of fixing
+ // the way Grid stores shared size state, the developers of WPF just created a
+ // binding of the internal state of the visual parent to the menu item. We don't
+ // have much choice but to do the same for now unless we want to refactor Grid,
+ // which I honestly am not brave enough to do right now. Here's the same hack in
+ // the WPF codebase:
+ //
+ // https://github.com/dotnet/wpf/blob/89537909bdf36bc918e88b37751add46a8980bb0/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/MenuItem.cs#L2126-L2141
+ //
+ // In addition to the hack from WPF, we also make sure to return null when we have
+ // no parent. If we don't do this, inheritance falls back to the logical tree,
+ // causing the shared size scope in the parent MenuItem to be used, breaking
+ // menu layout.
+
+ var parentSharedSizeScope = this.GetObservable(VisualParentProperty)
+ .SelectMany(x =>
+ {
+ var parent = x as Control;
+ return parent?.GetObservable(DefinitionBase.PrivateSharedSizeScopeProperty) ??
+ Observable.Return(null);
+ });
+
+ this.Bind(DefinitionBase.PrivateSharedSizeScopeProperty, parentSharedSizeScope);
}
///
@@ -166,7 +194,7 @@ namespace Avalonia.Controls
///
/// Gets or sets the command associated with the menu item.
///
- public ICommand Command
+ public ICommand? Command
{
get { return _command; }
set { SetAndRaise(CommandProperty, ref _command, value); }
@@ -246,7 +274,7 @@ namespace Avalonia.Controls
bool IMenuItem.IsPointerOverSubMenu => _popup?.IsPointerOverPopup ?? false;
///
- IMenuElement IMenuItem.Parent => Parent as IMenuElement;
+ IMenuElement? IMenuItem.Parent => Parent as IMenuElement;
protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute;
@@ -254,7 +282,7 @@ namespace Avalonia.Controls
bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap);
///
- IMenuItem IMenuElement.SelectedItem
+ IMenuItem? IMenuElement.SelectedItem
{
get
{
@@ -323,32 +351,6 @@ namespace Avalonia.Controls
{
Command.CanExecuteChanged -= CanExecuteChanged;
}
-
- _gridHack?.Dispose();
- _gridHack = null;
- }
-
- protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
- {
- base.OnAttachedToVisualTree(e);
-
- if (this.GetVisualParent() is IControl parent)
- {
- // HACK: This nasty but it's all WPF's fault. Grid uses an inherited attached
- // property to store SharedSizeGroup state, except property inheritance is done
- // down the logical tree. In this case, the control which is setting
- // Grid.IsSharedSizeScope="True" is not in the logical tree. Instead of fixing
- // the way Grid stores shared size state, the developers of WPF just created a
- // binding of the internal state of the visual parent to the menu item. We don't
- // have much choice but to do the same for now unless we want to refactor Grid,
- // which I honestly am not brave enough to do right now. Here's the same hack in
- // the WPF codebase:
- //
- // https://github.com/dotnet/wpf/blob/89537909bdf36bc918e88b37751add46a8980bb0/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/MenuItem.cs#L2126-L2141
- _gridHack = Bind(
- DefinitionBase.PrivateSharedSizeScopeProperty,
- parent.GetBindingObservable(DefinitionBase.PrivateSharedSizeScopeProperty));
- }
}
///
@@ -551,7 +553,7 @@ namespace Avalonia.Controls
/// The property change event.
private void IsSelectedChanged(AvaloniaPropertyChangedEventArgs e)
{
- if ((bool)e.NewValue)
+ if ((bool)e.NewValue!)
{
Focus();
}
@@ -563,7 +565,7 @@ namespace Avalonia.Controls
/// The property change event.
private void SubMenuOpenChanged(AvaloniaPropertyChangedEventArgs e)
{
- var value = (bool)e.NewValue;
+ var value = (bool)e.NewValue!;
if (value)
{
diff --git a/src/Avalonia.Controls/NativeControlHost.cs b/src/Avalonia.Controls/NativeControlHost.cs
new file mode 100644
index 0000000000..20eac11c2c
--- /dev/null
+++ b/src/Avalonia.Controls/NativeControlHost.cs
@@ -0,0 +1,198 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Controls.Platform;
+using Avalonia.Platform;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls
+{
+ public class NativeControlHost : Control
+ {
+ private TopLevel _currentRoot;
+ private INativeControlHostImpl _currentHost;
+ private INativeControlHostControlTopLevelAttachment _attachment;
+ private IPlatformHandle _nativeControlHandle;
+ private bool _queuedForDestruction;
+ private bool _queuedForMoveResize;
+ private readonly List _propertyChangedSubscriptions = new List();
+ private readonly EventHandler _propertyChangedHandler;
+ static NativeControlHost()
+ {
+ IsVisibleProperty.Changed.AddClassHandler(OnVisibleChanged);
+ }
+
+ public NativeControlHost()
+ {
+ _propertyChangedHandler = PropertyChangedHandler;
+ }
+
+ private static void OnVisibleChanged(NativeControlHost host, AvaloniaPropertyChangedEventArgs arg2)
+ => host.UpdateHost();
+
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ _currentRoot = e.Root as TopLevel;
+ var visual = (IVisual)this;
+ while (visual != _currentRoot)
+ {
+
+ if (visual is Visual v)
+ {
+ v.PropertyChanged += _propertyChangedHandler;
+ _propertyChangedSubscriptions.Add(v);
+ }
+
+ visual = visual.GetVisualParent();
+ }
+
+ UpdateHost();
+ }
+
+ private void PropertyChangedHandler(object sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.IsEffectiveValueChange && e.Property == BoundsProperty)
+ EnqueueForMoveResize();
+ }
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ _currentRoot = null;
+ if (_propertyChangedSubscriptions != null)
+ {
+ foreach (var v in _propertyChangedSubscriptions)
+ v.PropertyChanged -= _propertyChangedHandler;
+ _propertyChangedSubscriptions.Clear();
+ }
+ UpdateHost();
+ }
+
+
+ private void UpdateHost()
+ {
+ _queuedForMoveResize = false;
+ _currentHost = (_currentRoot?.PlatformImpl as ITopLevelImplWithNativeControlHost)?.NativeControlHost;
+ var needsAttachment = _currentHost != null;
+
+ if (needsAttachment)
+ {
+ // If there is an existing attachment, ensure that we are attached to the proper host or destroy the attachment
+ if (_attachment != null && _attachment.AttachedTo != _currentHost)
+ {
+ if (_attachment != null)
+ {
+ if (_attachment.IsCompatibleWith(_currentHost))
+ {
+ _attachment.AttachedTo = _currentHost;
+ }
+ else
+ {
+ _attachment.Dispose();
+ _attachment = null;
+ }
+ }
+ }
+
+ // If there is no attachment, but the control exists,
+ // attempt to attach to to the current toplevel or destroy the control if it's incompatible
+ if (_attachment == null && _nativeControlHandle != null)
+ {
+ if (_currentHost.IsCompatibleWith(_nativeControlHandle))
+ _attachment = _currentHost.CreateNewAttachment(_nativeControlHandle);
+ else
+ DestroyNativeControl();
+ }
+
+ // There is no control handle an no attachment, create both
+ if (_nativeControlHandle == null)
+ {
+ _attachment = _currentHost.CreateNewAttachment(parent =>
+ _nativeControlHandle = CreateNativeControlCore(parent));
+ }
+ }
+ else
+ {
+ // Immediately detach the control from the current toplevel if there is an existing attachment
+ if (_attachment != null)
+ _attachment.AttachedTo = null;
+
+ // Don't destroy the control immediately, it might be just being reparented to another TopLevel
+ if (_nativeControlHandle != null && !_queuedForDestruction)
+ {
+ _queuedForDestruction = true;
+ Dispatcher.UIThread.Post(CheckDestruction, DispatcherPriority.Background);
+ }
+ }
+
+ if (_attachment?.AttachedTo != _currentHost)
+ return;
+
+ TryUpdateNativeControlPosition();
+ }
+
+
+ private Rect? GetAbsoluteBounds()
+ {
+ var bounds = Bounds;
+ var position = this.TranslatePoint(bounds.Position, _currentRoot);
+ if (position == null)
+ return null;
+ return new Rect(position.Value, bounds.Size);
+ }
+
+ void EnqueueForMoveResize()
+ {
+ if(_queuedForMoveResize)
+ return;
+ _queuedForMoveResize = true;
+ Dispatcher.UIThread.Post(UpdateHost, DispatcherPriority.Render);
+ }
+
+ public bool TryUpdateNativeControlPosition()
+ {
+ if (_currentHost == null)
+ return false;
+
+ var bounds = GetAbsoluteBounds();
+ var needsShow = IsEffectivelyVisible && bounds.HasValue;
+
+ if (needsShow)
+ _attachment?.ShowInBounds(bounds.Value);
+ else
+ _attachment?.HideWithSize(Bounds.Size);
+ return false;
+ }
+
+ private void CheckDestruction()
+ {
+ _queuedForDestruction = false;
+ if (_currentRoot == null)
+ DestroyNativeControl();
+ }
+
+ protected virtual IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
+ {
+ if (_currentHost == null)
+ throw new InvalidOperationException();
+ return _currentHost.CreateDefaultChild(parent);
+ }
+
+ private void DestroyNativeControl()
+ {
+ if (_nativeControlHandle != null)
+ {
+ _attachment?.Dispose();
+ _attachment = null;
+
+ DestroyNativeControlCore(_nativeControlHandle);
+ _nativeControlHandle = null;
+ }
+ }
+
+ protected virtual void DestroyNativeControlCore(IPlatformHandle control)
+ {
+ ((INativeControlHostDestroyableControlHandle)control).Destroy();
+ }
+
+ }
+}
diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
index e61e88c22b..6d6398bcda 100644
--- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
+++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
@@ -67,6 +67,9 @@ namespace Avalonia.Controls.Platform
window.Deactivated += WindowDeactivated;
}
+ if (_root is TopLevel tl)
+ tl.PlatformImpl.LostFocus += TopLevelLostPlatformFocus;
+
_inputManagerSubscription = InputManager?.Process.Subscribe(RawInput);
}
@@ -96,6 +99,9 @@ namespace Avalonia.Controls.Platform
{
root.Deactivated -= WindowDeactivated;
}
+
+ if (_root is TopLevel tl)
+ tl.PlatformImpl.LostFocus -= TopLevelLostPlatformFocus;
_inputManagerSubscription?.Dispose();
@@ -228,7 +234,9 @@ namespace Avalonia.Controls.Platform
// If the the parent is an IMenu which successfully moved its selection,
// and the current menu is open then close the current menu and open the
// new menu.
- if (item.IsSubMenuOpen && item.Parent is IMenu)
+ if (item.IsSubMenuOpen &&
+ item.Parent is IMenu &&
+ item.Parent.SelectedItem is object)
{
item.Close();
Open(item.Parent.SelectedItem, true);
@@ -333,6 +341,10 @@ namespace Avalonia.Controls.Platform
{
item.Parent.SelectedItem = null;
}
+ else if (!item.IsPointerOverSubMenu)
+ {
+ item.IsSubMenuOpen = false;
+ }
}
}
@@ -352,6 +364,11 @@ namespace Avalonia.Controls.Platform
}
else
{
+ if (item.IsTopLevel && item.Parent is IMainMenu mainMenu)
+ {
+ mainMenu.Open();
+ }
+
Open(item, false);
}
@@ -374,7 +391,7 @@ namespace Avalonia.Controls.Platform
{
if (e.Source == Menu)
{
- Menu.MoveSelection(NavigationDirection.First, true);
+ Menu?.MoveSelection(NavigationDirection.First, true);
}
}
@@ -405,6 +422,11 @@ namespace Avalonia.Controls.Platform
{
Menu?.Close();
}
+
+ private void TopLevelLostPlatformFocus()
+ {
+ Menu?.Close();
+ }
protected void Click(IMenuItem item)
{
diff --git a/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs
new file mode 100644
index 0000000000..de3f58886b
--- /dev/null
+++ b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs
@@ -0,0 +1,38 @@
+using System;
+
+namespace Avalonia.Platform
+{
+ ///
+ /// Hint for Window Chrome when ClientArea is Extended.
+ ///
+ [Flags]
+ public enum ExtendClientAreaChromeHints
+ {
+ ///
+ /// The will be no chrome at all.
+ ///
+ NoChrome,
+
+ ///
+ /// The default for the platform.
+ ///
+ Default = SystemChrome,
+
+ ///
+ /// Use SystemChrome
+ ///
+ SystemChrome = 0x01,
+
+ ///
+ /// Use system chrome where possible. OSX system chrome is used, Windows managed chrome is used.
+ /// This is because Windows Chrome can not be shown ontop of user content.
+ ///
+ PreferSystemChrome = 0x02,
+
+ ///
+ /// On OSX the titlebar is the thicker toolbar kind. Causes traffic lights to be positioned
+ /// slightly lower than normal.
+ ///
+ OSXThickTitleBar = 0x08,
+ }
+}
diff --git a/src/Avalonia.Controls/Platform/IEmbeddableWindowImpl.cs b/src/Avalonia.Controls/Platform/IEmbeddableWindowImpl.cs
deleted file mode 100644
index e2d174d9b6..0000000000
--- a/src/Avalonia.Controls/Platform/IEmbeddableWindowImpl.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System;
-
-namespace Avalonia.Platform
-{
- ///
- /// Defines a platform-specific embeddable window implementation.
- ///
- public interface IEmbeddableWindowImpl : ITopLevelImpl
- {
- event Action LostFocus;
- }
-}
diff --git a/src/Avalonia.Controls/Platform/INativeControlHostImpl.cs b/src/Avalonia.Controls/Platform/INativeControlHostImpl.cs
new file mode 100644
index 0000000000..c6b1d09849
--- /dev/null
+++ b/src/Avalonia.Controls/Platform/INativeControlHostImpl.cs
@@ -0,0 +1,32 @@
+using System;
+using Avalonia.Platform;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls.Platform
+{
+ public interface INativeControlHostImpl
+ {
+ INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent);
+ INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func create);
+ INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle);
+ bool IsCompatibleWith(IPlatformHandle handle);
+ }
+
+ public interface INativeControlHostDestroyableControlHandle : IPlatformHandle
+ {
+ void Destroy();
+ }
+
+ public interface INativeControlHostControlTopLevelAttachment : IDisposable
+ {
+ INativeControlHostImpl AttachedTo { get; set; }
+ bool IsCompatibleWith(INativeControlHostImpl host);
+ void HideWithSize(Size size);
+ void ShowInBounds(Rect rect);
+ }
+
+ public interface ITopLevelImplWithNativeControlHost
+ {
+ INativeControlHostImpl NativeControlHost { get; }
+ }
+}
diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs
index c7875f6413..7514f214aa 100644
--- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs
+++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs
@@ -23,10 +23,10 @@ namespace Avalonia.Platform
Size ClientSize { get; }
///
- /// Gets the scaling factor for the toplevel.
+ /// Gets the scaling factor for the toplevel. This is used for rendering.
///
- double Scaling { get; }
-
+ double RenderScaling { get; }
+
///
/// The list of native platform's surfaces that can be consumed by rendering subsystems.
///
@@ -104,6 +104,11 @@ namespace Avalonia.Platform
/// Gets or sets a method called when the underlying implementation is destroyed.
///
Action Closed { get; set; }
+
+ ///
+ /// Gets or sets a method called when the input focus is lost.
+ ///
+ Action LostFocus { get; set; }
///
/// Gets a mouse device associated with toplevel
@@ -122,5 +127,10 @@ namespace Avalonia.Platform
/// Gets the current of the TopLevel.
///
WindowTransparencyLevel TransparencyLevel { get; }
+
+ ///
+ /// Gets the for the platform.
+ ///
+ AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; }
}
}
diff --git a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
index b190c4f2e7..ecaf87d1ed 100644
--- a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
+++ b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
@@ -13,6 +13,11 @@ namespace Avalonia.Platform
/// Hides the window.
///
void Hide();
+
+ ///
+ /// Gets the scaling factor for Window positioning and sizing.
+ ///
+ double DesktopScaling { get; }
///
/// Gets the position of the window in device pixels.
diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs
index cf31d30332..8a1554d344 100644
--- a/src/Avalonia.Controls/Platform/IWindowImpl.cs
+++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs
@@ -68,6 +68,34 @@ namespace Avalonia.Platform
///
Func Closing { get; set; }
+ ///
+ /// Gets a value to indicate if the platform was able to extend client area to non-client area.
+ ///
+ bool IsClientAreaExtendedToDecorations { get; }
+
+ ///
+ /// Gets or Sets an action that is called whenever one of the extend client area properties changed.
+ ///
+ Action ExtendClientAreaToDecorationsChanged { get; set; }
+
+ ///
+ /// Gets a flag that indicates if Managed decorations i.e. caption buttons are required.
+ /// This property is used when is set.
+ ///
+ bool NeedsManagedDecorations { get; }
+
+ ///
+ /// Gets a thickness that describes the amount each side of the non-client area extends into the client area.
+ /// It includes the titlebar.
+ ///
+ Thickness ExtendedMargins { get; }
+
+ ///
+ /// Gets a thickness that describes the margin around the window that is offscreen.
+ /// This may happen when a window is maximized and is set.
+ ///
+ Thickness OffScreenMargin { get; }
+
///
/// Starts moving a window with left button being held. Should be called from left mouse button press event handler.
///
@@ -94,5 +122,23 @@ namespace Avalonia.Platform
///
///
void SetMinMaxSize(Size minSize, Size maxSize);
+
+ ///
+ /// Sets if the ClientArea is extended into the non-client area.
+ ///
+ /// true to enable, false to disable
+ void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint);
+
+ ///
+ /// Sets hints that configure how the client area extends.
+ ///
+ ///
+ void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints);
+
+ ///
+ /// Sets how big the non-client titlebar area should be.
+ ///
+ /// -1 for platform default, otherwise the height in DIPs.
+ void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight);
}
}
diff --git a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs
index a55bd63c6a..be8939e19a 100644
--- a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs
+++ b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs
@@ -3,6 +3,6 @@ namespace Avalonia.Platform
public interface IWindowingPlatform
{
IWindowImpl CreateWindow();
- IEmbeddableWindowImpl CreateEmbeddableWindow();
+ IWindowImpl CreateEmbeddableWindow();
}
}
diff --git a/src/Avalonia.Controls/Platform/PlatformManager.cs b/src/Avalonia.Controls/Platform/PlatformManager.cs
index ef453274b8..19d034b4e2 100644
--- a/src/Avalonia.Controls/Platform/PlatformManager.cs
+++ b/src/Avalonia.Controls/Platform/PlatformManager.cs
@@ -34,7 +34,7 @@ namespace Avalonia.Controls.Platform
return s_designerMode ? (IWindowImpl)platform.CreateEmbeddableWindow() : platform.CreateWindow();
}
- public static IEmbeddableWindowImpl CreateEmbeddableWindow()
+ public static IWindowImpl CreateEmbeddableWindow()
{
var platform = AvaloniaLocator.Current.GetService();
if (platform == null)
diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs
index 70a7583daf..7888249bdd 100644
--- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs
@@ -155,6 +155,11 @@ namespace Avalonia.Controls.Presenters
}
}
+ protected override void PanelCreated(IPanel panel)
+ {
+ ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+ }
+
///
/// Moves to the selected page, animating if a is set.
///
diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
index f75184c686..8837901816 100644
--- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
@@ -86,7 +86,7 @@ namespace Avalonia.Controls.Presenters
private IControl _child;
private bool _createdChild;
- private IDataTemplate _dataTemplate;
+ private IRecyclingDataTemplate _recyclingDataTemplate;
private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper();
///
@@ -95,6 +95,7 @@ namespace Avalonia.Controls.Presenters
static ContentPresenter()
{
AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty);
+ AffectsArrange(HorizontalContentAlignmentProperty, VerticalContentAlignmentProperty);
AffectsMeasure(BorderThicknessProperty, PaddingProperty);
ContentProperty.Changed.AddClassHandler((x, e) => x.ContentChanged(e));
ContentTemplateProperty.Changed.AddClassHandler((x, e) => x.ContentChanged(e));
@@ -280,7 +281,7 @@ namespace Avalonia.Controls.Presenters
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
- _dataTemplate = null;
+ _recyclingDataTemplate = null;
_createdChild = false;
InvalidateMeasure();
}
@@ -306,22 +307,21 @@ namespace Avalonia.Controls.Presenters
{
var dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default;
- // We have content and it isn't a control, so if the new data template is the same
- // as the old data template, try to recycle the existing child control to display
- // the new data.
- if (dataTemplate == _dataTemplate && dataTemplate.SupportsRecycling)
+ if (dataTemplate is IRecyclingDataTemplate rdt)
{
- newChild = oldChild;
+ var toRecycle = rdt == _recyclingDataTemplate ? oldChild : null;
+ newChild = rdt.Build(content, toRecycle);
+ _recyclingDataTemplate = rdt;
}
else
{
- _dataTemplate = dataTemplate;
- newChild = _dataTemplate.Build(content);
+ newChild = dataTemplate.Build(content);
+ _recyclingDataTemplate = null;
}
}
else
{
- _dataTemplate = null;
+ _recyclingDataTemplate = null;
}
return newChild;
@@ -421,7 +421,7 @@ namespace Avalonia.Controls.Presenters
LogicalChildren.Remove(Child);
((ISetInheritanceParent)Child).SetParent(Child.Parent);
Child = null;
- _dataTemplate = null;
+ _recyclingDataTemplate = null;
}
InvalidateMeasure();
diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
index 23846bcd2e..52f173fc71 100644
--- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
+++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
@@ -229,8 +229,6 @@ namespace Avalonia.Controls.Presenters
}
PanelCreated(Panel);
-
- ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
///
diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs
index 09f86f462c..37490c3ef3 100644
--- a/src/Avalonia.Controls/Presenters/TextPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs
@@ -14,6 +14,9 @@ namespace Avalonia.Controls.Presenters
o => o.CaretIndex,
(o, v) => o.CaretIndex = v);
+ public static readonly StyledProperty RevealPasswordProperty =
+ AvaloniaProperty.Register(nameof(RevealPassword));
+
public static readonly StyledProperty PasswordCharProperty =
AvaloniaProperty.Register(nameof(PasswordChar));
@@ -75,7 +78,7 @@ namespace Avalonia.Controls.Presenters
static TextPresenter()
{
AffectsRender(SelectionBrushProperty);
- AffectsMeasure(TextProperty, PasswordCharProperty,
+ AffectsMeasure(TextProperty, PasswordCharProperty, RevealPasswordProperty,
TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty,
TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty);
@@ -84,7 +87,7 @@ namespace Avalonia.Controls.Presenters
TextBlock.FontSizeProperty.Changed, TextBlock.FontStyleProperty.Changed,
TextBlock.FontWeightProperty.Changed, TextBlock.FontFamilyProperty.Changed,
SelectionStartProperty.Changed, SelectionEndProperty.Changed,
- SelectionForegroundBrushProperty.Changed, PasswordCharProperty.Changed
+ SelectionForegroundBrushProperty.Changed, PasswordCharProperty.Changed, RevealPasswordProperty.Changed
).AddClassHandler((x, _) => x.InvalidateFormattedText());
CaretIndexProperty.Changed.AddClassHandler((x, e) => x.CaretIndexChanged((int)e.NewValue));
@@ -210,6 +213,12 @@ namespace Avalonia.Controls.Presenters
set => SetValue(PasswordCharProperty, value);
}
+ public bool RevealPassword
+ {
+ get => GetValue(RevealPasswordProperty);
+ set => SetValue(RevealPasswordProperty, value);
+ }
+
public IBrush SelectionBrush
{
get => GetValue(SelectionBrushProperty);
@@ -273,7 +282,7 @@ namespace Avalonia.Controls.Presenters
return new FormattedText
{
Constraint = constraint,
- Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle),
+ Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight),
FontSize = FontSize,
Text = text ?? string.Empty,
TextAlignment = TextAlignment,
@@ -426,7 +435,7 @@ namespace Avalonia.Controls.Presenters
var text = Text;
- if (PasswordChar != default(char))
+ if (PasswordChar != default(char) && !RevealPassword)
{
result = CreateFormattedTextInternal(_constraint, new string(PasswordChar, text?.Length ?? 0));
}
@@ -490,7 +499,7 @@ namespace Avalonia.Controls.Presenters
return new FormattedText
{
Text = "X",
- Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle),
+ Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight),
FontSize = FontSize,
TextAlignment = TextAlignment,
Constraint = availableSize,
diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs
index 4208a2f2f7..89f672deaa 100644
--- a/src/Avalonia.Controls/Primitives/AccessText.cs
+++ b/src/Avalonia.Controls/Primitives/AccessText.cs
@@ -97,9 +97,7 @@ namespace Avalonia.Controls.Primitives
{
var lastLine = TextLayout.TextLines[TextLayout.TextLines.Count - 1];
- var offsetX = lastLine.LineMetrics.BaselineOrigin.X;
-
- var lineX = offsetX + lastLine.LineMetrics.Size.Width;
+ var lineX = lastLine.LineMetrics.Size.Width;
var lineY = Bounds.Height - lastLine.LineMetrics.Size.Height;
@@ -110,32 +108,33 @@ namespace Avalonia.Controls.Primitives
foreach (var textLine in TextLayout.TextLines)
{
- if (textLine.Text.End < textPosition)
+ if (textLine.TextRange.End < textPosition)
{
currentY += textLine.LineMetrics.Size.Height;
continue;
}
- var currentX = textLine.LineMetrics.BaselineOrigin.X;
+ var currentX = 0.0;
foreach (var textRun in textLine.TextRuns)
{
- if (!(textRun is ShapedTextRun shapedRun))
+ if (!(textRun is ShapedTextCharacters shapedTextCharacters))
{
continue;
}
- if (shapedRun.GlyphRun.Characters.End < textPosition)
+ if (shapedTextCharacters.GlyphRun.Characters.End < textPosition)
{
- currentX += shapedRun.GlyphRun.Bounds.Width;
+ currentX += shapedTextCharacters.GlyphRun.Bounds.Width;
continue;
}
- var characterHit = shapedRun.GlyphRun.FindNearestCharacterHit(textPosition, out var width);
+ var characterHit =
+ shapedTextCharacters.GlyphRun.FindNearestCharacterHit(textPosition, out var width);
- var distance = shapedRun.GlyphRun.GetDistanceFromCharacterHit(characterHit);
+ var distance = shapedTextCharacters.GlyphRun.GetDistanceFromCharacterHit(characterHit);
currentX += distance - width;
@@ -144,7 +143,7 @@ namespace Avalonia.Controls.Primitives
width = 0.0;
}
- return new Rect(currentX, currentY, width, shapedRun.GlyphRun.Bounds.Height);
+ return new Rect(currentX, currentY, width, shapedTextCharacters.GlyphRun.Bounds.Height);
}
}
diff --git a/src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs b/src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs
new file mode 100644
index 0000000000..7171ecc302
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs
@@ -0,0 +1,34 @@
+using System.Linq;
+using Avalonia.Rendering;
+using Avalonia.VisualTree;
+
+#nullable enable
+
+namespace Avalonia.Controls.Primitives
+{
+ public class ChromeOverlayLayer : Panel, ICustomSimpleHitTest
+ {
+ public static Panel? GetOverlayLayer(IVisual visual)
+ {
+ foreach (var v in visual.GetVisualAncestors())
+ if (v is VisualLayerManager vlm)
+ if (vlm.OverlayLayer != null)
+ return vlm.ChromeOverlayLayer;
+
+ if (visual is TopLevel tl)
+ {
+ var layers = tl.GetVisualDescendants().OfType().FirstOrDefault();
+ return layers?.ChromeOverlayLayer;
+ }
+
+ return null;
+ }
+
+ public void Add(Control c)
+ {
+ base.Children.Add(c);
+ }
+
+ public bool HitTest(Point point) => Children.HitTestCustom(point);
+ }
+}
diff --git a/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs b/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs
new file mode 100644
index 0000000000..752eedb68a
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Linq;
+using Avalonia.Controls.Templates;
+using Avalonia.Input;
+using Avalonia.Rendering;
+using Avalonia.Styling;
+using Avalonia.VisualTree;
+
+#nullable enable
+
+namespace Avalonia.Controls.Primitives
+{
+ ///
+ /// A layer that is used to dismiss a when the user clicks outside.
+ ///
+ public class LightDismissOverlayLayer : Border, ICustomHitTest
+ {
+ public IInputElement? InputPassThroughElement { get; set; }
+
+ ///
+ /// Returns the light dismiss overlay for a specified visual.
+ ///
+ /// The visual.
+ /// The light dismiss overlay, or null if none found.
+ public static LightDismissOverlayLayer? GetLightDismissOverlayLayer(IVisual visual)
+ {
+ visual = visual ?? throw new ArgumentNullException(nameof(visual));
+
+ VisualLayerManager? manager;
+
+ if (visual is TopLevel topLevel)
+ {
+ manager = topLevel.GetTemplateChildren()
+ .OfType()
+ .FirstOrDefault();
+ }
+ else
+ {
+ manager = visual.FindAncestorOfType();
+ }
+
+ return manager?.LightDismissOverlayLayer;
+ }
+
+ public bool HitTest(Point point)
+ {
+ if (InputPassThroughElement is object)
+ {
+ var p = point.Transform(this.TransformToVisual(VisualRoot)!.Value);
+ var hit = VisualRoot.GetVisualAt(p, x => x != this);
+
+ if (hit is object)
+ {
+ return !InputPassThroughElement.IsVisualAncestorOf(hit);
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs
index ac4f805174..a676892384 100644
--- a/src/Avalonia.Controls/Primitives/Popup.cs
+++ b/src/Avalonia.Controls/Primitives/Popup.cs
@@ -1,14 +1,13 @@
using System;
-using System.Diagnostics;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Input.Raw;
-using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
+using Avalonia.Platform;
using Avalonia.VisualTree;
#nullable enable
@@ -85,12 +84,27 @@ namespace Avalonia.Controls.Primitives
AvaloniaProperty.Register(nameof(ObeyScreenEdges), true);
#pragma warning restore 618
+ public static readonly StyledProperty OverlayDismissEventPassThroughProperty =
+ AvaloniaProperty.Register(nameof(OverlayDismissEventPassThrough));
+
+ public static readonly DirectProperty OverlayInputPassThroughElementProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(OverlayInputPassThroughElement),
+ o => o.OverlayInputPassThroughElement,
+ (o, v) => o.OverlayInputPassThroughElement = v);
+
///
/// Defines the property.
///
public static readonly StyledProperty HorizontalOffsetProperty =
AvaloniaProperty.Register(nameof(HorizontalOffset));
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsLightDismissEnabledProperty =
+ AvaloniaProperty.Register(nameof(IsLightDismissEnabled));
+
///
/// Defines the property.
///
@@ -100,8 +114,13 @@ namespace Avalonia.Controls.Primitives
///
/// Defines the property.
///
- public static readonly StyledProperty StaysOpenProperty =
- AvaloniaProperty.Register(nameof(StaysOpen), true);
+ [Obsolete("Use IsLightDismissEnabledProperty")]
+ public static readonly DirectProperty StaysOpenProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(StaysOpen),
+ o => o.StaysOpen,
+ (o, v) => o.StaysOpen = v,
+ true);
///
/// Defines the property.
@@ -112,6 +131,7 @@ namespace Avalonia.Controls.Primitives
private bool _isOpen;
private bool _ignoreIsOpenChanged;
private PopupOpenState? _openState;
+ private IInputElement _overlayInputPassThroughElement;
///
/// Initializes static members of the class.
@@ -126,7 +146,7 @@ namespace Avalonia.Controls.Primitives
///
/// Raised when the popup closes.
///
- public event EventHandler? Closed;
+ public event EventHandler? Closed;
///
/// Raised when the popup opens.
@@ -164,6 +184,18 @@ namespace Avalonia.Controls.Primitives
set;
}
+ ///
+ /// Gets or sets a value that determines how the can be dismissed.
+ ///
+ ///
+ /// Light dismiss is when the user taps on any area other than the popup.
+ ///
+ public bool IsLightDismissEnabled
+ {
+ get => GetValue(IsLightDismissEnabledProperty);
+ set => SetValue(IsLightDismissEnabledProperty, value);
+ }
+
///
/// Gets or sets a value indicating whether the popup is currently open.
///
@@ -245,6 +277,32 @@ namespace Avalonia.Controls.Primitives
set => SetValue(ObeyScreenEdgesProperty, value);
}
+ ///
+ /// Gets or sets a value indicating whether the event that closes the popup is passed
+ /// through to the parent window.
+ ///
+ ///
+ /// When is set to true, clicks outside the the popup
+ /// cause the popup to close. When is set to
+ /// false, these clicks will be handled by the popup and not be registered by the parent
+ /// window. When set to true, the events will be passed through to the parent window.
+ ///
+ public bool OverlayDismissEventPassThrough
+ {
+ get => GetValue(OverlayDismissEventPassThroughProperty);
+ set => SetValue(OverlayDismissEventPassThroughProperty, value);
+ }
+
+ ///
+ /// Gets or sets an element that should receive pointer input events even when underneath
+ /// the popup's overlay.
+ ///
+ public IInputElement OverlayInputPassThroughElement
+ {
+ get => _overlayInputPassThroughElement;
+ set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value);
+ }
+
///
/// Gets or sets the Horizontal offset of the popup in relation to the .
///
@@ -267,10 +325,11 @@ namespace Avalonia.Controls.Primitives
/// Gets or sets a value indicating whether the popup should stay open when the popup is
/// pressed or loses focus.
///
+ [Obsolete("Use IsLightDismissEnabled")]
public bool StaysOpen
{
- get { return GetValue(StaysOpenProperty); }
- set { SetValue(StaysOpenProperty, value); }
+ get => !IsLightDismissEnabled;
+ set => IsLightDismissEnabled = !value;
}
///
@@ -351,6 +410,10 @@ namespace Avalonia.Controls.Primitives
DeferCleanup(SubscribeToEventHandler(window, WindowDeactivated,
(x, handler) => x.Deactivated += handler,
(x, handler) => x.Deactivated -= handler));
+
+ DeferCleanup(SubscribeToEventHandler(window.PlatformImpl, WindowLostFocus,
+ (x, handler) => x.LostFocus += handler,
+ (x, handler) => x.LostFocus -= handler));
}
else
{
@@ -358,14 +421,12 @@ namespace Avalonia.Controls.Primitives
if (parentPopupRoot?.Parent is Popup popup)
{
- DeferCleanup(SubscribeToEventHandler>(popup, ParentClosed,
+ DeferCleanup(SubscribeToEventHandler>(popup, ParentClosed,
(x, handler) => x.Closed += handler,
(x, handler) => x.Closed -= handler));
}
}
- DeferCleanup(topLevel.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel));
-
DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick));
var cleanupPopup = Disposable.Create((popupHost, handlerCleanup), state =>
@@ -379,6 +440,29 @@ namespace Avalonia.Controls.Primitives
state.popupHost.Dispose();
});
+ if (IsLightDismissEnabled)
+ {
+ var dismissLayer = LightDismissOverlayLayer.GetLightDismissOverlayLayer(placementTarget);
+
+ if (dismissLayer != null)
+ {
+ dismissLayer.IsVisible = true;
+ dismissLayer.InputPassThroughElement = _overlayInputPassThroughElement;
+
+ DeferCleanup(Disposable.Create(() =>
+ {
+ dismissLayer.IsVisible = false;
+ dismissLayer.InputPassThroughElement = null;
+ }));
+
+ DeferCleanup(SubscribeToEventHandler>(
+ dismissLayer,
+ PointerPressedDismissOverlay,
+ (x, handler) => x.PointerPressed += handler,
+ (x, handler) => x.PointerPressed -= handler));
+ }
+ }
+
_openState = new PopupOpenState(topLevel, popupHost, cleanupPopup);
WindowManagerAddShadowHintChanged(popupHost, WindowManagerAddShadowHint);
@@ -396,7 +480,7 @@ namespace Avalonia.Controls.Primitives
///
/// Closes the popup.
///
- public void Close() => CloseCore(null);
+ public void Close() => CloseCore();
///
/// Measures the control.
@@ -466,7 +550,7 @@ namespace Avalonia.Controls.Primitives
}
}
- private void CloseCore(EventArgs? closeEvent)
+ private void CloseCore()
{
if (_openState is null)
{
@@ -486,24 +570,46 @@ namespace Avalonia.Controls.Primitives
IsOpen = false;
}
- Closed?.Invoke(this, new PopupClosedEventArgs(closeEvent));
+ Closed?.Invoke(this, EventArgs.Empty);
}
private void ListenForNonClientClick(RawInputEventArgs e)
{
var mouse = e as RawPointerEventArgs;
- if (!StaysOpen && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
+ if (IsLightDismissEnabled && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
+ {
+ CloseCore();
+ }
+ }
+
+ private void PointerPressedDismissOverlay(object sender, PointerPressedEventArgs e)
+ {
+ if (IsLightDismissEnabled && e.Source is IVisual v && !IsChildOrThis(v))
{
- CloseCore(e);
+ CloseCore();
+
+ if (OverlayDismissEventPassThrough)
+ {
+ PassThroughEvent(e);
+ }
}
}
- private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
+ private void PassThroughEvent(PointerPressedEventArgs e)
{
- if (!StaysOpen && e.Source is IVisual v && !IsChildOrThis(v))
+ if (e.Source is LightDismissOverlayLayer layer &&
+ layer.GetVisualRoot() is IInputElement root)
{
- CloseCore(e);
+ var p = e.GetCurrentPoint(root);
+ var hit = root.InputHitTest(p.Position, x => x != layer);
+
+ if (hit != null)
+ {
+ e.Pointer.Capture(hit);
+ hit.RaiseEvent(e);
+ e.Handled = true;
+ }
}
}
@@ -597,7 +703,7 @@ namespace Avalonia.Controls.Primitives
private void WindowDeactivated(object sender, EventArgs e)
{
- if (!StaysOpen)
+ if (IsLightDismissEnabled)
{
Close();
}
@@ -605,11 +711,17 @@ namespace Avalonia.Controls.Primitives
private void ParentClosed(object sender, EventArgs e)
{
- if (!StaysOpen)
+ if (IsLightDismissEnabled)
{
Close();
}
}
+
+ private void WindowLostFocus()
+ {
+ if (IsLightDismissEnabled)
+ Close();
+ }
private IgnoreIsOpenScope BeginIgnoringIsOpen()
{
diff --git a/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs b/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs
deleted file mode 100644
index c51543438c..0000000000
--- a/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using System;
-using Avalonia.Interactivity;
-
-#nullable enable
-
-namespace Avalonia.Controls.Primitives
-{
- ///
- /// Holds data for the event.
- ///
- public class PopupClosedEventArgs : EventArgs
- {
- ///
- /// Initializes a new instance of the class.
- ///
- ///
- public PopupClosedEventArgs(EventArgs? closeEvent)
- {
- CloseEvent = closeEvent;
- }
-
- ///
- /// Gets the event that closed the popup, if any.
- ///
- ///
- /// If is false, then this property will hold details of the
- /// interaction that caused the popup to close if the close was caused by e.g. a pointer press
- /// outside the popup. It can be used to mark the event as handled if the event should not
- /// be propagated.
- ///
- public EventArgs? CloseEvent { get; }
- }
-}
diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs
index b0e3d1ab08..91ed5d975d 100644
--- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs
+++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs
@@ -40,9 +40,9 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
public void MoveAndResize(Point devicePoint, Size virtualSize)
{
- _moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _parent.Scaling);
+ _moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _parent.RenderScaling);
}
- public virtual double Scaling => _parent.Scaling;
+ public virtual double Scaling => _parent.DesktopScaling;
}
}
diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
index c915dc70b6..59b7777b1b 100644
--- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
+++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
@@ -692,14 +692,24 @@ namespace Avalonia.Controls.Primitives
}
}
- foreach (var i in e.SelectedIndices)
+ if (e.SelectedIndices.Count > 0 || e.DeselectedIndices.Count > 0)
{
- Mark(i.GetAt(0), true);
- }
+ foreach (var i in e.SelectedIndices)
+ {
+ Mark(i.GetAt(0), true);
+ }
- foreach (var i in e.DeselectedIndices)
+ foreach (var i in e.DeselectedIndices)
+ {
+ Mark(i.GetAt(0), false);
+ }
+ }
+ else if (e.DeselectedItems.Count > 0)
{
- Mark(i.GetAt(0), false);
+ // (De)selected indices being empty means that a selected item was removed from
+ // the Items (it can't tell us the index of the item because the index is no longer
+ // valid). In this case, we just update the selection state of all containers.
+ UpdateContainerSelection();
}
var newSelectedIndex = SelectedIndex;
diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs
index c91adaa26e..29e7f28b44 100644
--- a/src/Avalonia.Controls/Primitives/Track.cs
+++ b/src/Avalonia.Controls/Primitives/Track.cs
@@ -353,6 +353,15 @@ namespace Avalonia.Controls.Primitives
var trackLength = isVertical ? arrangeSize.Height : arrangeSize.Width;
double thumbMinLength = 10;
+ StyledProperty minLengthProperty = isVertical ? MinHeightProperty : MinWidthProperty;
+
+ var thumb = Thumb;
+
+ if (thumb != null && thumb.IsSet(minLengthProperty))
+ {
+ thumbMinLength = thumb.GetValue(minLengthProperty);
+ }
+
thumbLength = trackLength * viewportSize / extent;
CoerceLength(ref thumbLength, trackLength);
thumbLength = Math.Max(thumbMinLength, thumbLength);
diff --git a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs
index 86aeb5c62a..d8d3450c6f 100644
--- a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs
+++ b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs
@@ -1,18 +1,24 @@
using System.Collections.Generic;
using Avalonia.LogicalTree;
+using Avalonia.Media;
namespace Avalonia.Controls.Primitives
{
public class VisualLayerManager : Decorator
{
private const int AdornerZIndex = int.MaxValue - 100;
- private const int OverlayZIndex = int.MaxValue - 99;
+ private const int ChromeZIndex = int.MaxValue - 99;
+ private const int LightDismissOverlayZIndex = int.MaxValue - 98;
+ private const int OverlayZIndex = int.MaxValue - 97;
+
private ILogicalRoot _logicalRoot;
private readonly List _layers = new List();
-
+
+ public static readonly StyledProperty ChromeOverlayLayerProperty =
+ AvaloniaProperty.Register(nameof(ChromeOverlayLayer));
public bool IsPopup { get; set; }
-
+
public AdornerLayer AdornerLayer
{
get
@@ -24,6 +30,26 @@ namespace Avalonia.Controls.Primitives
}
}
+ public ChromeOverlayLayer ChromeOverlayLayer
+ {
+ get
+ {
+ var current = GetValue(ChromeOverlayLayerProperty);
+
+ if (current is null)
+ {
+ var chromeOverlayLayer = new ChromeOverlayLayer();
+ AddLayer(chromeOverlayLayer, ChromeZIndex);
+
+ SetValue(ChromeOverlayLayerProperty, chromeOverlayLayer);
+
+ current = chromeOverlayLayer;
+ }
+
+ return current;
+ }
+ }
+
public OverlayLayer OverlayLayer
{
get
@@ -31,12 +57,33 @@ namespace Avalonia.Controls.Primitives
if (IsPopup)
return null;
var rv = FindLayer();
- if(rv == null)
+ if (rv == null)
AddLayer(rv = new OverlayLayer(), OverlayZIndex);
return rv;
}
}
+ public LightDismissOverlayLayer LightDismissOverlayLayer
+ {
+ get
+ {
+ if (IsPopup)
+ return null;
+ var rv = FindLayer();
+ if (rv == null)
+ {
+ rv = new LightDismissOverlayLayer
+ {
+ Background = Brushes.Transparent,
+ IsVisible = false
+ };
+
+ AddLayer(rv, LightDismissOverlayZIndex);
+ }
+ return rv;
+ }
+ }
+
T FindLayer() where T : class
{
foreach (var layer in _layers)
@@ -52,7 +99,8 @@ namespace Avalonia.Controls.Primitives
layer.ZIndex = zindex;
VisualChildren.Add(layer);
if (((ILogical)this).IsAttachedToLogicalTree)
- ((ILogical)layer).NotifyAttachedToLogicalTree(new LogicalTreeAttachmentEventArgs(_logicalRoot, layer, this));
+ ((ILogical)layer).NotifyAttachedToLogicalTree(
+ new LogicalTreeAttachmentEventArgs(_logicalRoot, layer, this));
InvalidateArrange();
}
diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs
index c7356f2b4d..a92f24a050 100644
--- a/src/Avalonia.Controls/ProgressBar.cs
+++ b/src/Avalonia.Controls/ProgressBar.cs
@@ -1,8 +1,7 @@
-
using System;
using Avalonia.Controls.Primitives;
-using Avalonia.Data;
using Avalonia.Layout;
+using Avalonia.Media;
namespace Avalonia.Controls
{
@@ -11,6 +10,92 @@ namespace Avalonia.Controls
///
public class ProgressBar : RangeBase
{
+ public class ProgressBarTemplateProperties : AvaloniaObject
+ {
+ private double _container2Width;
+ private double _containerWidth;
+ private double _containerAnimationStartPosition;
+ private double _containerAnimationEndPosition;
+ private double _container2AnimationStartPosition;
+ private double _container2AnimationEndPosition;
+
+ public static readonly DirectProperty ContainerAnimationStartPositionProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(ContainerAnimationStartPosition),
+ p => p.ContainerAnimationStartPosition,
+ (p, o) => p.ContainerAnimationStartPosition = o, 0d);
+
+ public static readonly DirectProperty ContainerAnimationEndPositionProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(ContainerAnimationEndPosition),
+ p => p.ContainerAnimationEndPosition,
+ (p, o) => p.ContainerAnimationEndPosition = o, 0d);
+
+ public static readonly DirectProperty Container2AnimationStartPositionProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(Container2AnimationStartPosition),
+ p => p.Container2AnimationStartPosition,
+ (p, o) => p.Container2AnimationStartPosition = o, 0d);
+
+ public static readonly DirectProperty Container2AnimationEndPositionProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(Container2AnimationEndPosition),
+ p => p.Container2AnimationEndPosition,
+ (p, o) => p.Container2AnimationEndPosition = o);
+
+ public static readonly DirectProperty Container2WidthProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(Container2Width),
+ p => p.Container2Width,
+ (p, o) => p.Container2Width = o);
+
+ public static readonly DirectProperty ContainerWidthProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(ContainerWidth),
+ p => p.ContainerWidth,
+ (p, o) => p.ContainerWidth = o);
+
+ public double ContainerAnimationStartPosition
+ {
+ get => _containerAnimationStartPosition;
+ set => SetAndRaise(ContainerAnimationStartPositionProperty, ref _containerAnimationStartPosition, value);
+ }
+
+ public double ContainerAnimationEndPosition
+ {
+ get => _containerAnimationEndPosition;
+ set => SetAndRaise(ContainerAnimationEndPositionProperty, ref _containerAnimationEndPosition, value);
+ }
+
+ public double Container2AnimationStartPosition
+ {
+ get => _container2AnimationStartPosition;
+ set => SetAndRaise(Container2AnimationStartPositionProperty, ref _container2AnimationStartPosition, value);
+ }
+
+ public double Container2Width
+ {
+ get => _container2Width;
+ set => SetAndRaise(Container2WidthProperty, ref _container2Width, value);
+ }
+
+ public double ContainerWidth
+ {
+ get => _containerWidth;
+ set => SetAndRaise(ContainerWidthProperty, ref _containerWidth, value);
+ }
+
+ public double Container2AnimationEndPosition
+ {
+ get => _container2AnimationEndPosition;
+ set => SetAndRaise(Container2AnimationEndPositionProperty, ref _container2AnimationEndPosition, value);
+ }
+ }
+
+ private double _indeterminateStartingOffset;
+ private double _indeterminateEndingOffset;
+ private Border _indicator;
+
public static readonly StyledProperty IsIndeterminateProperty =
AvaloniaProperty.Register(nameof(IsIndeterminate));
@@ -20,19 +105,33 @@ namespace Avalonia.Controls
public static readonly StyledProperty OrientationProperty =
AvaloniaProperty.Register(nameof(Orientation), Orientation.Horizontal);
- private static readonly DirectProperty IndeterminateStartingOffsetProperty =
+ [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")]
+ public static readonly DirectProperty IndeterminateStartingOffsetProperty =
AvaloniaProperty.RegisterDirect(
nameof(IndeterminateStartingOffset),
p => p.IndeterminateStartingOffset,
(p, o) => p.IndeterminateStartingOffset = o);
- private static readonly DirectProperty IndeterminateEndingOffsetProperty =
+ [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")]
+ public static readonly DirectProperty IndeterminateEndingOffsetProperty =
AvaloniaProperty.RegisterDirect(
nameof(IndeterminateEndingOffset),
p => p.IndeterminateEndingOffset,
(p, o) => p.IndeterminateEndingOffset = o);
- private Border _indicator;
+ [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")]
+ public double IndeterminateStartingOffset
+ {
+ get => _indeterminateStartingOffset;
+ set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value);
+ }
+
+ [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")]
+ public double IndeterminateEndingOffset
+ {
+ get => _indeterminateEndingOffset;
+ set => SetAndRaise(IndeterminateEndingOffsetProperty, ref _indeterminateEndingOffset, value);
+ }
static ProgressBar()
{
@@ -45,6 +144,8 @@ namespace Avalonia.Controls
UpdatePseudoClasses(IsIndeterminate, Orientation);
}
+ public ProgressBarTemplateProperties TemplateProperties { get; } = new ProgressBarTemplateProperties();
+
public bool IsIndeterminate
{
get => GetValue(IsIndeterminateProperty);
@@ -62,19 +163,6 @@ namespace Avalonia.Controls
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
- private double _indeterminateStartingOffset;
- private double IndeterminateStartingOffset
- {
- get => _indeterminateStartingOffset;
- set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value);
- }
-
- private double _indeterminateEndingOffset;
- private double IndeterminateEndingOffset
- {
- get => _indeterminateEndingOffset;
- set => SetAndRaise(IndeterminateEndingOffsetProperty, ref _indeterminateEndingOffset, value);
- }
///
protected override Size ArrangeOverride(Size finalSize)
@@ -111,21 +199,33 @@ namespace Avalonia.Controls
{
if (IsIndeterminate)
{
- if (Orientation == Orientation.Horizontal)
- {
- var width = bounds.Width / 5.0;
- IndeterminateStartingOffset = -width;
- _indicator.Width = width;
- IndeterminateEndingOffset = bounds.Width;
+ // Pulled from ModernWPF.
- }
- else
- {
- var height = bounds.Height / 5.0;
- IndeterminateStartingOffset = -bounds.Height;
- _indicator.Height = height;
- IndeterminateEndingOffset = height;
- }
+ var dim = Orientation == Orientation.Horizontal ? bounds.Width : bounds.Height;
+ var barIndicatorWidth = dim * 0.4; // Indicator width at 40% of ProgressBar
+ var barIndicatorWidth2 = dim * 0.6; // Indicator width at 60% of ProgressBar
+
+ TemplateProperties.ContainerWidth = barIndicatorWidth;
+ TemplateProperties.Container2Width = barIndicatorWidth2;
+
+ TemplateProperties.ContainerAnimationStartPosition = barIndicatorWidth * -1.8; // Position at -180%
+ TemplateProperties.ContainerAnimationEndPosition = barIndicatorWidth * 3.0; // Position at 300%
+
+ TemplateProperties.Container2AnimationStartPosition = barIndicatorWidth2 * -1.5; // Position at -150%
+ TemplateProperties.Container2AnimationEndPosition = barIndicatorWidth2 * 1.66; // Position at 166%
+
+ // Remove these properties when we switch to fluent as default and removed the old one.
+ IndeterminateStartingOffset = -dim;
+ IndeterminateEndingOffset = dim;
+
+ var padding = Padding;
+ var rectangle = new RectangleGeometry(
+ new Rect(
+ padding.Left,
+ padding.Top,
+ bounds.Width - (padding.Right + padding.Left),
+ bounds.Height - (padding.Bottom + padding.Top)
+ ));
}
else
{
diff --git a/src/Avalonia.Controls/Properties/AssemblyInfo.cs b/src/Avalonia.Controls/Properties/AssemblyInfo.cs
index 060db46212..672fbe294e 100644
--- a/src/Avalonia.Controls/Properties/AssemblyInfo.cs
+++ b/src/Avalonia.Controls/Properties/AssemblyInfo.cs
@@ -13,3 +13,4 @@ using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Shapes")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Templates")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Notifications")]
+[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Chrome")]
diff --git a/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs b/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs
new file mode 100644
index 0000000000..f64c26682b
--- /dev/null
+++ b/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs
@@ -0,0 +1,546 @@
+using Avalonia.Layout;
+
+#nullable enable
+
+namespace Avalonia.Controls
+{
+ public partial class RelativePanel
+ {
+ private static void OnAlignPropertiesChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (d is Layoutable layoutable && layoutable.Parent is Layoutable layoutableParent)
+ {
+ layoutableParent.InvalidateArrange();
+ }
+ }
+
+ static RelativePanel()
+ {
+ ClipToBoundsProperty.OverrideDefaultValue(true);
+
+ AboveProperty.Changed.AddClassHandler(OnAlignPropertiesChanged);
+ AlignBottomWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged);
+ AlignBottomWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged);
+ AlignHorizontalCenterWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged);
+ AlignHorizontalCenterWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged);
+ AlignLeftWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged);
+ AlignLeftWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged);
+ AlignRightWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged);
+ AlignRightWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged);
+ AlignTopWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged);
+ AlignTopWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged);
+ AlignVerticalCenterWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged);
+ AlignVerticalCenterWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged);
+ BelowProperty.Changed.AddClassHandler(OnAlignPropertiesChanged);
+ LeftOfProperty.Changed.AddClassHandler(OnAlignPropertiesChanged);
+ RightOfProperty.Changed.AddClassHandler(OnAlignPropertiesChanged);
+ }
+
+ ///
+ /// Gets the value of the RelativePanel.Above XAML attached property for the target element.
+ ///
+ /// The object from which the property value is read.
+ ///
+ /// The RelativePanel.Above XAML attached property value of the specified object.
+ /// (The element to position this element above.)
+ ///
+ [ResolveByName]
+ public static object GetAbove(AvaloniaObject obj)
+ {
+ return (object)obj.GetValue(AboveProperty);
+ }
+
+ ///
+ /// Sets the value of the RelativePanel.Above XAML attached property for a target element.
+ ///
+ /// The object to which the property value is written.
+ /// The value to set. (The element to position this element above.)
+ public static void SetAbove(AvaloniaObject obj, object value)
+ {
+ obj.SetValue(AboveProperty, value);
+ }
+
+
+ ///
+ /// Identifies the XAML attached property.
+ ///
+
+ public static readonly AttachedProperty AboveProperty =
+ AvaloniaProperty.RegisterAttached("Above", typeof(RelativePanel));
+
+
+ ///
+ /// Gets the value of the RelativePanel.AlignBottomWithPanel XAML attached property for the target element.
+ ///
+ /// The object from which the property value is read.
+ ///
+ /// The RelativePanel.AlignBottomWithPanel XAML attached property value of the specified
+ /// object. (true to align this element's bottom edge with the panel's bottom edge;
+ /// otherwise, false.)
+ ///
+ public static bool GetAlignBottomWithPanel(AvaloniaObject obj)
+ {
+ return (bool)obj.GetValue(AlignBottomWithPanelProperty);
+ }
+
+ ///
+ /// Sets the value of the RelativePanel.Above XAML attached property for a target element.
+ ///
+ /// The object to which the property value is written.
+ ///
+ /// The value to set. (true to align this element's bottom edge with the panel's
+ /// bottom edge; otherwise, false.)
+ ///
+ public static void SetAlignBottomWithPanel(AvaloniaObject obj, bool value)
+ {
+ obj.SetValue(AlignBottomWithPanelProperty, value);
+ }
+
+ ///
+ /// Identifies the XAML attached property.
+ ///
+ public static readonly AttachedProperty AlignBottomWithPanelProperty =
+ AvaloniaProperty.RegisterAttached("AlignBottomWithPanel", typeof(RelativePanel));
+
+ ///
+ /// Gets the value of the RelativePanel.AlignBottomWith XAML attached property for the target element.
+ ///
+ /// The object from which the property value is read.
+ ///
+ /// The RelativePanel.AlignBottomWith XAML attached property value of the specified object.
+ /// (The element to align this element's bottom edge with.)
+ ///
+ [ResolveByName]
+ public static object GetAlignBottomWith(AvaloniaObject obj)
+ {
+ return (object)obj.GetValue(AlignBottomWithProperty);
+ }
+
+ ///
+ /// Sets the value of the RelativePanel.Above XAML attached property for a target element.
+ ///
+ /// The object to which the property value is written.
+ /// The value to set. (The element to align this element's bottom edge with.)
+ public static void SetAlignBottomWith(AvaloniaObject obj, object value)
+ {
+ obj.SetValue(AlignBottomWithProperty, value);
+ }
+
+ ///
+ /// Identifies the XAML attached property.
+ ///
+
+ public static readonly AttachedProperty AlignBottomWithProperty =
+ AvaloniaProperty.RegisterAttached("AlignBottomWith", typeof(RelativePanel));
+
+ ///
+ /// Gets the value of the RelativePanel.AlignHorizontalCenterWithPanel XAML attached property for the target element.
+ ///
+ /// The object from which the property value is read.
+ ///
+ /// The RelativePanel.AlignHorizontalCenterWithPanel XAML attached property value
+ /// of the specified object. (true to horizontally center this element in the panel;
+ /// otherwise, false.)
+ ///
+ public static bool GetAlignHorizontalCenterWithPanel(AvaloniaObject obj)
+ {
+ return (bool)obj.GetValue(AlignHorizontalCenterWithPanelProperty);
+ }
+
+ ///
+ /// Sets the value of the RelativePanel.Above XAML attached property for a target element.
+ ///
+ /// The object to which the property value is written.
+ ///
+ /// The value to set. (true to horizontally center this element in the panel; otherwise,
+ /// false.)
+ ///
+ public static void SetAlignHorizontalCenterWithPanel(AvaloniaObject obj, bool value)
+ {
+ obj.SetValue(AlignHorizontalCenterWithPanelProperty, value);
+ }
+
+ ///
+ /// Identifies the XAML attached property.
+ ///
+ public static readonly AttachedProperty AlignHorizontalCenterWithPanelProperty =
+ AvaloniaProperty.RegisterAttached("AlignHorizontalCenterWithPanel", typeof(RelativePanel), false);
+
+ ///