diff --git a/.gitmodules b/.gitmodules index 10c780c09f..9dbc50ef61 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,4 @@ 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 + 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 38f8e5f720..4ab647a25e 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -95,8 +95,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.UnitTests", "tests EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Benchmarks", "tests\Avalonia.Benchmarks\Avalonia.Benchmarks.csproj", "{410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Logging.Serilog", "src\Avalonia.Logging.Serilog\Avalonia.Logging.Serilog.csproj", "{B61B66A3-B82D-4875-8001-89D3394FE0C9}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport", "src\Avalonia.DesignerSupport\Avalonia.DesignerSupport.csproj", "{799A7BB5-3C2C-48B6-85A7-406A12C420DA}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog", "samples\ControlCatalog\ControlCatalog.csproj", "{D0A739B9-3C68-4BA6-A328-41606954B6BD}" @@ -140,7 +138,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\ReactiveUI.props = build\ReactiveUI.props build\Rx.props = build\Rx.props build\SampleApp.props = build\SampleApp.props - build\Serilog.props = build\Serilog.props build\SharpDX.props = build\SharpDX.props build\SkiaSharp.props = build\SkiaSharp.props build\System.Memory.props = build\System.Memory.props @@ -206,12 +203,20 @@ 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 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 @@ -1025,30 +1030,6 @@ Global {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Release|iPhone.Build.0 = Release|Any CPU {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.AppStore|Any CPU.Build.0 = Release|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.AppStore|iPhone.Build.0 = Release|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Debug|iPhone.Build.0 = Debug|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Release|Any CPU.Build.0 = Release|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Release|iPhone.ActiveCfg = Release|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Release|iPhone.Build.0 = Release|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {799A7BB5-3C2C-48B6-85A7-406A12C420DA}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {799A7BB5-3C2C-48B6-85A7-406A12C420DA}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {799A7BB5-3C2C-48B6-85A7-406A12C420DA}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -1849,6 +1830,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 @@ -1921,6 +1950,54 @@ 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 + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|iPhone.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|iPhone.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|Any CPU.Build.0 = Release|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhone.ActiveCfg = Release|Any CPU + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1978,6 +2055,7 @@ 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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/NOTICE.md b/NOTICE.md index 0e1d792e84..92fd725957 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -271,3 +271,35 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE + +# Chromium + +https://github.com/chromium/chromium + +// Copyright 2015 The Chromium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/build.sh b/build.sh index 40b1c225a6..a40e00f815 100755 --- a/build.sh +++ b/build.sh @@ -67,6 +67,8 @@ else fi fi +export PATH=$DOTNET_DIRECTORY:$PATH + echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]} diff --git a/build/CoreLibraries.props b/build/CoreLibraries.props index d61268173d..d17eec0135 100644 --- a/build/CoreLibraries.props +++ b/build/CoreLibraries.props @@ -7,10 +7,10 @@ - + diff --git a/build/Moq.props b/build/Moq.props index 7de9b6b6ba..9e2fd1db5d 100644 --- a/build/Moq.props +++ b/build/Moq.props @@ -1,5 +1,5 @@  - + diff --git a/build/Serilog.props b/build/Serilog.props deleted file mode 100644 index a814cf998d..0000000000 --- a/build/Serilog.props +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/build/readme.md b/build/readme.md index 387afb3425..e147556b1c 100644 --- a/build/readme.md +++ b/build/readme.md @@ -9,8 +9,6 @@ - - @@ -22,4 +20,4 @@ ```XML -``` \ No newline at end of file +``` diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index b86c679397..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; @@ -387,6 +404,9 @@ AVNCOM(IAvnClipboard, 0f) : IUnknown virtual HRESULT SetText (char* type, void* utf8Text) = 0; virtual HRESULT ObtainFormats(IAvnStringArray**ppv) = 0; virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) = 0; + virtual HRESULT SetBytes(char* type, void* utf8Text, int len) = 0; + virtual HRESULT GetBytes(char* type, IAvnString**ppv) = 0; + virtual HRESULT Clear() = 0; }; @@ -475,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/AvnString.h b/native/Avalonia.Native/src/OSX/AvnString.h index 88bc4e6963..5d299374e5 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.h +++ b/native/Avalonia.Native/src/OSX/AvnString.h @@ -12,4 +12,5 @@ extern IAvnString* CreateAvnString(NSString* string); extern IAvnStringArray* CreateAvnStringArray(NSArray* array); extern IAvnStringArray* CreateAvnStringArray(NSString* string); +extern IAvnString* CreateByteArray(void* data, int len); #endif /* AvnString_h */ diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm index 6445a9fef1..00b748ef63 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -29,6 +29,13 @@ public: memcpy((void*)_cstring, (void*)cstring, _length); } + AvnStringImpl(void*ptr, int len) + { + _length = len; + _cstring = (const char*)malloc(_length); + memcpy((void*)_cstring, ptr, len); + } + virtual ~AvnStringImpl() { free((void*)_cstring); @@ -114,3 +121,8 @@ IAvnStringArray* CreateAvnStringArray(NSString* string) { return new AvnStringArrayImpl(string); } + +IAvnString* CreateByteArray(void* data, int len) +{ + return new AvnStringImpl(data, len); +} 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/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm index 18d60d3853..116a08670e 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/native/Avalonia.Native/src/OSX/clipboard.mm @@ -82,6 +82,40 @@ public: return S_OK; } + + virtual HRESULT SetBytes(char* type, void* bytes, int len) override + { + auto typeString = [NSString stringWithUTF8String:(const char*)type]; + auto data = [NSData dataWithBytes:bytes length:len]; + if(_item == nil) + [_pb setData:data forType:typeString]; + else + [_item setData:data forType:typeString]; + return S_OK; + } + + virtual HRESULT GetBytes(char* type, IAvnString**ppv) override + { + *ppv = nil; + auto typeString = [NSString stringWithUTF8String:(const char*)type]; + NSData*data; + @try + { + if(_item) + data = [_item dataForType:typeString]; + else + data = [_pb dataForType:typeString]; + if(data == nil) + return E_FAIL; + } + @catch(NSException* e) + { + return E_FAIL; + } + *ppv = CreateByteArray((void*)data.bytes, (int)data.length); + return S_OK; + } + virtual HRESULT Clear() override { 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 abfae3cf1e..872269bb26 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; @@ -39,6 +37,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 +48,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 +111,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 +128,11 @@ public: } } + virtual bool ShouldTakeFocusOnShow() + { + return true; + } + virtual HRESULT Hide () override { @autoreleasepool @@ -390,14 +395,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 +482,8 @@ private: bool _inSetWindowState; NSRect _preZoomSize; bool _transitioningWindowState; + bool _isClientAreaExtended; + AvnExtendClientAreaChromeHints _extendClientHints; FORWARD_IUNKNOWN() BEGIN_INTERFACE_MAP() @@ -487,6 +497,8 @@ private: ComPtr WindowEvents; WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) { + _isClientAreaExtended = false; + _extendClientHints = AvnDefaultChrome; _fullScreenActive = false; _canResize = true; _decorations = SystemDecorationsFull; @@ -505,8 +517,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 +606,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 +691,6 @@ private: return S_OK; } - auto currentFrame = [Window frame]; - UpdateStyle(); HideOrShowTrafficLights(); @@ -766,6 +817,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 +910,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 +1060,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 @@ -1060,9 +1297,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}); } - - (void)updateLayer { + AvnInsidePotentialDeadlock deadlock; if (_parent == nullptr) { return; @@ -1107,7 +1344,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _lastPixelSize.Width = (int)fsize.width; _lastPixelSize.Height = (int)fsize.height; [self updateRenderTarget]; - _parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]); + + if(_parent != nullptr) + { + _parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]); + } [super viewDidChangeBackingProperties]; } @@ -1138,7 +1379,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]; @@ -1165,11 +1405,31 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent auto timestamp = [event timestamp] * 1000; auto modifiers = [self getModifiers:[event modifierFlags]]; - [self becomeFirstResponder]; - _parent->BaseEvents->RawMouseEvent(type, timestamp, modifiers, point, delta); + if(type != AvnRawMouseEventType::Move || + ( + [self window] != nil && + ( + [[self window] firstResponder] == nil + || ![[[self window] firstResponder] isKindOfClass: [NSView class]] + ) + ) + ) + [self becomeFirstResponder]; + + if(_parent != nullptr) + { + _parent->BaseEvents->RawMouseEvent(type, timestamp, modifiers, point, delta); + } + [super mouseMoved:event]; } +- (BOOL) resignFirstResponder +{ + _parent->BaseEvents->LostFocus(); + return YES; +} + - (void)mouseMoved:(NSEvent *)event { [self mouseEvent:event withType:Move]; @@ -1290,7 +1550,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent auto timestamp = [event timestamp] * 1000; auto modifiers = [self getModifiers:[event modifierFlags]]; - _lastKeyHandled = _parent->BaseEvents->RawKeyEvent(type, timestamp, modifiers, key); + if(_parent != nullptr) + { + _lastKeyHandled = _parent->BaseEvents->RawKeyEvent(type, timestamp, modifiers, key); + } } - (BOOL)performKeyEquivalent:(NSEvent *)event @@ -1381,7 +1644,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { if(!_lastKeyHandled) { - _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]); + if(_parent != nullptr) + { + _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]); + } } } @@ -1467,15 +1733,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]]; @@ -1594,6 +1888,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [self setOpaque:NO]; [self setBackgroundColor: [NSColor clearColor]]; [self invalidateShadow]; + _isExtended = false; return self; } @@ -1677,7 +1972,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { [self showWindowMenuWithAppMenu]; - _parent->BaseEvents->Activated(); + if(_parent != nullptr) + { + _parent->BaseEvents->Activated(); + } + [super becomeKeyWindow]; } } @@ -1794,8 +2093,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void)windowDidMove:(NSNotification *)notification { AvnPoint position; - _parent->GetPosition(&position); - _parent->BaseEvents->PositionChanged(position); + + if(_parent != nullptr) + { + _parent->GetPosition(&position); + _parent->BaseEvents->PositionChanged(position); + } } @end @@ -1813,7 +2116,6 @@ private: WindowEvents = events; [Window setLevel:NSPopUpMenuWindowLevel]; } - protected: virtual NSWindowStyleMask GetStyle() override { @@ -1831,6 +2133,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..fe877dc49c 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) @@ -176,7 +176,7 @@ partial class Build : NukeBuild .SetFramework(fw) .EnableNoBuild() .EnableNoRestore() - .When(Parameters.PublishTestResults, c => c + .When(Parameters.PublishTestResults, _ => _ .SetLogger("trx") .SetResultsDirectory(Parameters.TestResultsRoot))); } @@ -268,6 +268,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 8ae3f1ad66..6a04c7e31e 100644 --- a/readme.md +++ b/readme.md @@ -1,24 +1,20 @@ - +[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg) +
+[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) [![MyGet](https://img.shields.io/myget/avalonia-ci/vpre/Avalonia.svg?label=myget)](https://www.myget.org/gallery/avalonia-ci) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg) -# Avalonia +Avalonia -| Gitter Chat | Build Status (Win, Linux, OSX) | Open Collective | NuGet | MyGet | -|---|---|---|---|---| -| [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) | [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) | [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) | [![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) | [![MyGet](https://img.shields.io/myget/avalonia-ci/vpre/Avalonia.svg?label=myget)](https://www.myget.org/gallery/avalonia-ci) | +## 📖 About AvaloniaUI -## About +Avalonia is a cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows via .NET Framework and .NET Core, Linux via Xorg, macOS. Avalonia is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development. -**Avalonia** is a cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), macOS. + -**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development. +> **Note:** The UI theme you see in the picture above is still work-in-progress and will be available in the upcoming Avalonia 0.10.0 release. However, you can connect to our nightly build feed and install latest pre-release versions of Avalonia NuGet packages, if you are willing to help out with the development and testing. See [Using nightly build feed](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed) for more info. -To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239). +To see the status of some of our features, please see our [Roadmap](https://github.com/AvaloniaUI/Avalonia/issues/2239). You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been. [Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia! -You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been. - -[Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia! - -## Getting Started +## 🚀 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). @@ -30,6 +26,17 @@ Install-Package Avalonia Install-Package Avalonia.Desktop ``` +## Showcase + +Examples of UIs built with Avalonia +![image](https://user-images.githubusercontent.com/4672627/84707589-5b69a880-af35-11ea-87a6-7ad57a31d314.png) + +![image](https://user-images.githubusercontent.com/4672627/85069644-d8419000-b18a-11ea-8732-be9055bb61fd.PNG) + +![image](https://user-images.githubusercontent.com/4672627/85069659-dc6dad80-b18a-11ea-8375-39ef95315b5c.PNG) + +![image](https://user-images.githubusercontent.com/4672627/84708947-c3b98980-af37-11ea-8c9d-503334615bbf.png) + ## JetBrains Rider If you need to develop Avalonia app with JetBrains Rider, go and *vote* on [this issue](https://youtrack.jetbrains.com/issue/RIDER-39247) in their tracker. JetBrains won't do things without their users telling them that they want the feature, so only **YOU** can make it happen. @@ -61,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/App.xaml.cs b/samples/BindingDemo/App.xaml.cs index f2f44cd502..13875aeb21 100644 --- a/samples/BindingDemo/App.xaml.cs +++ b/samples/BindingDemo/App.xaml.cs @@ -1,10 +1,8 @@ using System; using Avalonia; using Avalonia.Controls; -using Avalonia.Logging.Serilog; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -using Serilog; namespace BindingDemo { diff --git a/samples/BindingDemo/BindingDemo.csproj b/samples/BindingDemo/BindingDemo.csproj index ce33f42143..817023fd71 100644 --- a/samples/BindingDemo/BindingDemo.csproj +++ b/samples/BindingDemo/BindingDemo.csproj @@ -10,7 +10,6 @@ - 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"> + @@ -29,7 +30,11 @@ ScrollViewer.HorizontalScrollBarVisibility="Disabled"> - + + + + + + + + + @@ -67,10 +76,12 @@ Full Decorations - Light - Dark + Fluent - Dark + Fluent - Light + Simple - Light + Simple - Dark - + None Transparent Blur diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 8c4818fb07..b0c205246e 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -32,30 +32,25 @@ namespace ControlCatalog } - var light = new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) - { - Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default") - }; - var dark = new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) - { - Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseDark.xaml?assembly=Avalonia.Themes.Default") - }; - - var themes = this.Find("Themes"); themes.SelectionChanged += (sender, e) => { switch (themes.SelectedIndex) { case 0: - Styles[0] = light; + Application.Current.Styles[0] = App.FluentDark; break; case 1: - Styles[0] = dark; + Application.Current.Styles[0] = App.FluentLight; + break; + case 2: + Application.Current.Styles[0] = App.DefaultLight; + break; + case 3: + Application.Current.Styles[0] = App.DefaultDark; break; } - }; - Styles.Add(light); + }; var decorations = this.Find("Decorations"); decorations.SelectionChanged += (sender, e) => @@ -63,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 a0bb956425..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="Transparent"> + 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/Models/Person.cs b/samples/ControlCatalog/Models/Person.cs index 4248cb8056..a0abcfe8f4 100644 --- a/samples/ControlCatalog/Models/Person.cs +++ b/samples/ControlCatalog/Models/Person.cs @@ -21,12 +21,12 @@ namespace ControlCatalog.Models get => _firstName; set { + _firstName = value; if (string.IsNullOrWhiteSpace(value)) SetError(nameof(FirstName), "First Name Required"); else SetError(nameof(FirstName), null); - _firstName = value; OnPropertyChanged(nameof(FirstName)); } @@ -37,12 +37,12 @@ namespace ControlCatalog.Models get => _lastName; set { + _lastName = value; if (string.IsNullOrWhiteSpace(value)) SetError(nameof(LastName), "Last Name Required"); else SetError(nameof(LastName), null); - _lastName = value; OnPropertyChanged(nameof(LastName)); } } @@ -95,4 +95,4 @@ namespace ControlCatalog.Models return null; } } -} \ No newline at end of file +} 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 c30056d5e5..bfd14cc627 100644 --- a/samples/ControlCatalog/Pages/BorderPage.xaml +++ b/samples/ControlCatalog/Pages/BorderPage.xaml @@ -9,26 +9,33 @@ 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 39d89590c2..a4427c70c5 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/ButtonPage.xaml.cs b/samples/ControlCatalog/Pages/ButtonPage.xaml.cs index 1d0c228a0e..5e555c8c91 100644 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ButtonPage.xaml.cs @@ -5,5 +5,25 @@ namespace ControlCatalog.Pages { public class ButtonPage : UserControl { + private int repeatButtonClickCount = 0; + + public ButtonPage() + { + InitializeComponent(); + + this.FindControl("RepeatButton").Click += OnRepeatButtonClick; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public void OnRepeatButtonClick(object sender, object args) + { + repeatButtonClickCount++; + var textBlock = this.FindControl("RepeatButtonTextBlock"); + textBlock.Text = $"Repeat Button: {repeatButtonClickCount}"; + } } } diff --git a/samples/ControlCatalog/Pages/DatePickerPage.xaml b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml similarity index 75% rename from samples/ControlCatalog/Pages/DatePickerPage.xaml rename to samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml index 30d5a7506f..107472105a 100644 --- a/samples/ControlCatalog/Pages/DatePickerPage.xaml +++ b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml @@ -1,8 +1,8 @@ + x:Class="ControlCatalog.Pages.CalendarDatePickerPage"> - DatePicker + CalendarDatePicker A control for selecting dates with a calendar drop-down - - - - - - - + diff --git a/samples/ControlCatalog/Pages/DatePickerPage.xaml.cs b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml.cs similarity index 57% rename from samples/ControlCatalog/Pages/DatePickerPage.xaml.cs rename to samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml.cs index ef01887c9e..95bdeb363a 100644 --- a/samples/ControlCatalog/Pages/DatePickerPage.xaml.cs +++ b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml.cs @@ -4,17 +4,17 @@ using System; namespace ControlCatalog.Pages { - public class DatePickerPage : UserControl + public class CalendarDatePickerPage : UserControl { - public DatePickerPage() + public CalendarDatePickerPage() { InitializeComponent(); - var dp1 = this.FindControl("DatePicker1"); - var dp2 = this.FindControl("DatePicker2"); - var dp3 = this.FindControl("DatePicker3"); - var dp4 = this.FindControl("DatePicker4"); - var dp5 = this.FindControl("DatePicker5"); + var dp1 = this.FindControl("DatePicker1"); + var dp2 = this.FindControl("DatePicker2"); + var dp3 = this.FindControl("DatePicker3"); + var dp4 = this.FindControl("DatePicker4"); + var dp5 = this.FindControl("DatePicker5"); dp1.SelectedDate = DateTime.Today; dp2.SelectedDate = DateTime.Today.AddDays(10); diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml b/samples/ControlCatalog/Pages/ComboBoxPage.xaml index bbfbd4b4cd..369f703718 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml @@ -6,7 +6,7 @@ A drop-down list. - + Inline Items Inline Item 2 Inline Item 3 @@ -16,7 +16,7 @@ - + Control Items diff --git a/samples/ControlCatalog/Pages/ContextMenuPage.xaml b/samples/ControlCatalog/Pages/ContextMenuPage.xaml index 8f147638b5..8ccd8e97f7 100644 --- a/samples/ControlCatalog/Pages/ContextMenuPage.xaml +++ b/samples/ControlCatalog/Pages/ContextMenuPage.xaml @@ -9,7 +9,7 @@ Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16"> - @@ -34,7 +34,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/DialogsPage.xaml b/samples/ControlCatalog/Pages/DialogsPage.xaml index 0834e829d8..a0e82663bf 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml @@ -11,5 +11,7 @@ + + diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index dcb94a89e7..cf6c771e34 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -93,6 +93,21 @@ namespace ControlCatalog.Pages window.ShowInTaskbar = false; window.ShowDialog(GetWindow()); }; + this.FindControl + + - + - + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs index 8f5d380078..a4003cadc9 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -5,23 +5,32 @@ using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Markup.Xaml; +using Avalonia.VisualTree; using ControlCatalog.ViewModels; namespace ControlCatalog.Pages { public class ItemsRepeaterPage : UserControl { + private readonly ItemsRepeaterPageViewModel _viewModel; private ItemsRepeater _repeater; private ScrollViewer _scroller; + private Button _scrollToLast; + private Button _scrollToRandom; + private Random _random = new Random(0); public ItemsRepeaterPage() { this.InitializeComponent(); _repeater = this.FindControl("repeater"); _scroller = this.FindControl("scroller"); + _scrollToLast = this.FindControl + Single 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 26d25a6266..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 de5669123f..73aaeff994 100644 --- a/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.ObjectModel; using System.Linq; +using Avalonia.Media; using ReactiveUI; namespace ControlCatalog.ViewModels @@ -27,7 +28,7 @@ namespace ControlCatalog.ViewModels public void AddItem() { var index = SelectedItem != null ? Items.IndexOf(SelectedItem) : -1; - Items.Insert(index + 1, new Item { Text = $"New Item {_newItemIndex++}" }); + Items.Insert(index + 1, new Item(index + 1) { Text = $"New Item {_newItemIndex++}" }); } public void RandomizeHeights() @@ -52,7 +53,7 @@ namespace ControlCatalog.ViewModels _newGenerationIndex++; return new ObservableCollection( - Enumerable.Range(1, 100000).Select(i => new Item + Enumerable.Range(1, 100000).Select(i => new Item(i) { Text = $"Item {i.ToString()} {suffix}" })); @@ -61,6 +62,12 @@ namespace ControlCatalog.ViewModels public class Item : ReactiveObject { private double _height = double.NaN; + private int _index; + + public Item(int index) + { + _index = index; + } public string Text { get; set; } @@ -69,6 +76,8 @@ 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/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/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/ControlCatalog/ViewModels/TreeViewPageViewModel.cs b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs index d396ef2b3d..5bc23e2fe5 100644 --- a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs @@ -23,12 +23,14 @@ namespace ControlCatalog.ViewModels AddItemCommand = ReactiveCommand.Create(AddItem); RemoveItemCommand = ReactiveCommand.Create(RemoveItem); + SelectRandomItemCommand = ReactiveCommand.Create(SelectRandomItem); } public ObservableCollection Items { get; } public SelectionModel Selection { get; } public ReactiveCommand AddItemCommand { get; } public ReactiveCommand RemoveItemCommand { get; } + public ReactiveCommand SelectRandomItemCommand { get; } public SelectionMode SelectionMode { @@ -74,6 +76,15 @@ namespace ControlCatalog.ViewModels } } + private void SelectRandomItem() + { + var random = new Random(); + var depth = random.Next(4); + var indexes = Enumerable.Range(0, 4).Select(x => random.Next(10)); + var path = new IndexPath(indexes); + Selection.SelectedIndex = path; + } + private void SelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) { var selected = string.Join(",", e.SelectedIndices); diff --git a/samples/RenderDemo/App.xaml.cs b/samples/RenderDemo/App.xaml.cs index 923174d4c7..233160b025 100644 --- a/samples/RenderDemo/App.xaml.cs +++ b/samples/RenderDemo/App.xaml.cs @@ -1,5 +1,4 @@ using Avalonia; -using Avalonia.Logging.Serilog; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; @@ -22,6 +21,5 @@ namespace RenderDemo .UsePlatformDetect() .UseReactiveUI() .LogToDebug(); - } } diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index c098ef411e..14ccc82043 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -29,6 +29,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/TransitionsPage.xaml b/samples/RenderDemo/Pages/TransitionsPage.xaml new file mode 100644 index 0000000000..d6da293ff3 --- /dev/null +++ b/samples/RenderDemo/Pages/TransitionsPage.xaml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hover to activate Transform Keyframe Animations. + + + + + + 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/ClipboardImpl.cs b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs index 51e0a1e799..7802f336fb 100644 --- a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs +++ b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs @@ -43,5 +43,11 @@ namespace Avalonia.Android.Platform return Task.FromResult(null); } + + public Task SetDataObjectAsync(IDataObject data) => throw new PlatformNotSupportedException(); + + public Task GetFormatsAsync() => throw new PlatformNotSupportedException(); + + public Task GetDataAsync(string format) => throw new PlatformNotSupportedException(); } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 71706676d6..69fd2a9f13 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -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/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj index 2f95a6e4bd..b8697e0ca2 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj +++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj @@ -149,7 +149,6 @@ - diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 9e9b84537b..067d9f462f 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -93,17 +93,17 @@ namespace Avalonia.Animation var oldTransitions = change.OldValue.GetValueOrDefault(); var newTransitions = change.NewValue.GetValueOrDefault(); - if (oldTransitions is object) - { - oldTransitions.CollectionChanged -= TransitionsCollectionChanged; - RemoveTransitions(oldTransitions); - } - if (newTransitions is object) { newTransitions.CollectionChanged += TransitionsCollectionChanged; AddTransitions(newTransitions); } + + if (oldTransitions is object) + { + oldTransitions.CollectionChanged -= TransitionsCollectionChanged; + RemoveTransitions(oldTransitions); + } } else if (_transitionsEnabled && Transitions is object && @@ -111,8 +111,10 @@ namespace Avalonia.Animation !change.Property.IsDirect && change.Priority > BindingPriority.Animation) { - foreach (var transition in Transitions) + for (var i = Transitions.Count -1; i >= 0; --i) { + var transition = Transitions[i]; + if (transition.Property == change.Property) { var state = _transitionState[transition]; 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.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs index ad2001d621..5184341324 100644 --- a/src/Avalonia.Animation/TransitionInstance.cs +++ b/src/Avalonia.Animation/TransitionInstance.cs @@ -4,6 +4,7 @@ using System.Reactive.Linq; using Avalonia.Animation.Easings; using Avalonia.Animation.Utils; using Avalonia.Reactive; +using Avalonia.Utilities; namespace Avalonia.Animation { @@ -13,31 +14,56 @@ namespace Avalonia.Animation internal class TransitionInstance : SingleSubscriberObservableBase { private IDisposable _timerSubscription; + private TimeSpan _delay; private TimeSpan _duration; private readonly IClock _baseClock; private IClock _clock; - public TransitionInstance(IClock clock, TimeSpan Duration) + public TransitionInstance(IClock clock, TimeSpan delay, TimeSpan duration) { clock = clock ?? throw new ArgumentNullException(nameof(clock)); - _duration = Duration; + _delay = delay; + _duration = duration; _baseClock = clock; } private void TimerTick(TimeSpan t) { - var interpVal = _duration.Ticks == 0 ? 1d : (double)t.Ticks / _duration.Ticks; + + // [<------------- normalizedTotalDur ------------------>] + // [<---- Delay ---->][<---------- Duration ------------>] + // ^- normalizedDelayEnd + // [<---- normalizedInterpVal --->] + + var normalizedInterpVal = 1d; + + if (!MathUtilities.AreClose(_duration.TotalSeconds, 0d)) + { + var normalizedTotalDur = _delay + _duration; + var normalizedDelayEnd = _delay.TotalSeconds / normalizedTotalDur.TotalSeconds; + var normalizedPresentationTime = t.TotalSeconds / normalizedTotalDur.TotalSeconds; + + if (normalizedPresentationTime < normalizedDelayEnd + || MathUtilities.AreClose(normalizedPresentationTime, normalizedDelayEnd)) + { + normalizedInterpVal = 0d; + } + else + { + normalizedInterpVal = (t.TotalSeconds - _delay.TotalSeconds) / _duration.TotalSeconds; + } + } // Clamp interpolation value. - if (interpVal >= 1d | interpVal < 0d) + if (normalizedInterpVal >= 1d || normalizedInterpVal < 0d) { PublishNext(1d); PublishCompleted(); } else { - PublishNext(interpVal); + PublishNext(normalizedInterpVal); } } diff --git a/src/Avalonia.Animation/Transition`1.cs b/src/Avalonia.Animation/Transition`1.cs index 138131acb9..4542a137e5 100644 --- a/src/Avalonia.Animation/Transition`1.cs +++ b/src/Avalonia.Animation/Transition`1.cs @@ -13,10 +13,15 @@ namespace Avalonia.Animation private AvaloniaProperty _prop; /// - /// Gets the duration of the animation. + /// Gets or sets the duration of the transition. /// public TimeSpan Duration { get; set; } + /// + /// Gets or sets delay before starting the transition. + /// + public TimeSpan Delay { get; set; } = TimeSpan.Zero; + /// /// Gets the easing class to be used. /// @@ -47,7 +52,7 @@ namespace Avalonia.Animation /// public virtual IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue) { - var transition = DoTransition(new TransitionInstance(clock, Duration), (T)oldValue, (T)newValue); + var transition = DoTransition(new TransitionInstance(clock, Delay, Duration), (T)oldValue, (T)newValue); return control.Bind((AvaloniaProperty)Property, transition, Data.BindingPriority.Animation); } } diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index f387d7e0b6..d18f0b3f94 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -421,8 +421,7 @@ namespace Avalonia throw new ArgumentException($"The property {property.Name} is readonly."); } - Logger.TryGet(LogEventLevel.Verbose)?.Log( - LogArea.Property, + Logger.TryGet(LogEventLevel.Verbose, LogArea.Property)?.Log( this, "Bound {Property} to {Binding} with priority LocalValue", property, @@ -501,8 +500,7 @@ namespace Avalonia if (change.IsEffectiveValueChange) { - Logger.TryGet(LogEventLevel.Verbose)?.Log( - LogArea.Property, + Logger.TryGet(LogEventLevel.Verbose, LogArea.Property)?.Log( this, "{Property} changed from {$Old} to {$Value} with priority {Priority}", property, @@ -586,8 +584,7 @@ namespace Avalonia /// The binding error. protected internal virtual void LogBindingError(AvaloniaProperty property, Exception e) { - Logger.TryGet(LogEventLevel.Warning)?.Log( - LogArea.Binding, + Logger.TryGet(LogEventLevel.Warning, LogArea.Binding)?.Log( this, "Error in binding to {Target}.{Property}: {Message}", this, @@ -857,8 +854,7 @@ namespace Avalonia /// The priority. private void LogPropertySet(AvaloniaProperty property, T value, BindingPriority priority) { - Logger.TryGet(LogEventLevel.Verbose)?.Log( - LogArea.Property, + Logger.TryGet(LogEventLevel.Verbose, LogArea.Property)?.Log( this, "Set {Property} to {$Value} with priority {Priority}", property, diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index daa7191cc5..a873d5fd42 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. @@ -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/Converters/BoolConverters.cs b/src/Avalonia.Base/Data/Converters/BoolConverters.cs index 817d1cea9a..6740c2168f 100644 --- a/src/Avalonia.Base/Data/Converters/BoolConverters.cs +++ b/src/Avalonia.Base/Data/Converters/BoolConverters.cs @@ -3,7 +3,7 @@ using System.Linq; namespace Avalonia.Data.Converters { /// - /// Provides a set of useful s for working with string values. + /// Provides a set of useful s for working with bool values. /// public static class BoolConverters { diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 9721369e47..c4f61dfedb 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -168,8 +168,7 @@ namespace Avalonia.Data.Core } else { - Logger.TryGet(LogEventLevel.Error)?.Log( - LogArea.Binding, + Logger.TryGet(LogEventLevel.Error, LogArea.Binding)?.Log( this, "Could not convert FallbackValue {FallbackValue} to {Type}", _fallbackValue, 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 _getter; + private readonly Action _setter; + + public ClrPropertyInfo(string name, Func getter, Action setter, Type propertyType) + { + _getter = getter; + _setter = setter; + PropertyType = propertyType; + Name = name; + } + + public string Name { get; } + public Type PropertyType { get; } + + public object Get(object target) + { + if (_getter == null) + throw new NotSupportedException("Property " + Name + " doesn't have a getter"); + return _getter(target); + } + + public void Set(object target, object value) + { + if (_setter == null) + throw new NotSupportedException("Property " + Name + " doesn't have a setter"); + _setter(target, value); + } + + public bool CanSet => _setter != null; + public bool CanGet => _getter != null; + } + + public class ReflectionClrPropertyInfo : ClrPropertyInfo + { + static Action CreateSetter(PropertyInfo info) + { + if (info.SetMethod == null) + return null; + var target = Expression.Parameter(typeof(object), "target"); + var value = Expression.Parameter(typeof(object), "value"); + return Expression.Lambda>( + Expression.Call(Expression.Convert(target, info.DeclaringType), info.SetMethod, + Expression.Convert(value, info.SetMethod.GetParameters()[0].ParameterType)), + target, value) + .Compile(); + } + + static Func CreateGetter(PropertyInfo info) + { + if (info.GetMethod == null) + return null; + var target = Expression.Parameter(typeof(object), "target"); + return Expression.Lambda>( + Expression.Convert(Expression.Call(Expression.Convert(target, info.DeclaringType), info.GetMethod), + typeof(object))) + .Compile(); + } + + public ReflectionClrPropertyInfo(PropertyInfo info) : base(info.Name, + CreateGetter(info), CreateSetter(info), info.PropertyType) + { + + } + } +} diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 3bacd38a20..7ecaa278d7 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -54,6 +54,7 @@ namespace Avalonia.Data.Core private object _root; private IDisposable _rootSubscription; private WeakReference _value; + private IReadOnlyList _transformNodes; /// /// Initializes a new instance of the class. @@ -188,6 +189,24 @@ namespace Avalonia.Data.Core description ?? expression.ToString()); } + private IReadOnlyList GetTransformNodesFromChain() + { + LinkedList transforms = new LinkedList(); + var node = _node; + while (node != null) + { + if (node is ITransformNode transform) + { + transforms.AddFirst(transform); + } + node = node.Next; + } + + return new List(transforms); + } + + private IReadOnlyList TransformNodes => (_transformNodes ?? (_transformNodes = GetTransformNodesFromChain())); + /// /// Attempts to set the value of a property expression. /// @@ -203,18 +222,13 @@ namespace Avalonia.Data.Core { if (Leaf is SettableNode settable) { - var node = _node; - while (node != null) + foreach (var transform in TransformNodes) { - if (node is ITransformNode transform) + value = transform.Transform(value); + if (value is BindingNotification) { - value = transform.Transform(value); - if (value is BindingNotification) - { - return false; - } + return false; } - node = node.Next; } return settable.SetTargetValue(value, priority); } diff --git a/src/Avalonia.Base/Data/Core/IPropertyInfo.cs b/src/Avalonia.Base/Data/Core/IPropertyInfo.cs new file mode 100644 index 0000000000..2417d0ffc4 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/IPropertyInfo.cs @@ -0,0 +1,14 @@ +using System; + +namespace Avalonia.Data.Core +{ + public interface IPropertyInfo + { + string Name { get; } + object Get(object target); + void Set(object target, object value); + bool CanSet { get; } + bool CanGet { get; } + Type PropertyType { get; } + } +} diff --git a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs index 7c402f42f6..6d09befeba 100644 --- a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs +++ b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs @@ -12,8 +12,19 @@ namespace Avalonia.Data.Core base.NextValueChanged(Negate(value)); } - private static object Negate(object v) + private static object Negate(object value) { + var notification = value as BindingNotification; + var v = BindingNotification.ExtractValue(value); + + BindingNotification GenerateError(Exception e) + { + notification ??= new BindingNotification(AvaloniaProperty.UnsetValue); + notification.AddError(e, BindingErrorType.Error); + notification.ClearValue(); + return notification; + } + if (v != AvaloniaProperty.UnsetValue) { var s = v as string; @@ -28,9 +39,7 @@ namespace Avalonia.Data.Core } else { - return new BindingNotification( - new InvalidCastException($"Unable to convert '{s}' to bool."), - BindingErrorType.Error); + return GenerateError(new InvalidCastException($"Unable to convert '{s}' to bool.")); } } else @@ -38,24 +47,31 @@ namespace Avalonia.Data.Core try { var boolean = Convert.ToBoolean(v, CultureInfo.InvariantCulture); - return !boolean; + + if (notification is object) + { + notification.SetValue(!boolean); + return notification; + } + else + { + return !boolean; + } } catch (InvalidCastException) { // The error message here is "Unable to cast object of type 'System.Object' // to type 'System.IConvertible'" which is kinda useless so provide our own. - return new BindingNotification( - new InvalidCastException($"Unable to convert '{v}' to bool."), - BindingErrorType.Error); + return GenerateError(new InvalidCastException($"Unable to convert '{v}' to bool.")); } catch (Exception e) { - return new BindingNotification(e, BindingErrorType.Error); + return GenerateError(e); } } } - return AvaloniaProperty.UnsetValue; + return notification ?? AvaloniaProperty.UnsetValue; } public object Transform(object value) diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index f2ed86d2aa..8fc2a7b77c 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -1,7 +1,5 @@ using System; using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; using System.Reflection; using Avalonia.Utilities; @@ -14,7 +12,7 @@ namespace Avalonia.Data.Core.Plugins public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin { /// - public bool Match(object obj, string propertyName) => true; + public bool Match(object obj, string propertyName) => GetPropertyWithName(obj.GetType(), propertyName) != null; /// /// Starts monitoring the value of a property on an object. @@ -31,7 +29,8 @@ namespace Avalonia.Data.Core.Plugins Contract.Requires(propertyName != null); reference.TryGetTarget(out object instance); - var p = instance.GetType().GetRuntimeProperties().FirstOrDefault(x => x.Name == propertyName); + + var p = GetPropertyWithName(instance.GetType(), propertyName); if (p != null) { @@ -45,6 +44,14 @@ namespace Avalonia.Data.Core.Plugins } } + private static PropertyInfo GetPropertyWithName(Type type, string propertyName) + { + const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | + BindingFlags.Static | BindingFlags.Instance; + + return type.GetProperty(propertyName, bindingFlags); + } + private class Accessor : PropertyAccessorBase, IWeakSubscriber { private readonly WeakReference _reference; diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs index c19ee8dba7..5d694f4cf9 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs @@ -1,13 +1,16 @@ using System; -using System.Linq; +using System.Collections.Generic; +using System.Linq.Expressions; using System.Reflection; namespace Avalonia.Data.Core.Plugins { - class MethodAccessorPlugin : IPropertyAccessorPlugin + public class MethodAccessorPlugin : IPropertyAccessorPlugin { - public bool Match(object obj, string methodName) - => obj.GetType().GetRuntimeMethods().Any(x => x.Name == methodName); + private readonly Dictionary<(Type, string), MethodInfo> _methodLookup = + new Dictionary<(Type, string), MethodInfo>(); + + public bool Match(object obj, string methodName) => GetFirstMethodWithName(obj.GetType(), methodName) != null; public IPropertyAccessor Start(WeakReference reference, string methodName) { @@ -15,17 +18,22 @@ namespace Avalonia.Data.Core.Plugins Contract.Requires(methodName != null); reference.TryGetTarget(out object instance); - var method = instance.GetType().GetRuntimeMethods().FirstOrDefault(x => x.Name == methodName); + + var method = GetFirstMethodWithName(instance.GetType(), methodName); if (method != null) { - if (method.GetParameters().Length + (method.ReturnType == typeof(void) ? 0 : 1) > 8) + var parameters = method.GetParameters(); + + if (parameters.Length + (method.ReturnType == typeof(void) ? 0 : 1) > 8) { - var exception = new ArgumentException("Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.", nameof(methodName)); + var exception = new ArgumentException( + "Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.", + nameof(methodName)); return new PropertyError(new BindingNotification(exception, BindingErrorType.Error)); } - return new Accessor(reference, method); + return new Accessor(reference, method, parameters); } else { @@ -35,31 +43,72 @@ namespace Avalonia.Data.Core.Plugins } } + private MethodInfo GetFirstMethodWithName(Type type, string methodName) + { + var key = (type, methodName); + + if (!_methodLookup.TryGetValue(key, out MethodInfo methodInfo)) + { + methodInfo = TryFindAndCacheMethod(type, methodName); + } + + return methodInfo; + } + + private MethodInfo TryFindAndCacheMethod(Type type, string methodName) + { + MethodInfo found = null; + + const BindingFlags bindingFlags = + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance; + + var methods = type.GetMethods(bindingFlags); + + foreach (MethodInfo methodInfo in methods) + { + if (methodInfo.Name == methodName) + { + found = methodInfo; + + break; + } + } + + _methodLookup.Add((type, methodName), found); + + return found; + } + private sealed class Accessor : PropertyAccessorBase { - public Accessor(WeakReference reference, MethodInfo method) + public Accessor(WeakReference reference, MethodInfo method, ParameterInfo[] parameters) { Contract.Requires(reference != null); Contract.Requires(method != null); - var paramTypes = method.GetParameters().Select(param => param.ParameterType).ToArray(); var returnType = method.ReturnType; - - if (returnType == typeof(void)) + bool hasReturn = returnType != typeof(void); + + var signatureTypeCount = (hasReturn ? 1 : 0) + parameters.Length; + + var paramTypes = new Type[signatureTypeCount]; + + for (var i = 0; i < parameters.Length; i++) { - if (paramTypes.Length == 0) - { - PropertyType = typeof(Action); - } - else - { - PropertyType = Type.GetType($"System.Action`{paramTypes.Length}").MakeGenericType(paramTypes); - } + ParameterInfo parameter = parameters[i]; + + paramTypes[i] = parameter.ParameterType; + } + + if (hasReturn) + { + paramTypes[paramTypes.Length - 1] = returnType; + + PropertyType = Expression.GetFuncType(paramTypes); } else { - var genericTypeParameters = paramTypes.Concat(new[] { returnType }).ToArray(); - PropertyType = Type.GetType($"System.Func`{genericTypeParameters.Length}").MakeGenericType(genericTypeParameters); + PropertyType = Expression.GetActionType(paramTypes); } if (method.IsStatic) diff --git a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs index dad0832e12..debf050a97 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs @@ -59,7 +59,7 @@ namespace Avalonia.Data.Core.Plugins return Observable.Empty(); } - protected IObservable HandleCompleted(Task task) + private IObservable HandleCompleted(Task task) { var resultProperty = task.GetType().GetRuntimeProperty("Result"); diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index e20685b1dd..d5e835cabd 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core @@ -6,6 +7,7 @@ namespace Avalonia.Data.Core public class PropertyAccessorNode : SettableNode { private readonly bool _enableValidation; + private IPropertyAccessorPlugin _customPlugin; private IPropertyAccessor _accessor; public PropertyAccessorNode(string propertyName, bool enableValidation) @@ -14,6 +16,13 @@ namespace Avalonia.Data.Core _enableValidation = enableValidation; } + public PropertyAccessorNode(string propertyName, bool enableValidation, IPropertyAccessorPlugin customPlugin) + { + PropertyName = propertyName; + _enableValidation = enableValidation; + _customPlugin = customPlugin; + } + public override string Description => PropertyName; public string PropertyName { get; } public override Type PropertyType => _accessor?.PropertyType; @@ -36,18 +45,20 @@ namespace Avalonia.Data.Core { reference.TryGetTarget(out object target); - IPropertyAccessorPlugin plugin = null; + var plugin = _customPlugin ?? GetPropertyAccessorPluginForObject(target); + var accessor = plugin?.Start(reference, PropertyName); - foreach (IPropertyAccessorPlugin x in ExpressionObserver.PropertyAccessors) + // We need to handle accessor fallback before handling validation. Validators do not support null accessors. + if (accessor == null) { - if (x.Match(target, PropertyName)) - { - plugin = x; - break; - } - } + reference.TryGetTarget(out object instance); - var accessor = plugin?.Start(reference, PropertyName); + var message = $"Could not find a matching property accessor for '{PropertyName}' on '{instance}'"; + + var exception = new MissingMemberException(message); + + accessor = new PropertyError(new BindingNotification(exception, BindingErrorType.Error)); + } if (_enableValidation && Next == null) { @@ -60,16 +71,27 @@ namespace Avalonia.Data.Core } } - if (accessor == null) + if (accessor is null) { - throw new NotSupportedException( - $"Could not find a matching property accessor for {PropertyName}."); + throw new AvaloniaInternalException("Data validators must return non-null accessor."); } _accessor = accessor; accessor.Subscribe(ValueChanged); } + private IPropertyAccessorPlugin GetPropertyAccessorPluginForObject(object target) + { + foreach (IPropertyAccessorPlugin x in ExpressionObserver.PropertyAccessors) + { + if (x.Match(target, PropertyName)) + { + return x; + } + } + return null; + } + protected override void StopListeningCore() { _accessor.Dispose(); diff --git a/src/Avalonia.Base/Data/Core/PropertyPath.cs b/src/Avalonia.Base/Data/Core/PropertyPath.cs new file mode 100644 index 0000000000..665953c4a1 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/PropertyPath.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Avalonia.Data.Core +{ + public class PropertyPath + { + public IReadOnlyList Elements { get; } + + public PropertyPath(IEnumerable elements) + { + Elements = elements.ToList(); + } + } + + public class PropertyPathBuilder + { + readonly List _elements = new List(); + + public PropertyPathBuilder Property(IPropertyInfo property) + { + _elements.Add(new PropertyPropertyPathElement(property)); + return this; + } + + + public PropertyPathBuilder ChildTraversal() + { + _elements.Add(new ChildTraversalPropertyPathElement()); + return this; + } + + public PropertyPathBuilder EnsureType(Type type) + { + _elements.Add(new EnsureTypePropertyPathElement(type)); + return this; + } + + public PropertyPathBuilder Cast(Type type) + { + _elements.Add(new CastTypePropertyPathElement(type)); + return this; + } + + public PropertyPath Build() + { + return new PropertyPath(_elements); + } + } + + public interface IPropertyPathElement + { + + } + + public class PropertyPropertyPathElement : IPropertyPathElement + { + public IPropertyInfo Property { get; } + + public PropertyPropertyPathElement(IPropertyInfo property) + { + Property = property; + } + } + + public class ChildTraversalPropertyPathElement : IPropertyPathElement + { + + } + + public class EnsureTypePropertyPathElement : IPropertyPathElement + { + public Type Type { get; } + + public EnsureTypePropertyPathElement(Type type) + { + Type = type; + } + } + + public class CastTypePropertyPathElement : IPropertyPathElement + { + public CastTypePropertyPathElement(Type type) + { + Type = type; + } + + public Type Type { get; } + } +} diff --git a/src/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs index e501c0a03d..023999f5c5 100644 --- a/src/Avalonia.Base/Data/Core/StreamNode.cs +++ b/src/Avalonia.Base/Data/Core/StreamNode.cs @@ -1,35 +1,54 @@ using System; using System.Reactive.Linq; +using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core { public class StreamNode : ExpressionNode { + private IStreamPlugin _customPlugin = null; private IDisposable _subscription; public override string Description => "^"; + public StreamNode() { } + + public StreamNode(IStreamPlugin customPlugin) + { + _customPlugin = customPlugin; + } + protected override void StartListeningCore(WeakReference reference) { + GetPlugin(reference)?.Start(reference).Subscribe(ValueChanged); + } + + protected override void StopListeningCore() + { + _subscription?.Dispose(); + _subscription = null; + } + + private IStreamPlugin GetPlugin(WeakReference reference) + { + if (_customPlugin != null) + { + return _customPlugin; + } + foreach (var plugin in ExpressionObserver.StreamHandlers) { if (plugin.Match(reference)) { - _subscription = plugin.Start(reference).Subscribe(ValueChanged); - return; + return plugin; } } - // TODO: Improve error. + // TODO: Improve error ValueChanged(new BindingNotification( new MarkupBindingChainException("Stream operator applied to unsupported type", Description), BindingErrorType.Error)); - } - - protected override void StopListeningCore() - { - _subscription?.Dispose(); - _subscription = null; + return null; } } } diff --git a/src/Avalonia.Base/Data/Optional.cs b/src/Avalonia.Base/Data/Optional.cs index dd952c895c..8e044d7896 100644 --- a/src/Avalonia.Base/Data/Optional.cs +++ b/src/Avalonia.Base/Data/Optional.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; #nullable enable @@ -22,13 +23,13 @@ namespace Avalonia.Data /// public readonly struct Optional : IEquatable> { - private readonly T _value; + [AllowNull] private readonly T _value; /// /// Initializes a new instance of the struct with value. /// /// The value. - public Optional(T value) + public Optional([AllowNull] T value) { _value = value; HasValue = true; @@ -48,7 +49,7 @@ namespace Avalonia.Data public T Value => HasValue ? _value : throw new InvalidOperationException("Optional has no value."); /// - public override bool Equals(object obj) => obj is Optional o && this == o; + public override bool Equals(object? obj) => obj is Optional o && this == o; /// public bool Equals(Optional other) => this == other; @@ -69,6 +70,7 @@ namespace Avalonia.Data /// Gets the value if present, otherwise the default value. /// /// The value. + [return: MaybeNull] public T GetValueOrDefault() => HasValue ? _value : default; /// @@ -85,6 +87,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 ? @@ -101,7 +104,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 @@ -112,7 +116,7 @@ namespace Avalonia.Data /// Creates an from an instance of the underlying value type. /// /// The value. - public static implicit operator Optional(T value) => new Optional(value); + public static implicit operator Optional([AllowNull] T value) => new Optional(value); /// /// Compares two s for inequality. @@ -128,7 +132,7 @@ namespace Avalonia.Data /// The first value. /// The second value. /// True if the values are equal; otherwise false. - public static bool operator==(Optional x, Optional y) + public static bool operator ==(Optional x, Optional y) { if (!x.HasValue && !y.HasValue) { diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 0e65379abd..d42c030245 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -120,7 +120,7 @@ namespace Avalonia return o.GetValue(this); } - internal override object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) + internal override object? RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) { return o.GetValue(this); } diff --git a/src/Avalonia.Base/IStyledPropertyMetadata.cs b/src/Avalonia.Base/IStyledPropertyMetadata.cs index f567cd930c..a68b65e5e0 100644 --- a/src/Avalonia.Base/IStyledPropertyMetadata.cs +++ b/src/Avalonia.Base/IStyledPropertyMetadata.cs @@ -1,5 +1,3 @@ -using System; - namespace Avalonia { /// diff --git a/src/Avalonia.Base/Logging/DebugLogSink.cs b/src/Avalonia.Base/Logging/DebugLogSink.cs new file mode 100644 index 0000000000..3695afa860 --- /dev/null +++ b/src/Avalonia.Base/Logging/DebugLogSink.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using Avalonia.Utilities; + +namespace Avalonia.Logging +{ + public class DebugLogSink : ILogSink + { + private readonly LogEventLevel _level; + private readonly IList _areas; + + public DebugLogSink( + LogEventLevel minimumLevel, + IList areas = null) + { + _level = minimumLevel; + _areas = areas?.Count > 0 ? areas : null; + } + + public bool IsEnabled(LogEventLevel level, string area) + { + return level >= _level && (_areas?.Contains(area) ?? true); + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate) + { + if (IsEnabled(level, area)) + { + Debug.WriteLine(Format(area, messageTemplate, source)); + } + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate, T0 propertyValue0) + { + if (IsEnabled(level, area)) + { + Debug.WriteLine(Format(area, messageTemplate, source, propertyValue0)); + } + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + if (IsEnabled(level, area)) + { + Debug.WriteLine(Format(area, messageTemplate, source, propertyValue0, propertyValue1)); + } + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + if (IsEnabled(level, area)) + { + Debug.WriteLine(Format(area, messageTemplate, source, propertyValue0, propertyValue1, propertyValue2)); + } + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate, params object[] propertyValues) + { + if (IsEnabled(level, area)) + { + Debug.WriteLine(Format(area, messageTemplate, source, propertyValues)); + } + } + + private static string Format( + string area, + string template, + object source, + T0 v0 = default, + T1 v1 = default, + T2 v2 = default) + { + var result = new StringBuilder(template.Length); + var r = new CharacterReader(template.AsSpan()); + var i = 0; + + result.Append('['); + result.Append(area); + result.Append("] "); + + while (!r.End) + { + var c = r.Take(); + + if (c != '{') + { + result.Append(c); + } + else + { + if (r.Peek != '{') + { + result.Append('\''); + result.Append(i++ switch + { + 0 => v0, + 1 => v1, + 2 => v2, + _ => null + }); + result.Append('\''); + r.TakeUntil('}'); + r.Take(); + } + else + { + result.Append('{'); + r.Take(); + } + } + } + + if (source is object) + { + result.Append(" ("); + result.Append(source.GetType().Name); + result.Append(" #"); + result.Append(source.GetHashCode()); + result.Append(')'); + } + + return result.ToString(); + } + + private static string Format( + string area, + string template, + object source, + object[] v) + { + var result = new StringBuilder(template.Length); + var r = new CharacterReader(template.AsSpan()); + var i = 0; + + result.Append('['); + result.Append(area); + result.Append(']'); + + while (!r.End) + { + var c = r.Take(); + + if (c != '{') + { + result.Append(c); + } + else + { + if (r.Peek != '{') + { + result.Append('\''); + result.Append(i < v.Length ? v[i++] : null); + result.Append('\''); + r.TakeUntil('}'); + r.Take(); + } + else + { + result.Append('{'); + r.Take(); + } + } + } + + if (source is object) + { + result.Append('('); + result.Append(source.GetType().Name); + result.Append(" #"); + result.Append(source.GetHashCode()); + result.Append(')'); + } + + return result.ToString(); + } + } +} diff --git a/src/Avalonia.Base/Logging/ILogSink.cs b/src/Avalonia.Base/Logging/ILogSink.cs index 1649679b95..71268d5965 100644 --- a/src/Avalonia.Base/Logging/ILogSink.cs +++ b/src/Avalonia.Base/Logging/ILogSink.cs @@ -6,11 +6,12 @@ namespace Avalonia.Logging public interface ILogSink { /// - /// Checks if given log level is enabled. + /// Checks if given log level and area is enabled. /// /// The log event level. + /// The log area. /// if given log level is enabled. - bool IsEnabled(LogEventLevel level); + bool IsEnabled(LogEventLevel level, string area); /// /// Logs an event. diff --git a/src/Avalonia.Base/Logging/Logger.cs b/src/Avalonia.Base/Logging/Logger.cs index 136f56a620..ed3fad93fc 100644 --- a/src/Avalonia.Base/Logging/Logger.cs +++ b/src/Avalonia.Base/Logging/Logger.cs @@ -14,36 +14,39 @@ namespace Avalonia.Logging /// Checks if given log level is enabled. /// /// The log event level. + /// The log area. /// if given log level is enabled. - public static bool IsEnabled(LogEventLevel level) + public static bool IsEnabled(LogEventLevel level, string area) { - return Sink?.IsEnabled(level) == true; + return Sink?.IsEnabled(level, area) == true; } /// /// Returns parametrized logging sink if given log level is enabled. /// /// The log event level. + /// The area that the event originates from. /// Log sink or if log level is not enabled. - public static ParametrizedLogger? TryGet(LogEventLevel level) + public static ParametrizedLogger? TryGet(LogEventLevel level, string area) { - if (!IsEnabled(level)) + if (!IsEnabled(level, area)) { return null; } - return new ParametrizedLogger(Sink, level); + return new ParametrizedLogger(Sink, level, area); } /// /// Returns parametrized logging sink if given log level is enabled. /// /// The log event level. + /// The area that the event originates from. /// Log sink that is valid only if method returns . /// if logger was obtained successfully. - public static bool TryGet(LogEventLevel level, out ParametrizedLogger outLogger) + public static bool TryGet(LogEventLevel level, string area, out ParametrizedLogger outLogger) { - ParametrizedLogger? logger = TryGet(level); + ParametrizedLogger? logger = TryGet(level, area); outLogger = logger.GetValueOrDefault(); diff --git a/src/Avalonia.Base/Logging/ParametrizedLogger.cs b/src/Avalonia.Base/Logging/ParametrizedLogger.cs index 3dfb3c1ecf..adadb0f990 100644 --- a/src/Avalonia.Base/Logging/ParametrizedLogger.cs +++ b/src/Avalonia.Base/Logging/ParametrizedLogger.cs @@ -9,11 +9,13 @@ namespace Avalonia.Logging { private readonly ILogSink _sink; private readonly LogEventLevel _level; + private readonly string _area; - public ParametrizedLogger(ILogSink sink, LogEventLevel level) + public ParametrizedLogger(ILogSink sink, LogEventLevel level, string area) { _sink = sink; _level = level; + _area = area; } /// @@ -24,58 +26,51 @@ namespace Avalonia.Logging /// /// Logs an event. /// - /// The area that the event originates. /// The object from which the event originates. /// The message template. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Log( - string area, object source, string messageTemplate) { - _sink.Log(_level, area, source, messageTemplate); + _sink.Log(_level, _area, source, messageTemplate); } /// /// Logs an event. /// - /// The area that the event originates. /// The object from which the event originates. /// The message template. /// Message property value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Log( - string area, object source, string messageTemplate, T0 propertyValue0) { - _sink.Log(_level, area, source, messageTemplate, propertyValue0); + _sink.Log(_level, _area, source, messageTemplate, propertyValue0); } /// /// Logs an event. /// - /// The area that the event originates. /// The object from which the event originates. /// The message template. /// Message property value. /// Message property value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Log( - string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1) { - _sink.Log(_level, area, source, messageTemplate, propertyValue0, propertyValue1); + _sink.Log(_level, _area, source, messageTemplate, propertyValue0, propertyValue1); } /// /// Logs an event. /// - /// The area that the event originates. /// The object from which the event originates. /// The message template. /// Message property value. @@ -83,20 +78,18 @@ namespace Avalonia.Logging /// Message property value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Log( - string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) { - _sink.Log(_level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + _sink.Log(_level, _area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2); } /// /// Logs an event. /// - /// The area that the event originates. /// The object from which the event originates. /// The message template. /// Message property value. @@ -105,7 +98,6 @@ namespace Avalonia.Logging /// Message property value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Log( - string area, object source, string messageTemplate, T0 propertyValue0, @@ -113,13 +105,12 @@ namespace Avalonia.Logging T2 propertyValue2, T3 propertyValue3) { - _sink.Log(_level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2, propertyValue3); + _sink.Log(_level, _area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2, propertyValue3); } /// /// Logs an event. /// - /// The area that the event originates. /// The object from which the event originates. /// The message template. /// Message property value. @@ -129,7 +120,6 @@ namespace Avalonia.Logging /// Message property value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Log( - string area, object source, string messageTemplate, T0 propertyValue0, @@ -138,13 +128,12 @@ namespace Avalonia.Logging T3 propertyValue3, T4 propertyValue4) { - _sink.Log(_level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2, propertyValue3, propertyValue4); + _sink.Log(_level, _area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2, propertyValue3, propertyValue4); } /// /// Logs an event. /// - /// The area that the event originates. /// The object from which the event originates. /// The message template. /// Message property value. @@ -155,7 +144,6 @@ namespace Avalonia.Logging /// Message property value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Log( - string area, object source, string messageTemplate, T0 propertyValue0, @@ -165,7 +153,7 @@ namespace Avalonia.Logging T4 propertyValue4, T5 propertyValue5) { - _sink.Log(_level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2, propertyValue3, propertyValue4, propertyValue5); + _sink.Log(_level, _area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2, propertyValue3, propertyValue4, propertyValue5); } } } diff --git a/src/Avalonia.Base/Metadata/NullableAttributes.cs b/src/Avalonia.Base/Metadata/NullableAttributes.cs new file mode 100644 index 0000000000..91f5e81863 --- /dev/null +++ b/src/Avalonia.Base/Metadata/NullableAttributes.cs @@ -0,0 +1,140 @@ +#pragma warning disable MA0048 // File name must match type name +#define INTERNAL_NULLABLE_ATTRIBUTES +#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + +// https://github.com/dotnet/corefx/blob/48363ac826ccf66fbe31a5dcb1dc2aab9a7dd768/src/Common/src/CoreLib/System/Diagnostics/CodeAnalysis/NullableAttributes.cs + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class AllowNullAttribute : Attribute + { } + + /// Specifies that null is disallowed as an input even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class DisallowNullAttribute : Attribute + { } + + /// Specifies that an output may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class MaybeNullAttribute : Attribute + { } + + /// Specifies that an output will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class NotNullAttribute : Attribute + { } + + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class MaybeNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class NotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that the output will be non-null if the named parameter is non-null. + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class NotNullIfNotNullAttribute : Attribute + { + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } + } + + /// Applied to a method that will never return under any circumstance. + [AttributeUsage(AttributeTargets.Method, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class DoesNotReturnAttribute : Attribute + { } + + /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class DoesNotReturnIfAttribute : Attribute + { + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + /// Gets the condition parameter value. + public bool ParameterValue { get; } + } +} +#endif diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs index 75d58f45d5..692982cdc6 100644 --- a/src/Avalonia.Base/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Base/Properties/AssemblyInfo.cs @@ -7,4 +7,5 @@ using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Data.Converters")] [assembly: InternalsVisibleTo("Avalonia.Base.UnitTests")] [assembly: InternalsVisibleTo("Avalonia.UnitTests")] -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] +[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid")] diff --git a/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs index 6ed6c2ef52..4d82381323 100644 --- a/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs @@ -1,7 +1,4 @@ -using System; -using Avalonia.Data; - -#nullable enable +#nullable enable namespace Avalonia.PropertyStore { @@ -10,8 +7,6 @@ namespace Avalonia.PropertyStore /// internal interface IPriorityValueEntry : IValue { - BindingPriority Priority { get; } - void Reparent(IValueSink sink); } diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs index 59c017bc09..859e9ba81c 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs @@ -1,4 +1,5 @@ -using Avalonia.Data; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Data; #nullable enable @@ -11,9 +12,9 @@ namespace Avalonia.PropertyStore /// The property type. internal class LocalValueEntry : IValue { - private T _value; + [AllowNull] private T _value; - public LocalValueEntry(T value) => _value = value; + public LocalValueEntry([AllowNull] T value) => _value = value; public BindingPriority Priority => BindingPriority.LocalValue; Optional IValue.GetValue() => new Optional(_value); diff --git a/src/Avalonia.Base/Reactive/TypedBindingAdapter.cs b/src/Avalonia.Base/Reactive/TypedBindingAdapter.cs index bd9b31b100..b99cef0f51 100644 --- a/src/Avalonia.Base/Reactive/TypedBindingAdapter.cs +++ b/src/Avalonia.Base/Reactive/TypedBindingAdapter.cs @@ -32,8 +32,7 @@ namespace Avalonia.Reactive } catch (InvalidCastException e) { - Logger.TryGet(LogEventLevel.Error)?.Log( - LogArea.Binding, + Logger.TryGet(LogEventLevel.Error, LogArea.Binding)?.Log( _target, "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", _property.Name, diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index 300548db0a..cf0a0c34ec 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using Avalonia.Data; namespace Avalonia @@ -35,7 +34,7 @@ namespace Avalonia /// /// Gets the value coercion callback, if any. /// - public Func? CoerceValue { get; private set; } + public Func CoerceValue { get; private set; } object IStyledPropertyMetadata.DefaultValue => DefaultValue; diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index ebafc8b946..56cde9738e 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs @@ -14,7 +14,7 @@ namespace Avalonia.Threading private readonly DispatcherPriority _priority; private TimeSpan _interval; - + /// /// Initializes a new instance of the class. /// @@ -154,6 +154,8 @@ namespace Avalonia.Threading TimeSpan interval, DispatcherPriority priority = DispatcherPriority.Normal) { + interval = (interval != TimeSpan.Zero) ? interval : TimeSpan.FromTicks(1); + var timer = new DispatcherTimer(priority) { Interval = interval }; timer.Tick += (s, e) => @@ -197,7 +199,7 @@ namespace Avalonia.Threading } } - + /// /// Raises the event on the dispatcher thread. diff --git a/src/Avalonia.Base/Utilities/CharacterReader.cs b/src/Avalonia.Base/Utilities/CharacterReader.cs index 0a05915802..2b98785d0d 100644 --- a/src/Avalonia.Base/Utilities/CharacterReader.cs +++ b/src/Avalonia.Base/Utilities/CharacterReader.cs @@ -79,5 +79,25 @@ namespace Avalonia.Utilities Position += len; return span; } + + public ReadOnlySpan TryPeek(int count) + { + if (_s.Length < count) + return ReadOnlySpan.Empty; + return _s.Slice(0, count); + } + + public ReadOnlySpan PeekWhitespace() + { + var trimmed = _s.TrimStart(); + return _s.Slice(0, _s.Length - trimmed.Length); + } + + public void Skip(int count) + { + if (_s.Length < count) + throw new IndexOutOfRangeException(); + _s = _s.Slice(count); + } } } diff --git a/src/Avalonia.Base/Utilities/KeywordParser.cs b/src/Avalonia.Base/Utilities/KeywordParser.cs new file mode 100644 index 0000000000..16ef95f5f4 --- /dev/null +++ b/src/Avalonia.Base/Utilities/KeywordParser.cs @@ -0,0 +1,46 @@ +using System; + +namespace Avalonia.Utilities +{ +#if !BUILDTASK + public +#endif + static class KeywordParser + { + public static bool CheckKeyword(this ref CharacterReader r, string keyword) + { + return (CheckKeywordInternal(ref r, keyword) >= 0); + } + + static int CheckKeywordInternal(this ref CharacterReader r, string keyword) + { + var ws = r.PeekWhitespace(); + + var chars = r.TryPeek(ws.Length + keyword.Length); + if (chars.IsEmpty) + return -1; + if (SpanEquals(chars.Slice(ws.Length), keyword.AsSpan())) + return chars.Length; + return -1; + } + + static bool SpanEquals(ReadOnlySpan left, ReadOnlySpan right) + { + if (left.Length != right.Length) + return false; + for(var c=0; c public static class MathUtilities { + // smallest such that 1.0+DoubleEpsilon != 1.0 + internal static readonly double DoubleEpsilon = 2.2204460492503131e-016; + + private const float FloatEpsilon = 1.192092896e-07F; + /// /// AreClose - Returns whether or not two doubles are "close". That is, whether or /// not they are within epsilon of each other. @@ -18,11 +22,26 @@ namespace Avalonia.Utilities { //in case they are Infinities (then epsilon check does not work) if (value1 == value2) return true; - double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * double.Epsilon; + double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * DoubleEpsilon; double delta = value1 - value2; return (-eps < delta) && (eps > delta); } + /// + /// AreClose - Returns whether or not two floats are "close". That is, whether or + /// not they are within epsilon of each other. + /// + /// The first float to compare. + /// The second float to compare. + public static bool AreClose(float value1, float value2) + { + //in case they are Infinities (then epsilon check does not work) + if (value1 == value2) return true; + float eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0f) * FloatEpsilon; + float delta = value1 - value2; + return (-eps < delta) && (eps > delta); + } + /// /// LessThan - Returns whether or not the first double is less than the second double. /// That is, whether or not the first is strictly less than *and* not within epsilon of @@ -35,6 +54,18 @@ namespace Avalonia.Utilities return (value1 < value2) && !AreClose(value1, value2); } + /// + /// LessThan - Returns whether or not the first float is less than the second float. + /// That is, whether or not the first is strictly less than *and* not within epsilon of + /// the other number. + /// + /// The first single float to compare. + /// The second single float to compare. + public static bool LessThan(float value1, float value2) + { + return (value1 < value2) && !AreClose(value1, value2); + } + /// /// GreaterThan - Returns whether or not the first double is greater than the second double. /// That is, whether or not the first is strictly greater than *and* not within epsilon of @@ -47,6 +78,18 @@ namespace Avalonia.Utilities return (value1 > value2) && !AreClose(value1, value2); } + /// + /// GreaterThan - Returns whether or not the first float is greater than the second float. + /// That is, whether or not the first is strictly greater than *and* not within epsilon of + /// the other number. + /// + /// The first float to compare. + /// The second float to compare. + public static bool GreaterThan(float value1, float value2) + { + return (value1 > value2) && !AreClose(value1, value2); + } + /// /// LessThanOrClose - Returns whether or not the first double is less than or close to /// the second double. That is, whether or not the first is strictly less than or within @@ -59,6 +102,18 @@ namespace Avalonia.Utilities return (value1 < value2) || AreClose(value1, value2); } + /// + /// LessThanOrClose - Returns whether or not the first float is less than or close to + /// the second float. That is, whether or not the first is strictly less than or within + /// epsilon of the other number. + /// + /// The first float to compare. + /// The second float to compare. + public static bool LessThanOrClose(float value1, float value2) + { + return (value1 < value2) || AreClose(value1, value2); + } + /// /// GreaterThanOrClose - Returns whether or not the first double is greater than or close to /// the second double. That is, whether or not the first is strictly greater than or within @@ -71,6 +126,18 @@ namespace Avalonia.Utilities return (value1 > value2) || AreClose(value1, value2); } + /// + /// GreaterThanOrClose - Returns whether or not the first float is greater than or close to + /// the second float. That is, whether or not the first is strictly greater than or within + /// epsilon of the other number. + /// + /// The first float to compare. + /// The second float to compare. + public static bool GreaterThanOrClose(float value1, float value2) + { + return (value1 > value2) || AreClose(value1, value2); + } + /// /// IsOne - Returns whether or not the double is "close" to 1. Same as AreClose(double, 1), /// but this is faster. @@ -78,7 +145,17 @@ namespace Avalonia.Utilities /// The double to compare to 1. public static bool IsOne(double value) { - return Math.Abs(value - 1.0) < 10.0 * double.Epsilon; + return Math.Abs(value - 1.0) < 10.0 * DoubleEpsilon; + } + + /// + /// IsOne - Returns whether or not the float is "close" to 1. Same as AreClose(float, 1), + /// but this is faster. + /// + /// The float to compare to 1. + public static bool IsOne(float value) + { + return Math.Abs(value - 1.0f) < 10.0f * FloatEpsilon; } /// @@ -88,7 +165,17 @@ namespace Avalonia.Utilities /// The double to compare to 0. public static bool IsZero(double value) { - return Math.Abs(value) < 10.0 * double.Epsilon; + return Math.Abs(value) < 10.0 * DoubleEpsilon; + } + + /// + /// IsZero - Returns whether or not the float is "close" to 0. Same as AreClose(float, 0), + /// but this is faster. + /// + /// The float to compare to 0. + public static bool IsZero(float value) + { + return Math.Abs(value) < 10.0f * FloatEpsilon; } /// @@ -100,6 +187,11 @@ namespace Avalonia.Utilities /// The clamped value. public static double Clamp(double val, double min, double max) { + if (min > max) + { + ThrowCannotBeGreaterThanException(min, max); + } + if (val < min) { return min; @@ -114,39 +206,6 @@ namespace Avalonia.Utilities } } - /// - /// Calculates the value to be used for layout rounding at high DPI. - /// - /// Input value to be rounded. - /// Ratio of screen's DPI to layout DPI - /// Adjusted value that will produce layout rounding on screen at high dpi. - /// This is a layout helper method. It takes DPI into account and also does not return - /// the rounded value if it is unacceptable for layout, e.g. Infinity or NaN. It's a helper associated with - /// UseLayoutRounding property and should not be used as a general rounding utility. - public static double RoundLayoutValue(double value, double dpiScale) - { - double newValue; - - // If DPI == 1, don't use DPI-aware rounding. - if (!MathUtilities.AreClose(dpiScale, 1.0)) - { - newValue = Math.Round(value * dpiScale) / dpiScale; - // If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value. - if (double.IsNaN(newValue) || - double.IsInfinity(newValue) || - MathUtilities.AreClose(newValue, double.MaxValue)) - { - newValue = value; - } - } - else - { - newValue = Math.Round(value); - } - - return newValue; - } - /// /// Clamps a value between a minimum and maximum value. /// @@ -158,7 +217,7 @@ namespace Avalonia.Utilities { if (min > max) { - throw new ArgumentException($"{min} cannot be greater than {max}."); + ThrowCannotBeGreaterThanException(min, max); } if (val < min) @@ -174,5 +233,40 @@ namespace Avalonia.Utilities return val; } } + + /// + /// Converts an angle in degrees to radians. + /// + /// The angle in degrees. + /// The angle in radians. + public static double Deg2Rad(double angle) + { + return angle * (Math.PI / 180d); + } + + /// + /// Converts an angle in gradians to radians. + /// + /// The angle in gradians. + /// The angle in radians. + public static double Grad2Rad(double angle) + { + return angle * (Math.PI / 200d); + } + + /// + /// Converts an angle in turns to radians. + /// + /// The angle in turns. + /// The angle in radians. + public static double Turn2Rad(double angle) + { + return angle * 2 * Math.PI; + } + + private static void ThrowCannotBeGreaterThanException(double min, double max) + { + throw new ArgumentException($"{min} cannot be greater than {max}."); + } } } diff --git a/src/Avalonia.Base/Utilities/StyleClassParser.cs b/src/Avalonia.Base/Utilities/StyleClassParser.cs new file mode 100644 index 0000000000..2db58f73d9 --- /dev/null +++ b/src/Avalonia.Base/Utilities/StyleClassParser.cs @@ -0,0 +1,45 @@ +using System; +using System.Globalization; + +namespace Avalonia.Utilities +{ +#if !BUILDTASK + public +#endif + static class StyleClassParser + { + public static ReadOnlySpan ParseStyleClass(this ref CharacterReader r) + { + if (IsValidIdentifierStart(r.Peek)) + { + return r.TakeWhile(c => IsValidIdentifierChar(c)); + } + else + { + return ReadOnlySpan.Empty; + } + } + + private static bool IsValidIdentifierStart(char c) + { + return char.IsLetter(c) || c == '_'; + } + + private static bool IsValidIdentifierChar(char c) + { + if (IsValidIdentifierStart(c) || c == '-') + { + return true; + } + else + { + var cat = CharUnicodeInfo.GetUnicodeCategory(c); + return cat == UnicodeCategory.NonSpacingMark || + cat == UnicodeCategory.SpacingCombiningMark || + cat == UnicodeCategory.ConnectorPunctuation || + cat == UnicodeCategory.Format || + cat == UnicodeCategory.DecimalDigitNumber; + } + } + } +} diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 05e66f2e0a..6b89fcbdb9 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -162,7 +162,7 @@ namespace Avalonia _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( _owner, property, - old, + new Optional(old), default, BindingPriority.Unset)); } diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 582e4499c5..7117fd51a2 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -5,7 +5,7 @@ exe false tools - $(DefineConstants);BUILDTASK;XAMLIL_CECIL_INTERNAL;XAMLIL_INTERNAL + $(DefineConstants);BUILDTASK;XAMLX_CECIL_INTERNAL;XAMLX_INTERNAL true NU1605 @@ -21,15 +21,18 @@ XamlIlExtensions/%(RecursiveDir)%(FileName)%(Extension) - + XamlIl/%(RecursiveDir)%(FileName)%(Extension) - + XamlIl.Cecil/%(RecursiveDir)%(FileName)%(Extension) Markup/%(RecursiveDir)%(FileName)%(Extension) + + Markup/%(RecursiveDir)%(FileName)%(Extension) + Markup/%(RecursiveDir)%(FileName)%(Extension) @@ -38,33 +41,25 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) - + Markup/%(RecursiveDir)%(FileName)%(Extension) - + + Markup/%(RecursiveDir)%(FileName)%(Extension) + + + Markup/%(RecursiveDir)%(FileName)%(Extension) + + + Markup/%(RecursiveDir)%(FileName)%(Extension) + + + Markup/%(RecursiveDir)%(FileName)%(Extension) + - + - - - - - - $(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..c1cc0e7bf0 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")) @@ -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/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index cfe47a09d5..7e893d131e 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -2681,7 +2681,7 @@ namespace Avalonia.Controls { return; } - Debug.Assert(DoubleUtil.LessThanOrClose(_vScrollBar.Value, _vScrollBar.Maximum)); + Debug.Assert(MathUtilities.LessThanOrClose(_vScrollBar.Value, _vScrollBar.Maximum)); _verticalScrollChangesIgnored++; try @@ -2698,7 +2698,7 @@ namespace Avalonia.Controls } else if (scrollEventType == ScrollEventType.SmallDecrement) { - if (DoubleUtil.GreaterThan(NegVerticalOffset, 0)) + if (MathUtilities.GreaterThan(NegVerticalOffset, 0)) { DisplayData.PendingVerticalScrollHeight -= NegVerticalOffset; } @@ -2717,7 +2717,7 @@ namespace Avalonia.Controls DisplayData.PendingVerticalScrollHeight = _vScrollBar.Value - _verticalOffset; } - if (!DoubleUtil.IsZero(DisplayData.PendingVerticalScrollHeight)) + if (!MathUtilities.IsZero(DisplayData.PendingVerticalScrollHeight)) { // Invalidate so the scroll happens on idle InvalidateRowsMeasure(invalidateIndividualElements: false); @@ -3346,22 +3346,22 @@ namespace Avalonia.Controls bool needHorizScrollbarWithoutVertScrollbar = false; if (allowHorizScrollbar && - DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) && - DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth) && - DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight)) + MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) && + MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth) && + MathUtilities.LessThanOrClose(horizScrollBarHeight, cellsHeight)) { double oldDataHeight = cellsHeight; cellsHeight -= horizScrollBarHeight; Debug.Assert(cellsHeight >= 0); needHorizScrollbarWithoutVertScrollbar = needHorizScrollbar = true; - if (allowVertScrollbar && (DoubleUtil.LessThanOrClose(totalVisibleWidth - cellsWidth, vertScrollBarWidth) || - DoubleUtil.LessThanOrClose(cellsWidth - totalVisibleFrozenWidth, vertScrollBarWidth))) + if (allowVertScrollbar && (MathUtilities.LessThanOrClose(totalVisibleWidth - cellsWidth, vertScrollBarWidth) || + MathUtilities.LessThanOrClose(cellsWidth - totalVisibleFrozenWidth, vertScrollBarWidth))) { // Would we still need a horizontal scrollbar without the vertical one? UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight); if (DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount) { - needHorizScrollbar = DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth - vertScrollBarWidth); + needHorizScrollbar = MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth - vertScrollBarWidth); } } @@ -3374,8 +3374,8 @@ namespace Avalonia.Controls UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight); if (allowVertScrollbar && - DoubleUtil.GreaterThan(cellsHeight, 0) && - DoubleUtil.LessThanOrClose(vertScrollBarWidth, cellsWidth) && + MathUtilities.GreaterThan(cellsHeight, 0) && + MathUtilities.LessThanOrClose(vertScrollBarWidth, cellsWidth) && DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount) { cellsWidth -= vertScrollBarWidth; @@ -3389,9 +3389,9 @@ namespace Avalonia.Controls if (allowHorizScrollbar && needVertScrollbar && !needHorizScrollbar && - DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) && - DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth) && - DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight)) + MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) && + MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth) && + MathUtilities.LessThanOrClose(horizScrollBarHeight, cellsHeight)) { cellsWidth += vertScrollBarWidth; cellsHeight -= horizScrollBarHeight; @@ -3422,7 +3422,7 @@ namespace Avalonia.Controls if (allowVertScrollbar) { if (cellsHeight > 0 && - DoubleUtil.LessThanOrClose(vertScrollBarWidth, cellsWidth) && + MathUtilities.LessThanOrClose(vertScrollBarWidth, cellsWidth) && DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount) { cellsWidth -= vertScrollBarWidth; @@ -3439,9 +3439,9 @@ namespace Avalonia.Controls if (allowHorizScrollbar) { if (cellsWidth > 0 && - DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight) && - DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) && - DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth)) + MathUtilities.LessThanOrClose(horizScrollBarHeight, cellsHeight) && + MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) && + MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth)) { cellsHeight -= horizScrollBarHeight; Debug.Assert(cellsHeight >= 0); @@ -5387,7 +5387,7 @@ namespace Avalonia.Controls private void SetVerticalOffset(double newVerticalOffset) { _verticalOffset = newVerticalOffset; - if (_vScrollBar != null && !DoubleUtil.AreClose(newVerticalOffset, _vScrollBar.Value)) + if (_vScrollBar != null && !MathUtilities.AreClose(newVerticalOffset, _vScrollBar.Value)) { _vScrollBar.Value = _verticalOffset; } diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index df2b03798a..e6cc7e5e40 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -301,7 +301,7 @@ namespace Avalonia.Controls private static bool CanResizeColumn(DataGridColumn column) { if (column.OwningGrid != null && column.OwningGrid.ColumnsInternal != null && column.OwningGrid.UsesStarSizing && - (column.OwningGrid.ColumnsInternal.LastVisibleColumn == column || !DoubleUtil.AreClose(column.OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth, column.OwningGrid.CellsWidth))) + (column.OwningGrid.ColumnsInternal.LastVisibleColumn == column || !MathUtilities.AreClose(column.OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth, column.OwningGrid.CellsWidth))) { return false; } diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs index 16b63ad696..5b75bc73f9 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs @@ -44,7 +44,7 @@ namespace Avalonia.Controls /// The remaining amount of adjustment. internal double AdjustColumnWidths(int displayIndex, double amount, bool userInitiated) { - if (!DoubleUtil.IsZero(amount)) + if (!MathUtilities.IsZero(amount)) { if (amount < 0) { @@ -777,7 +777,7 @@ namespace Avalonia.Controls private double AdjustStarColumnWidths(int displayIndex, double adjustment, bool userInitiated) { double remainingAdjustment = adjustment; - if (DoubleUtil.IsZero(remainingAdjustment)) + if (MathUtilities.IsZero(remainingAdjustment)) { return remainingAdjustment; } @@ -843,7 +843,7 @@ namespace Avalonia.Controls /// The remaining amount of adjustment. private double AdjustStarColumnWidths(int displayIndex, double remainingAdjustment, bool userInitiated, Func targetWidth) { - if (DoubleUtil.IsZero(remainingAdjustment)) + if (MathUtilities.IsZero(remainingAdjustment)) { return remainingAdjustment; } @@ -1244,7 +1244,7 @@ namespace Avalonia.Controls Debug.Assert(amount < 0); Debug.Assert(column.Width.UnitType != DataGridLengthUnitType.Star); - if (DoubleUtil.GreaterThanOrClose(targetWidth, column.Width.DisplayValue)) + if (MathUtilities.GreaterThanOrClose(targetWidth, column.Width.DisplayValue)) { return amount; } @@ -1271,7 +1271,7 @@ namespace Avalonia.Controls /// The remaining amount of adjustment. private double DecreaseNonStarColumnWidths(int displayIndex, Func targetWidth, double amount, bool reverse, bool affectNewColumns) { - if (DoubleUtil.GreaterThanOrClose(amount, 0)) + if (MathUtilities.GreaterThanOrClose(amount, 0)) { return amount; } @@ -1285,7 +1285,7 @@ namespace Avalonia.Controls (affectNewColumns || column.IsInitialDesiredWidthDetermined))) { amount = DecreaseNonStarColumnWidth(column, Math.Max(column.ActualMinWidth, targetWidth(column)), amount); - if (DoubleUtil.IsZero(amount)) + if (MathUtilities.IsZero(amount)) { break; } @@ -1392,7 +1392,7 @@ namespace Avalonia.Controls /// The remaining amount of adjustment. private double IncreaseNonStarColumnWidths(int displayIndex, Func targetWidth, double amount, bool reverse, bool affectNewColumns) { - if (DoubleUtil.LessThanOrClose(amount, 0)) + if (MathUtilities.LessThanOrClose(amount, 0)) { return amount; } @@ -1406,7 +1406,7 @@ namespace Avalonia.Controls (affectNewColumns || column.IsInitialDesiredWidthDetermined))) { amount = IncreaseNonStarColumnWidth(column, Math.Min(column.ActualMaxWidth, targetWidth(column)), amount); - if (DoubleUtil.IsZero(amount)) + if (MathUtilities.IsZero(amount)) { break; } diff --git a/src/Avalonia.Controls.DataGrid/DataGridLength.cs b/src/Avalonia.Controls.DataGrid/DataGridLength.cs index 6a545a35ec..4841ddd494 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridLength.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridLength.cs @@ -529,7 +529,7 @@ namespace Avalonia.Controls // in this case drop value part and print only "Star" case DataGridLengthUnitType.Star: return ( - DoubleUtil.AreClose(1.0, dataGridLength.Value.Value) + MathUtilities.AreClose(1.0, dataGridLength.Value.Value) ? _starSuffix : Convert.ToString(dataGridLength.Value.Value, culture ?? CultureInfo.CurrentCulture) + DataGridLengthConverter._starSuffix); diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index df200240ff..830eff1102 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -879,7 +879,7 @@ namespace Avalonia.Controls && (double.IsNaN(_detailsContent.Height)) && (AreDetailsVisible) && (!double.IsNaN(_detailsDesiredHeight)) - && !DoubleUtil.AreClose(_detailsContent.Bounds.Inflate(_detailsContent.Margin).Height, _detailsDesiredHeight) + && !MathUtilities.AreClose(_detailsContent.Bounds.Inflate(_detailsContent.Margin).Height, _detailsDesiredHeight) && Slot != -1) { _detailsDesiredHeight = _detailsContent.Bounds.Inflate(_detailsContent.Margin).Height; diff --git a/src/Avalonia.Controls.DataGrid/DataGridRows.cs b/src/Avalonia.Controls.DataGrid/DataGridRows.cs index fda14cf5b4..924156f5f4 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRows.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRows.cs @@ -329,7 +329,7 @@ namespace Avalonia.Controls internal void OnRowsMeasure() { - if (!DoubleUtil.IsZero(DisplayData.PendingVerticalScrollHeight)) + if (!MathUtilities.IsZero(DisplayData.PendingVerticalScrollHeight)) { ScrollSlotsByHeight(DisplayData.PendingVerticalScrollHeight); DisplayData.PendingVerticalScrollHeight = 0; @@ -432,7 +432,7 @@ namespace Avalonia.Controls } else if (DisplayData.FirstScrollingSlot == slot && slot != -1) { - if (!DoubleUtil.IsZero(NegVerticalOffset)) + if (!MathUtilities.IsZero(NegVerticalOffset)) { // First displayed row is partially scrolled of. Let's scroll it so that NegVerticalOffset becomes 0. DisplayData.PendingVerticalScrollHeight = -NegVerticalOffset; @@ -447,7 +447,7 @@ namespace Avalonia.Controls { // Scroll up to the new row so it becomes the first displayed row firstFullSlot = DisplayData.FirstScrollingSlot - 1; - if (DoubleUtil.GreaterThan(NegVerticalOffset, 0)) + if (MathUtilities.GreaterThan(NegVerticalOffset, 0)) { deltaY = -NegVerticalOffset; } @@ -470,7 +470,7 @@ namespace Avalonia.Controls // Figure out how much of the last row is cut off double rowHeight = GetExactSlotElementHeight(DisplayData.LastScrollingSlot); double availableHeight = AvailableSlotElementRoom + rowHeight; - if (DoubleUtil.AreClose(rowHeight, availableHeight)) + if (MathUtilities.AreClose(rowHeight, availableHeight)) { if (DisplayData.LastScrollingSlot == slot) { @@ -499,7 +499,7 @@ namespace Avalonia.Controls { ResetDisplayedRows(); } - if (DoubleUtil.GreaterThanOrClose(GetExactSlotElementHeight(slot), CellsHeight)) + if (MathUtilities.GreaterThanOrClose(GetExactSlotElementHeight(slot), CellsHeight)) { // The entire row won't fit in the DataGrid so we start showing it from the top NegVerticalOffset = 0; @@ -519,7 +519,7 @@ namespace Avalonia.Controls } // - Debug.Assert(DoubleUtil.LessThanOrClose(NegVerticalOffset, _verticalOffset)); + Debug.Assert(MathUtilities.LessThanOrClose(NegVerticalOffset, _verticalOffset)); SetVerticalOffset(_verticalOffset); @@ -1660,7 +1660,7 @@ namespace Avalonia.Controls private void ScrollSlotsByHeight(double height) { Debug.Assert(DisplayData.FirstScrollingSlot >= 0); - Debug.Assert(!DoubleUtil.IsZero(height)); + Debug.Assert(!MathUtilities.IsZero(height)); _scrollingByHeight = true; try @@ -1672,7 +1672,7 @@ namespace Avalonia.Controls { // Scrolling Down int lastVisibleSlot = GetPreviousVisibleSlot(SlotCount); - if (_vScrollBar != null && DoubleUtil.AreClose(_vScrollBar.Maximum, newVerticalOffset)) + if (_vScrollBar != null && MathUtilities.AreClose(_vScrollBar.Maximum, newVerticalOffset)) { // We've scrolled to the bottom of the ScrollBar, automatically place the user at the very bottom // of the DataGrid. If this produces very odd behavior, evaluate the coping strategy used by @@ -1684,7 +1684,7 @@ namespace Avalonia.Controls else { deltaY = GetSlotElementHeight(newFirstScrollingSlot) - NegVerticalOffset; - if (DoubleUtil.LessThan(height, deltaY)) + if (MathUtilities.LessThan(height, deltaY)) { // We've merely covered up more of the same row we're on NegVerticalOffset += height; @@ -1707,7 +1707,7 @@ namespace Avalonia.Controls } else { - while (DoubleUtil.LessThanOrClose(deltaY, height)) + while (MathUtilities.LessThanOrClose(deltaY, height)) { if (newFirstScrollingSlot < lastVisibleSlot) { @@ -1727,7 +1727,7 @@ namespace Avalonia.Controls double rowHeight = GetExactSlotElementHeight(newFirstScrollingSlot); double remainingHeight = height - deltaY; - if (DoubleUtil.LessThanOrClose(rowHeight, remainingHeight)) + if (MathUtilities.LessThanOrClose(rowHeight, remainingHeight)) { deltaY += rowHeight; } @@ -1744,7 +1744,7 @@ namespace Avalonia.Controls else { // Scrolling Up - if (DoubleUtil.GreaterThanOrClose(height + NegVerticalOffset, 0)) + if (MathUtilities.GreaterThanOrClose(height + NegVerticalOffset, 0)) { // We've merely exposing more of the row we're on NegVerticalOffset += height; @@ -1778,7 +1778,7 @@ namespace Avalonia.Controls else { int lastScrollingSlot = DisplayData.LastScrollingSlot; - while (DoubleUtil.GreaterThan(deltaY, height)) + while (MathUtilities.GreaterThan(deltaY, height)) { if (newFirstScrollingSlot > 0) { @@ -1797,7 +1797,7 @@ namespace Avalonia.Controls } double rowHeight = GetExactSlotElementHeight(newFirstScrollingSlot); double remainingHeight = height - deltaY; - if (DoubleUtil.LessThanOrClose(rowHeight + remainingHeight, 0)) + if (MathUtilities.LessThanOrClose(rowHeight + remainingHeight, 0)) { deltaY -= rowHeight; } @@ -1809,7 +1809,7 @@ namespace Avalonia.Controls } } } - if (DoubleUtil.GreaterThanOrClose(0, newVerticalOffset) && newFirstScrollingSlot != 0) + if (MathUtilities.GreaterThanOrClose(0, newVerticalOffset) && newFirstScrollingSlot != 0) { // We've scrolled to the top of the ScrollBar, automatically place the user at the very top // of the DataGrid. If this produces very odd behavior, evaluate the RowHeight estimate. @@ -1822,7 +1822,7 @@ namespace Avalonia.Controls } double firstRowHeight = GetExactSlotElementHeight(newFirstScrollingSlot); - if (DoubleUtil.LessThan(firstRowHeight, NegVerticalOffset)) + if (MathUtilities.LessThan(firstRowHeight, NegVerticalOffset)) { // We've scrolled off more of the first row than what's possible. This can happen // if the first row got shorter (Ex: Collpasing RowDetails) or if the user has a recycling @@ -1838,11 +1838,11 @@ namespace Avalonia.Controls UpdateDisplayedRows(newFirstScrollingSlot, CellsHeight); double firstElementHeight = GetExactSlotElementHeight(DisplayData.FirstScrollingSlot); - if (DoubleUtil.GreaterThan(NegVerticalOffset, firstElementHeight)) + if (MathUtilities.GreaterThan(NegVerticalOffset, firstElementHeight)) { int firstElementSlot = DisplayData.FirstScrollingSlot; // We filled in some rows at the top and now we have a NegVerticalOffset that's greater than the first element - while (newFirstScrollingSlot > 0 && DoubleUtil.GreaterThan(NegVerticalOffset, firstElementHeight)) + while (newFirstScrollingSlot > 0 && MathUtilities.GreaterThan(NegVerticalOffset, firstElementHeight)) { int previousSlot = GetPreviousVisibleSlot(firstElementSlot); if (previousSlot == -1) @@ -1872,7 +1872,7 @@ namespace Avalonia.Controls { _verticalOffset = NegVerticalOffset; } - else if (DoubleUtil.GreaterThan(NegVerticalOffset, newVerticalOffset)) + else if (MathUtilities.GreaterThan(NegVerticalOffset, newVerticalOffset)) { // The scrolled-in row was larger than anticipated. Adjust the DataGrid so the ScrollBar thumb // can stay in the same place @@ -1890,8 +1890,8 @@ namespace Avalonia.Controls DisplayData.FullyRecycleElements(); - Debug.Assert(DoubleUtil.GreaterThanOrClose(NegVerticalOffset, 0)); - Debug.Assert(DoubleUtil.GreaterThanOrClose(_verticalOffset, NegVerticalOffset)); + Debug.Assert(MathUtilities.GreaterThanOrClose(NegVerticalOffset, 0)); + Debug.Assert(MathUtilities.GreaterThanOrClose(_verticalOffset, NegVerticalOffset)); } finally { @@ -2032,7 +2032,7 @@ namespace Avalonia.Controls double deltaY = -NegVerticalOffset; int visibleScrollingRows = 0; - if (DoubleUtil.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0) + if (MathUtilities.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0) { return; } @@ -2044,7 +2044,7 @@ namespace Avalonia.Controls } int slot = firstDisplayedScrollingSlot; - while (slot < SlotCount && !DoubleUtil.GreaterThanOrClose(deltaY, displayHeight)) + while (slot < SlotCount && !MathUtilities.GreaterThanOrClose(deltaY, displayHeight)) { deltaY += GetExactSlotElementHeight(slot); visibleScrollingRows++; @@ -2052,7 +2052,7 @@ namespace Avalonia.Controls slot = GetNextVisibleSlot(slot); } - while (DoubleUtil.LessThan(deltaY, displayHeight) && slot >= 0) + while (MathUtilities.LessThan(deltaY, displayHeight) && slot >= 0) { slot = GetPreviousVisibleSlot(firstDisplayedScrollingSlot); if (slot >= 0) @@ -2063,14 +2063,14 @@ namespace Avalonia.Controls } } // If we're up to the first row, and we still have room left, uncover as much of the first row as we can - if (firstDisplayedScrollingSlot == 0 && DoubleUtil.LessThan(deltaY, displayHeight)) + if (firstDisplayedScrollingSlot == 0 && MathUtilities.LessThan(deltaY, displayHeight)) { double newNegVerticalOffset = Math.Max(0, NegVerticalOffset - displayHeight + deltaY); deltaY += NegVerticalOffset - newNegVerticalOffset; NegVerticalOffset = newNegVerticalOffset; } - if (DoubleUtil.GreaterThan(deltaY, displayHeight) || (DoubleUtil.AreClose(deltaY, displayHeight) && DoubleUtil.GreaterThan(NegVerticalOffset, 0))) + if (MathUtilities.GreaterThan(deltaY, displayHeight) || (MathUtilities.AreClose(deltaY, displayHeight) && MathUtilities.GreaterThan(NegVerticalOffset, 0))) { DisplayData.NumTotallyDisplayedScrollingElements = visibleScrollingRows - 1; } @@ -2108,7 +2108,7 @@ namespace Avalonia.Controls double deltaY = 0; int visibleScrollingRows = 0; - if (DoubleUtil.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0) + if (MathUtilities.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0) { ResetDisplayedRows(); return; @@ -2120,7 +2120,7 @@ namespace Avalonia.Controls } int slot = lastDisplayedScrollingRow; - while (DoubleUtil.LessThan(deltaY, displayHeight) && slot >= 0) + while (MathUtilities.LessThan(deltaY, displayHeight) && slot >= 0) { deltaY += GetExactSlotElementHeight(slot); visibleScrollingRows++; @@ -2542,7 +2542,7 @@ namespace Avalonia.Controls double heightChange = UpdateRowGroupVisibility(rowGroupInfo, isVisible, isDisplayed: false); // Use epsilon instead of 0 here so that in the off chance that our estimates put the vertical offset negative // the user can still scroll to the top since the offset is non-zero - SetVerticalOffset(Math.Max(DoubleUtil.DBL_EPSILON, _verticalOffset + heightChange)); + SetVerticalOffset(Math.Max(MathUtilities.DoubleEpsilon, _verticalOffset + heightChange)); } else { @@ -3024,4 +3024,4 @@ namespace Avalonia.Controls } #endif } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs index 0f513e7f42..6e0703c90f 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs @@ -310,9 +310,9 @@ namespace Avalonia.Controls.Primitives double leftEdge = column.IsFrozen ? frozenLeftEdge : scrollingLeftEdge; double rightEdge = leftEdge + column.ActualWidth; return - DoubleUtil.GreaterThan(rightEdge, 0) && - DoubleUtil.LessThanOrClose(leftEdge, OwningGrid.CellsWidth) && - DoubleUtil.GreaterThan(rightEdge, frozenLeftEdge); // scrolling column covered up by frozen column(s) + MathUtilities.GreaterThan(rightEdge, 0) && + MathUtilities.LessThanOrClose(leftEdge, OwningGrid.CellsWidth) && + MathUtilities.GreaterThan(rightEdge, frozenLeftEdge); // scrolling column covered up by frozen column(s) } } } diff --git a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml index 17e7ecba43..738732f671 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml @@ -188,7 +188,7 @@ - diff --git a/src/Avalonia.Themes.Default/CaptionButtons.xaml b/src/Avalonia.Themes.Default/CaptionButtons.xaml new file mode 100644 index 0000000000..68249905a1 --- /dev/null +++ b/src/Avalonia.Themes.Default/CaptionButtons.xaml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/ComboBox.xaml b/src/Avalonia.Themes.Default/ComboBox.xaml index 95bd9550a5..ae5b902ae8 100644 --- a/src/Avalonia.Themes.Default/ComboBox.xaml +++ b/src/Avalonia.Themes.Default/ComboBox.xaml @@ -1,10 +1,29 @@ + + + + + Item 1 + Item 2 + + + Item 1 + Item 2 + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/Slider.xaml b/src/Avalonia.Themes.Default/Slider.xaml index b21cbf3650..ba4b1ee998 100644 --- a/src/Avalonia.Themes.Default/Slider.xaml +++ b/src/Avalonia.Themes.Default/Slider.xaml @@ -46,7 +46,7 @@ - + @@ -87,7 +87,10 @@ - + + diff --git a/src/Avalonia.Themes.Default/TitleBar.xaml b/src/Avalonia.Themes.Default/TitleBar.xaml new file mode 100644 index 0000000000..45798d3fa1 --- /dev/null +++ b/src/Avalonia.Themes.Default/TitleBar.xaml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/ToggleSwitch.xaml b/src/Avalonia.Themes.Default/ToggleSwitch.xaml new file mode 100644 index 0000000000..893d64f505 --- /dev/null +++ b/src/Avalonia.Themes.Default/ToggleSwitch.xaml @@ -0,0 +1,293 @@ + + + 0,0,0,6 + 6 + 6 + 154 + 20 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/Window.xaml b/src/Avalonia.Themes.Default/Window.xaml index 0e7ec42856..739887fb35 100644 --- a/src/Avalonia.Themes.Default/Window.xaml +++ b/src/Avalonia.Themes.Default/Window.xaml @@ -5,7 +5,9 @@ - + + + + + + + #FFF0F0F0 + #FF000000 + #FF6D6D6D + #FF3399FF + #FFFFFFFF + #FF0066CC + #FFFFFFFF + #FF000000 + avares://Avalonia.Themes.Fluent/Assets#Open Sans + 13 + + + True + 1 + 2 + 10,6,6,5 + + diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml new file mode 100644 index 0000000000..fb08b97b47 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml @@ -0,0 +1,346 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml new file mode 100644 index 0000000000..6c8d16ddfa --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml @@ -0,0 +1,345 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml new file mode 100644 index 0000000000..1c65911593 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml @@ -0,0 +1,455 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml new file mode 100644 index 0000000000..ad23955776 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml @@ -0,0 +1,458 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml new file mode 100644 index 0000000000..653e4733cb --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -0,0 +1,804 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml new file mode 100644 index 0000000000..6a2a04f732 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -0,0 +1,811 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml new file mode 100644 index 0000000000..9ef92a44d5 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml new file mode 100644 index 0000000000..8c92040122 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Assets/OpenSans-Bold.ttf b/src/Avalonia.Themes.Fluent/Assets/OpenSans-Bold.ttf new file mode 100644 index 0000000000..efdd5e84a0 Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/OpenSans-Bold.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/OpenSans-BoldItalic.ttf b/src/Avalonia.Themes.Fluent/Assets/OpenSans-BoldItalic.ttf new file mode 100644 index 0000000000..9bf9b4e97b Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/OpenSans-BoldItalic.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/OpenSans-ExtraBold.ttf b/src/Avalonia.Themes.Fluent/Assets/OpenSans-ExtraBold.ttf new file mode 100644 index 0000000000..67fcf0fb2a Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/OpenSans-ExtraBold.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/OpenSans-ExtraBoldItalic.ttf b/src/Avalonia.Themes.Fluent/Assets/OpenSans-ExtraBoldItalic.ttf new file mode 100644 index 0000000000..086722809c Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/OpenSans-ExtraBoldItalic.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/OpenSans-Italic.ttf b/src/Avalonia.Themes.Fluent/Assets/OpenSans-Italic.ttf new file mode 100644 index 0000000000..117856707b Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/OpenSans-Italic.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/OpenSans-Light.ttf b/src/Avalonia.Themes.Fluent/Assets/OpenSans-Light.ttf new file mode 100644 index 0000000000..6580d3a169 Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/OpenSans-Light.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/OpenSans-LightItalic.ttf b/src/Avalonia.Themes.Fluent/Assets/OpenSans-LightItalic.ttf new file mode 100644 index 0000000000..1e0c331981 Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/OpenSans-LightItalic.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/OpenSans-Regular.ttf b/src/Avalonia.Themes.Fluent/Assets/OpenSans-Regular.ttf new file mode 100644 index 0000000000..29bfd35a2b Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/OpenSans-Regular.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/OpenSans-SemiBold.ttf b/src/Avalonia.Themes.Fluent/Assets/OpenSans-SemiBold.ttf new file mode 100644 index 0000000000..54e7059cf3 Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/OpenSans-SemiBold.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/OpenSans-SemiBoldItalic.ttf b/src/Avalonia.Themes.Fluent/Assets/OpenSans-SemiBoldItalic.ttf new file mode 100644 index 0000000000..aebcf14212 Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/OpenSans-SemiBoldItalic.ttf differ diff --git a/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml b/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml new file mode 100644 index 0000000000..0d5d733cd9 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml @@ -0,0 +1,70 @@ + + + + + + Alabama + Alaska + Arizona + Arkansas + California + Colorado + Connecticut + Delaware + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj new file mode 100644 index 0000000000..c2a1359e8a --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -0,0 +1,20 @@ + + + netstandard2.0 + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Button.xaml b/src/Avalonia.Themes.Fluent/Button.xaml new file mode 100644 index 0000000000..f9a3601edd --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Button.xaml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/CaptionButtons.xaml b/src/Avalonia.Themes.Fluent/CaptionButtons.xaml new file mode 100644 index 0000000000..f288372b77 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/CaptionButtons.xaml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Carousel.xaml b/src/Avalonia.Themes.Fluent/Carousel.xaml new file mode 100644 index 0000000000..955a49a974 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Carousel.xaml @@ -0,0 +1,16 @@ + diff --git a/src/Avalonia.Themes.Fluent/CheckBox.xaml b/src/Avalonia.Themes.Fluent/CheckBox.xaml new file mode 100644 index 0000000000..5f82fed08c --- /dev/null +++ b/src/Avalonia.Themes.Fluent/CheckBox.xaml @@ -0,0 +1,293 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/ComboBox.xaml b/src/Avalonia.Themes.Fluent/ComboBox.xaml new file mode 100644 index 0000000000..2788344842 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ComboBox.xaml @@ -0,0 +1,227 @@ + + + + + + Item 1 + Item 2 + + + Item 1 + Item 2 + + + + + + 0,0,0,4 + 15 + 7 + + 12,5,0,7 + 11,5,32,6 + 32 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/ComboBoxItem.xaml b/src/Avalonia.Themes.Fluent/ComboBoxItem.xaml new file mode 100644 index 0000000000..66ccea99b5 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ComboBoxItem.xaml @@ -0,0 +1,89 @@ + + + + + + Item 1 + Item 2 long + Item 3 + Item 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Common.xaml b/src/Avalonia.Themes.Fluent/Common.xaml new file mode 100644 index 0000000000..e09e39d7cb --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Common.xaml @@ -0,0 +1,6 @@ + + + diff --git a/src/Avalonia.Themes.Fluent/ContentControl.xaml b/src/Avalonia.Themes.Fluent/ContentControl.xaml new file mode 100644 index 0000000000..d76eecee84 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ContentControl.xaml @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/src/Avalonia.Themes.Fluent/ContextMenu.xaml b/src/Avalonia.Themes.Fluent/ContextMenu.xaml new file mode 100644 index 0000000000..a6b6156944 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ContextMenu.xaml @@ -0,0 +1,62 @@ + diff --git a/src/Avalonia.Themes.Fluent/DataValidationErrors.xaml b/src/Avalonia.Themes.Fluent/DataValidationErrors.xaml new file mode 100644 index 0000000000..f4145a51f5 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/DataValidationErrors.xaml @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/DatePicker.xaml b/src/Avalonia.Themes.Fluent/DatePicker.xaml new file mode 100644 index 0000000000..8b669a30ed --- /dev/null +++ b/src/Avalonia.Themes.Fluent/DatePicker.xaml @@ -0,0 +1,336 @@ + + + + + 0,0,0,4 + 40 + 40 + 41 + 296 + 456 + 0,3,0,6 + 9,3,0,6 + 0,3,0,6 + 9,3,0,6 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml b/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml new file mode 100644 index 0000000000..f0f3e5ea16 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml @@ -0,0 +1,23 @@ + + + + diff --git a/src/Avalonia.Themes.Fluent/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Fluent/EmbeddableControlRoot.xaml new file mode 100644 index 0000000000..3ff47111eb --- /dev/null +++ b/src/Avalonia.Themes.Fluent/EmbeddableControlRoot.xaml @@ -0,0 +1,19 @@ + diff --git a/src/Avalonia.Themes.Fluent/Expander.xaml b/src/Avalonia.Themes.Fluent/Expander.xaml new file mode 100644 index 0000000000..e9eb939798 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Expander.xaml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml new file mode 100644 index 0000000000..00ab9bc375 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs new file mode 100644 index 0000000000..9c65a7227c --- /dev/null +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Markup.Xaml; +using Avalonia.Styling; + +namespace Avalonia.Themes.Fluent +{ + /// + /// The default Avalonia theme. + /// + public class FluentTheme : Styles + { + } +} diff --git a/src/Avalonia.Themes.Fluent/FocusAdorner.xaml b/src/Avalonia.Themes.Fluent/FocusAdorner.xaml new file mode 100644 index 0000000000..f20dc2e650 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/FocusAdorner.xaml @@ -0,0 +1,34 @@ + + + 0 + 2 + 1 + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/GridSplitter.xaml b/src/Avalonia.Themes.Fluent/GridSplitter.xaml new file mode 100644 index 0000000000..d68d74d884 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/GridSplitter.xaml @@ -0,0 +1,23 @@ + + + + + diff --git a/src/Avalonia.Themes.Fluent/InverseBooleanValueConverter.cs b/src/Avalonia.Themes.Fluent/InverseBooleanValueConverter.cs new file mode 100644 index 0000000000..20bade2a67 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/InverseBooleanValueConverter.cs @@ -0,0 +1,20 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Avalonia.Themes.Fluent +{ + class InverseBooleanValueConverter : IValueConverter + { + public bool Default { get; set; } + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is bool b ? !b : Default; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is bool b ? !b : !Default; + } + } +} diff --git a/src/Avalonia.Themes.Fluent/ItemsControl.xaml b/src/Avalonia.Themes.Fluent/ItemsControl.xaml new file mode 100644 index 0000000000..8bb0fc297c --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ItemsControl.xaml @@ -0,0 +1,15 @@ + diff --git a/src/Avalonia.Themes.Fluent/ListBox.xaml b/src/Avalonia.Themes.Fluent/ListBox.xaml new file mode 100644 index 0000000000..ae4fdca490 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ListBox.xaml @@ -0,0 +1,39 @@ + + + + + Test + Test + Test + Test + + + + + diff --git a/src/Avalonia.Themes.Fluent/ListBoxItem.xaml b/src/Avalonia.Themes.Fluent/ListBoxItem.xaml new file mode 100644 index 0000000000..430d3c5468 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ListBoxItem.xaml @@ -0,0 +1,97 @@ + + + + + Disabled + Test + Test + + + + + 12,9,12,12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Menu.xaml b/src/Avalonia.Themes.Fluent/Menu.xaml new file mode 100644 index 0000000000..5f22f77d18 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Menu.xaml @@ -0,0 +1,34 @@ + diff --git a/src/Avalonia.Themes.Fluent/MenuItem.xaml b/src/Avalonia.Themes.Fluent/MenuItem.xaml new file mode 100644 index 0000000000..fbb994e90c --- /dev/null +++ b/src/Avalonia.Themes.Fluent/MenuItem.xaml @@ -0,0 +1,259 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0,4,0,4 + 0,0,12,0 + 24,0,0,0 + M 1,0 10,10 l -9,10 -1,-1 L 8,10 -0,1 Z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/NativeMenuBar.xaml b/src/Avalonia.Themes.Fluent/NativeMenuBar.xaml new file mode 100644 index 0000000000..799fe6ffe4 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/NativeMenuBar.xaml @@ -0,0 +1,26 @@ + + + + + + diff --git a/src/Avalonia.Themes.Fluent/NotificationCard.xaml b/src/Avalonia.Themes.Fluent/NotificationCard.xaml new file mode 100644 index 0000000000..5a0fa32657 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/NotificationCard.xaml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/NumericUpDown.xaml b/src/Avalonia.Themes.Fluent/NumericUpDown.xaml new file mode 100644 index 0000000000..d1610b343c --- /dev/null +++ b/src/Avalonia.Themes.Fluent/NumericUpDown.xaml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/OverlayPopupHost.xaml b/src/Avalonia.Themes.Fluent/OverlayPopupHost.xaml new file mode 100644 index 0000000000..3809e11251 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/OverlayPopupHost.xaml @@ -0,0 +1,17 @@ + diff --git a/src/Avalonia.Themes.Fluent/PopupRoot.xaml b/src/Avalonia.Themes.Fluent/PopupRoot.xaml new file mode 100644 index 0000000000..25c71e3493 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/PopupRoot.xaml @@ -0,0 +1,21 @@ + + + diff --git a/src/Avalonia.Themes.Fluent/ProgressBar.xaml b/src/Avalonia.Themes.Fluent/ProgressBar.xaml new file mode 100644 index 0000000000..036595b61c --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ProgressBar.xaml @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/RadioButton.xaml b/src/Avalonia.Themes.Fluent/RadioButton.xaml new file mode 100644 index 0000000000..acde4ea0be --- /dev/null +++ b/src/Avalonia.Themes.Fluent/RadioButton.xaml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/RepeatButton.xaml b/src/Avalonia.Themes.Fluent/RepeatButton.xaml new file mode 100644 index 0000000000..5afaf05e9f --- /dev/null +++ b/src/Avalonia.Themes.Fluent/RepeatButton.xaml @@ -0,0 +1,61 @@ + + + + + + + + + + 8,5,8,6 + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/ScrollBar.xaml b/src/Avalonia.Themes.Fluent/ScrollBar.xaml new file mode 100644 index 0000000000..4727ff72b9 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ScrollBar.xaml @@ -0,0 +1,302 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/ScrollViewer.xaml b/src/Avalonia.Themes.Fluent/ScrollViewer.xaml new file mode 100644 index 0000000000..fb73f7eab1 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ScrollViewer.xaml @@ -0,0 +1,69 @@ + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Separator.xaml b/src/Avalonia.Themes.Fluent/Separator.xaml new file mode 100644 index 0000000000..dc968fe86c --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Separator.xaml @@ -0,0 +1,18 @@ + + + + + diff --git a/src/Avalonia.Themes.Fluent/Slider.xaml b/src/Avalonia.Themes.Fluent/Slider.xaml new file mode 100644 index 0000000000..8abf78cc81 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Slider.xaml @@ -0,0 +1,263 @@ + + + + + + + + + + + + + + + + + 0,0,0,4 + 15 + 15 + 32 + 32 + 10 + 20 + 20 + 20 + 20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/SplitView.xaml b/src/Avalonia.Themes.Fluent/SplitView.xaml new file mode 100644 index 0000000000..71e92459f1 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/SplitView.xaml @@ -0,0 +1,219 @@ + + + + 320 + 48 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/TabControl.xaml b/src/Avalonia.Themes.Fluent/TabControl.xaml new file mode 100644 index 0000000000..c38ecb0452 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/TabControl.xaml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + 0 0 0 2 + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/TabItem.xaml b/src/Avalonia.Themes.Fluent/TabItem.xaml new file mode 100644 index 0000000000..2b0a0c1ea0 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/TabItem.xaml @@ -0,0 +1,130 @@ + + + + + + + + + + + 48 + 24 + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/TabStrip.xaml b/src/Avalonia.Themes.Fluent/TabStrip.xaml new file mode 100644 index 0000000000..8b3c0a8310 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/TabStrip.xaml @@ -0,0 +1,26 @@ + + + + + Item 1 + Item 2 + Disabled + + + + + diff --git a/src/Avalonia.Themes.Fluent/TabStripItem.xaml b/src/Avalonia.Themes.Fluent/TabStripItem.xaml new file mode 100644 index 0000000000..628ab8dddd --- /dev/null +++ b/src/Avalonia.Themes.Fluent/TabStripItem.xaml @@ -0,0 +1,103 @@ + + + + + Leaf + Arch + + + + + 48 + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/TextBox.xaml b/src/Avalonia.Themes.Fluent/TextBox.xaml new file mode 100644 index 0000000000..c1b60d061c --- /dev/null +++ b/src/Avalonia.Themes.Fluent/TextBox.xaml @@ -0,0 +1,151 @@ + + + 0,0,0,4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/TimePicker.xaml b/src/Avalonia.Themes.Fluent/TimePicker.xaml new file mode 100644 index 0000000000..f82e194a73 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/TimePicker.xaml @@ -0,0 +1,282 @@ + + + + + 40 + 1 + 1 + 0,0,0,4 + 40 + 41 + 242 + 456 + 0,3,0,6 + 0,3,0,6 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/TitleBar.xaml b/src/Avalonia.Themes.Fluent/TitleBar.xaml new file mode 100644 index 0000000000..45798d3fa1 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/TitleBar.xaml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/ToggleButton.xaml b/src/Avalonia.Themes.Fluent/ToggleButton.xaml new file mode 100644 index 0000000000..f2f07d3e2a --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ToggleButton.xaml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + 8,5,8,6 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml new file mode 100644 index 0000000000..893d64f505 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml @@ -0,0 +1,293 @@ + + + 0,0,0,6 + 6 + 6 + 154 + 20 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/ToolTip.xaml b/src/Avalonia.Themes.Fluent/ToolTip.xaml new file mode 100644 index 0000000000..f7a1ebbc6b --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ToolTip.xaml @@ -0,0 +1,85 @@ + + + + + + Hover Here + + + + + + ToolTip + A control which pops up a hint when a control is hovered + + + ToolTip bottom placement + + + + + + 320 + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/TreeView.xaml b/src/Avalonia.Themes.Fluent/TreeView.xaml new file mode 100644 index 0000000000..292317eb34 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/TreeView.xaml @@ -0,0 +1,24 @@ + diff --git a/src/Avalonia.Themes.Fluent/TreeViewItem.xaml b/src/Avalonia.Themes.Fluent/TreeViewItem.xaml new file mode 100644 index 0000000000..a938394b56 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/TreeViewItem.xaml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + 16 + 12 + 12, 0, 12, 0 + M 1,0 10,10 l -9,10 -1,-1 L 8,10 -0,1 Z + M0,1 L10,10 20,1 19,0 10,8 1,0 Z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/UserControl.xaml b/src/Avalonia.Themes.Fluent/UserControl.xaml new file mode 100644 index 0000000000..f4d0c21367 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/UserControl.xaml @@ -0,0 +1,15 @@ + diff --git a/src/Avalonia.Themes.Fluent/Window.xaml b/src/Avalonia.Themes.Fluent/Window.xaml new file mode 100644 index 0000000000..794f516f71 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Window.xaml @@ -0,0 +1,24 @@ + diff --git a/src/Avalonia.Themes.Fluent/WindowNotificationManager.xaml b/src/Avalonia.Themes.Fluent/WindowNotificationManager.xaml new file mode 100644 index 0000000000..46324ea537 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/WindowNotificationManager.xaml @@ -0,0 +1,45 @@ + + + + + + + + + + + diff --git a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs index 8776d3a7b7..a8e618af27 100644 --- a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs @@ -1,6 +1,5 @@ using System; using System.Reactive.Disposables; -using Avalonia.Logging; using Avalonia.Media; using Avalonia.Media.Immutable; @@ -11,9 +10,9 @@ namespace Avalonia.Animation.Animators /// public class SolidColorBrushAnimator : Animator { - ColorAnimator _colorAnimator; + private ColorAnimator _colorAnimator; - void InitializeColorAnimator() + private void InitializeColorAnimator() { _colorAnimator = new ColorAnimator(); @@ -27,46 +26,44 @@ namespace Avalonia.Animation.Animators public override IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable match, Action onComplete) { + // Preprocess keyframe values to Color if the xaml parser converts them to ISCB. foreach (var keyframe in this) { - if (keyframe.Value as ISolidColorBrush == null) - return Disposable.Empty; - - // Preprocess keyframe values to Color if the xaml parser converts them to ISCB. - if (keyframe.Value.GetType() == typeof(ImmutableSolidColorBrush)) + if (keyframe.Value is ISolidColorBrush colorBrush) { - keyframe.Value = ((ImmutableSolidColorBrush)keyframe.Value).Color; + keyframe.Value = colorBrush.Color; + } + else + { + return Disposable.Empty; } } - // Add SCB if the target prop is empty. - if (control.GetValue(Property) == null) - control.SetValue(Property, new SolidColorBrush(Colors.Transparent)); - + SolidColorBrush finalTarget; var targetVal = control.GetValue(Property); - - // Continue if target prop is not empty & is a SolidColorBrush derivative. - if (typeof(ISolidColorBrush).IsAssignableFrom(targetVal.GetType())) + if (targetVal is null) + { + finalTarget = new SolidColorBrush(Colors.Transparent); + control.SetValue(Property, finalTarget); + } + else if (targetVal is ImmutableSolidColorBrush immutableSolidColorBrush) + { + finalTarget = new SolidColorBrush(immutableSolidColorBrush.Color); + control.SetValue(Property, finalTarget); + } + else if (targetVal is ISolidColorBrush) { - if (_colorAnimator == null) - InitializeColorAnimator(); - - SolidColorBrush finalTarget; - - // If it's ISCB, change it back to SCB. - if (targetVal.GetType() == typeof(ImmutableSolidColorBrush)) - { - var col = (ImmutableSolidColorBrush)targetVal; - targetVal = new SolidColorBrush(col.Color); - control.SetValue(Property, targetVal); - } - finalTarget = targetVal as SolidColorBrush; - - return _colorAnimator.Apply(animation, finalTarget, clock ?? control.Clock, match, onComplete); } + else + { + return Disposable.Empty; + } + + if (_colorAnimator == null) + InitializeColorAnimator(); - return Disposable.Empty; + return _colorAnimator.Apply(animation, finalTarget, clock ?? control.Clock, match, onComplete); } public override SolidColorBrush Interpolate(double p, SolidColorBrush o, SolidColorBrush n) => null; diff --git a/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs index 1f1590bdcd..1b2142f6c9 100644 --- a/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs @@ -1,6 +1,8 @@ using System; +using System.Reactive.Disposables; using Avalonia.Logging; using Avalonia.Media; +using Avalonia.Media.Transformation; namespace Avalonia.Animation.Animators { @@ -19,6 +21,12 @@ namespace Avalonia.Animation.Animators // Check if the Target Property is Transform derived. if (typeof(Transform).IsAssignableFrom(Property.OwnerType)) { + if (ctrl.RenderTransform is TransformOperations) + { + // HACK: This animator cannot reasonably animate CSS transforms at the moment. + return Disposable.Empty; + } + if (ctrl.RenderTransform == null) { var normalTransform = new TransformGroup(); @@ -51,7 +59,7 @@ namespace Avalonia.Animation.Animators // It's a transform object so let's target that. if (renderTransformType == Property.OwnerType) { - return _doubleAnimator.Apply(animation, ctrl.RenderTransform, clock ?? control.Clock, obsMatch, onComplete); + return _doubleAnimator.Apply(animation, (Transform) ctrl.RenderTransform, clock ?? control.Clock, obsMatch, onComplete); } // It's a TransformGroup and try finding the target there. else if (renderTransformType == typeof(TransformGroup)) @@ -65,15 +73,13 @@ namespace Avalonia.Animation.Animators } } - Logger.TryGet(LogEventLevel.Warning)?.Log( - LogArea.Animations, + Logger.TryGet(LogEventLevel.Warning, LogArea.Animations)?.Log( control, $"Cannot find the appropriate transform: \"{Property.OwnerType}\" in {control}."); } else { - Logger.TryGet(LogEventLevel.Error)?.Log( - LogArea.Animations, + Logger.TryGet(LogEventLevel.Error, LogArea.Animations)?.Log( control, $"Cannot apply animation: Target property owner {Property.OwnerType} is not a Transform object."); } diff --git a/src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs new file mode 100644 index 0000000000..8e9d20eb8f --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs @@ -0,0 +1,35 @@ +using System; +using Avalonia.Media; +using Avalonia.Media.Transformation; + +namespace Avalonia.Animation.Animators +{ + public class TransformOperationsAnimator : Animator + { + public TransformOperationsAnimator() + { + Validate = ValidateTransform; + } + + public override TransformOperations Interpolate(double progress, TransformOperations oldValue, TransformOperations newValue) + { + var oldTransform = EnsureOperations(oldValue); + var newTransform = EnsureOperations(newValue); + + return TransformOperations.Interpolate(oldTransform, newTransform, progress); + } + + internal static TransformOperations EnsureOperations(ITransform value) + { + return value as TransformOperations ?? TransformOperations.Identity; + } + + private void ValidateTransform(AnimatorKeyFrame kf) + { + if (!(kf.Value is TransformOperations)) + { + throw new InvalidOperationException($"All keyframes must be of type {typeof(TransformOperations)}."); + } + } + } +} diff --git a/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs new file mode 100644 index 0000000000..104acb71ad --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs @@ -0,0 +1,28 @@ +using System; +using System.Reactive.Linq; +using Avalonia.Animation.Animators; +using Avalonia.Media; + +namespace Avalonia.Animation +{ + public class TransformOperationsTransition : Transition + { + private static readonly TransformOperationsAnimator _operationsAnimator = new TransformOperationsAnimator(); + + public override IObservable DoTransition(IObservable progress, + ITransform oldValue, + ITransform newValue) + { + var oldTransform = TransformOperationsAnimator.EnsureOperations(oldValue); + var newTransform = TransformOperationsAnimator.EnsureOperations(newValue); + + return progress + .Select(p => + { + var f = Easing.Ease(p); + + return _operationsAnimator.Interpolate(f, oldTransform, newTransform); + }); + } + } +} diff --git a/src/Avalonia.Visuals/Assets/GraphemeBreak.trie b/src/Avalonia.Visuals/Assets/GraphemeBreak.trie index 704dea4e86..482bf9b44d 100644 Binary files a/src/Avalonia.Visuals/Assets/GraphemeBreak.trie and b/src/Avalonia.Visuals/Assets/GraphemeBreak.trie differ diff --git a/src/Avalonia.Visuals/Assets/UnicodeData.trie b/src/Avalonia.Visuals/Assets/UnicodeData.trie index 2e39745646..f96106a5fa 100644 Binary files a/src/Avalonia.Visuals/Assets/UnicodeData.trie and b/src/Avalonia.Visuals/Assets/UnicodeData.trie differ diff --git a/src/Avalonia.Visuals/Matrix.cs b/src/Avalonia.Visuals/Matrix.cs index 898c6027a5..206b842220 100644 --- a/src/Avalonia.Visuals/Matrix.cs +++ b/src/Avalonia.Visuals/Matrix.cs @@ -54,7 +54,7 @@ namespace Avalonia /// /// HasInverse Property - returns true if this matrix is invertible, false otherwise. /// - public bool HasInverse => GetDeterminant() != 0; + public bool HasInverse => !MathUtilities.IsZero(GetDeterminant()); /// /// The first element of the first row @@ -286,7 +286,7 @@ namespace Avalonia { double d = GetDeterminant(); - if (d == 0) + if (MathUtilities.IsZero(d)) { throw new InvalidOperationException("Transform is not invertible."); } @@ -319,5 +319,76 @@ namespace Avalonia ); } } + + /// + /// Decomposes given matrix into transform operations. + /// + /// Matrix to decompose. + /// Decomposed matrix. + /// The status of the operation. + public static bool TryDecomposeTransform(Matrix matrix, out Decomposed decomposed) + { + decomposed = default; + + var determinant = matrix.GetDeterminant(); + + if (MathUtilities.IsZero(determinant)) + { + return false; + } + + var m11 = matrix.M11; + var m21 = matrix.M21; + var m12 = matrix.M12; + var m22 = matrix.M22; + + // Translation. + decomposed.Translate = new Vector(matrix.M31, matrix.M32); + + // Scale sign. + var scaleX = 1d; + var scaleY = 1d; + + if (determinant < 0) + { + if (m11 < m22) + { + scaleX *= -1d; + } + else + { + scaleY *= -1d; + } + } + + // X Scale. + scaleX *= Math.Sqrt(m11 * m11 + m12 * m12); + + m11 /= scaleX; + m12 /= scaleX; + + // XY Shear. + double scaledShear = m11 * m21 + m12 * m22; + + m21 -= m11 * scaledShear; + m22 -= m12 * scaledShear; + + // Y Scale. + scaleY *= Math.Sqrt(m21 * m21 + m22 * m22); + + decomposed.Scale = new Vector(scaleX, scaleY); + decomposed.Skew = new Vector(scaledShear / scaleY, 0d); + decomposed.Angle = Math.Atan2(m12, m11); + + return true; + } + + public struct Decomposed + { + public Vector Translate; + public Vector Scale; + public Vector Skew; + public double Angle; + } } } diff --git a/src/Avalonia.Visuals/Media/AcrylicBackgroundSource.cs b/src/Avalonia.Visuals/Media/AcrylicBackgroundSource.cs new file mode 100644 index 0000000000..34cede4018 --- /dev/null +++ b/src/Avalonia.Visuals/Media/AcrylicBackgroundSource.cs @@ -0,0 +1,20 @@ +namespace Avalonia.Media +{ + /// + /// Background Sources for Acrylic. + /// + public enum AcrylicBackgroundSource + { + /// + /// The acrylic has no background. + /// + None, + + /// + /// Cuts through all render layers to reveal the window background. + /// This means if your window is transparent or blurred it + /// will be blended with the material. + /// + Digger + } +} diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index 2e06d2578f..052ee5e1b7 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -89,33 +89,64 @@ namespace Avalonia.Media /// The . public static Color Parse(string s) { - if (s == null) throw new ArgumentNullException(nameof(s)); - if (s.Length == 0) throw new FormatException(); + if (TryParse(s, out Color color)) + { + return color; + } - if (s[0] == '#') + throw new FormatException($"Invalid color string: '{s}'."); + } + + /// + /// Parses a color string. + /// + /// The color string. + /// The . + public static Color Parse(ReadOnlySpan s) + { + if (TryParse(s, out Color color)) { - var or = 0u; + return color; + } - if (s.Length == 7) - { - or = 0xff000000; - } - else if (s.Length != 9) - { - throw new FormatException($"Invalid color string: '{s}'."); - } + throw new FormatException($"Invalid color string: '{s.ToString()}'."); + } - return FromUInt32(uint.Parse(s.Substring(1), NumberStyles.HexNumber, CultureInfo.InvariantCulture) | or); + /// + /// Parses a color string. + /// + /// The color string. + /// The parsed color + /// The status of the operation. + public static bool TryParse(string s, out Color color) + { + if (s == null) + { + throw new ArgumentNullException(nameof(s)); + } + + if (s.Length == 0) + { + throw new FormatException(); + } + + if (s[0] == '#' && TryParseInternal(s.AsSpan(), out color)) + { + return true; } var knownColor = KnownColors.GetKnownColor(s); if (knownColor != KnownColor.None) { - return knownColor.ToColor(); + color = knownColor.ToColor(); + + return true; } - throw new FormatException($"Invalid color string: '{s}'."); + color = default; + + return false; } /// @@ -126,40 +157,79 @@ namespace Avalonia.Media /// The status of the operation. public static bool TryParse(ReadOnlySpan s, out Color color) { - color = default; - if (s == null) - return false; if (s.Length == 0) + { + color = default; + return false; + } if (s[0] == '#') { - var or = 0u; + return TryParseInternal(s, out color); + } + + var knownColor = KnownColors.GetKnownColor(s.ToString()); + + if (knownColor != KnownColor.None) + { + color = knownColor.ToColor(); + + return true; + } + + color = default; + + return false; + } + + private static bool TryParseInternal(ReadOnlySpan s, out Color color) + { + static bool TryParseCore(ReadOnlySpan input, ref Color color) + { + var alphaComponent = 0u; - if (s.Length == 7) + if (input.Length == 6) { - or = 0xff000000; + alphaComponent = 0xff000000; } - else if (s.Length != 9) + else if (input.Length != 8) { return false; } - if(!uint.TryParse(s.Slice(1).ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed)) + // TODO: (netstandard 2.1) Can use allocation free parsing. + if (!uint.TryParse(input.ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture, + out var parsed)) + { return false; - color = FromUInt32(parsed| or); + } + + color = FromUInt32(parsed | alphaComponent); + return true; } - var knownColor = KnownColors.GetKnownColor(s.ToString()); + color = default; - if (knownColor != KnownColor.None) + ReadOnlySpan input = s.Slice(1); + + // Handle shorthand cases like #FFF (RGB) or #FFFF (ARGB). + if (input.Length == 3 || input.Length == 4) { - color = knownColor.ToColor(); - return true; + var extendedLength = 2 * input.Length; + Span extended = stackalloc char[extendedLength]; + + for (int i = 0; i < input.Length; i++) + { + extended[2 * i + 0] = input[i]; + extended[2 * i + 1] = input[i]; + } + + return TryParseCore(extended, ref color); } - return false; + return TryParseCore(input, ref color); } /// diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index 6fdcd9631b..ba7191d7a6 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -4,6 +4,7 @@ using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; using Avalonia.Threading; +using Avalonia.Utilities; using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Media @@ -154,12 +155,12 @@ namespace Avalonia.Media return; } - if (Math.Abs(radiusX) > double.Epsilon) + if (!MathUtilities.IsZero(radiusX)) { radiusX = Math.Min(radiusX, rect.Width / 2); } - if (Math.Abs(radiusY) > double.Epsilon) + if (!MathUtilities.IsZero(radiusY)) { radiusY = Math.Min(radiusY, rect.Height / 2); } @@ -282,6 +283,12 @@ namespace Avalonia.Media } + public PushedState PushClip(RoundedRect clip) + { + PlatformImpl.PushClip(clip); + return new PushedState(this, PushedState.PushedStateType.Clip); + } + /// /// Pushes a clip rectangle. /// diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs new file mode 100644 index 0000000000..6b699acd2e --- /dev/null +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs @@ -0,0 +1,345 @@ +using System; + +namespace Avalonia.Media +{ + public class ExperimentalAcrylicMaterial : AvaloniaObject, IMutableExperimentalAcrylicMaterial + { + private Color _effectiveTintColor; + private Color _effectiveLuminosityColor; + + static ExperimentalAcrylicMaterial() + { + AffectsRender( + TintColorProperty, + BackgroundSourceProperty, + TintOpacityProperty, + MaterialOpacityProperty, + PlatformTransparencyCompensationLevelProperty); + + TintColorProperty.Changed.AddClassHandler((b, e) => + { + b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); + b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); + }); + + TintOpacityProperty.Changed.AddClassHandler((b, e) => + { + b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); + b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); + }); + + MaterialOpacityProperty.Changed.AddClassHandler((b, e) => + { + b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); + b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); + }); + + PlatformTransparencyCompensationLevelProperty.Changed.AddClassHandler((b, e) => + { + b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); + b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); + }); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty TintColorProperty = + AvaloniaProperty.Register(nameof(TintColor)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty BackgroundSourceProperty = + AvaloniaProperty.Register(nameof(BackgroundSource)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty TintOpacityProperty = + AvaloniaProperty.Register(nameof(TintOpacity), 0.8); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaterialOpacityProperty = + AvaloniaProperty.Register(nameof(MaterialOpacity), 0.5); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PlatformTransparencyCompensationLevelProperty = + AvaloniaProperty.Register(nameof(PlatformTransparencyCompensationLevel), 0.0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FallbackColorProperty = + AvaloniaProperty.Register(nameof(FallbackColor)); + + /// + public event EventHandler Invalidated; + + /// + /// Gets or Sets the BackgroundSource . + /// + public AcrylicBackgroundSource BackgroundSource + { + get => GetValue(BackgroundSourceProperty); + set => SetValue(BackgroundSourceProperty, value); + } + + /// + /// Gets or Sets the TintColor. + /// + public Color TintColor + { + get => GetValue(TintColorProperty); + set => SetValue(TintColorProperty, value); + } + + /// + /// Gets or Sets the Tint Opacity. + /// + public double TintOpacity + { + get => GetValue(TintOpacityProperty); + set => SetValue(TintOpacityProperty, value); + } + + /// + /// Gets or Sets the Fallback Color. + /// This is used on rendering plaforms that dont support acrylic. + /// + public Color FallbackColor + { + get => GetValue(FallbackColorProperty); + set => SetValue(FallbackColorProperty, value); + } + + /// + /// Gets or Sets the MaterialOpacity. + /// This makes the material more or less opaque. + /// + public double MaterialOpacity + { + get => GetValue(MaterialOpacityProperty); + set => SetValue(MaterialOpacityProperty, value); + } + + /// + /// Gets or Sets the PlatformTransparencyCompensationLevel. + /// This value defines the minimum that can be used. + /// It means material opacity is re-scaled from this value to 1. + /// + public double PlatformTransparencyCompensationLevel + { + get => GetValue(PlatformTransparencyCompensationLevelProperty); + set => SetValue(PlatformTransparencyCompensationLevelProperty, value); + } + + Color IExperimentalAcrylicMaterial.MaterialColor => _effectiveLuminosityColor; + + Color IExperimentalAcrylicMaterial.TintColor => _effectiveTintColor; + + private struct HsvColor + { + public float Hue { get; set; } + public float Saturation { get; set; } + public float Value { get; set; } + } + + private static HsvColor RgbToHsv(Color color) + { + var r = color.R / 255.0f; + var g = color.G / 255.0f; + var b = color.B / 255.0f; + var max = Math.Max(r, Math.Max(g, b)); + var min = Math.Min(r, Math.Min(g, b)); + + float h, s, v; + h = v = max; + + var d = max - min; + s = max == 0 ? 0 : d / max; + + if (max == min) + { + h = 0; // achromatic + } + else + { + if (max == r) + { + h = (g - b) / d + (g < b ? 6 : 0); + } + else if (max == g) + { + h = (b - r) / d + 2; + } + else if (max == b) + { + h = (r - g) / d + 4; + } + + h /= 6; + } + + return new HsvColor { Hue = h, Saturation = s, Value = v }; + } + + private static Color GetEffectiveTintColor(Color tintColor, double tintOpacity) + { + // Update tintColor's alpha with the combined opacity value + double tintOpacityModifier = GetTintOpacityModifier(tintColor); + + return new Color((byte)(255 * ((255.0 / tintColor.A) * tintOpacity) * tintOpacityModifier), tintColor.R, tintColor.G, tintColor.B); + } + + private static double GetTintOpacityModifier(Color tintColor) + { + // This method supresses the maximum allowable tint opacity depending on the luminosity and saturation of a color by + // compressing the range of allowable values - for example, a user-defined value of 100% will be mapped to 45% for pure + // white (100% luminosity), 85% for pure black (0% luminosity), and 90% for pure gray (50% luminosity). The intensity of + // the effect increases linearly as luminosity deviates from 50%. After this effect is calculated, we cancel it out + // linearly as saturation increases from zero. + + const double midPoint = 0.5; // Mid point of HsvV range that these calculations are based on. This is here for easy tuning. + + const double whiteMaxOpacity = 0.2; // 100% luminosity + const double midPointMaxOpacity = 0.45; // 50% luminosity + const double blackMaxOpacity = 0.45; // 0% luminosity + + var hsv = RgbToHsv(tintColor); + + double opacityModifier = midPointMaxOpacity; + + if (hsv.Value != midPoint) + { + // Determine maximum suppression amount + double lowestMaxOpacity = midPointMaxOpacity; + double maxDeviation = midPoint; + + if (hsv.Value > midPoint) + { + lowestMaxOpacity = whiteMaxOpacity; // At white (100% hsvV) + maxDeviation = 1 - maxDeviation; + } + else if (hsv.Value < midPoint) + { + lowestMaxOpacity = blackMaxOpacity; // At black (0% hsvV) + } + + double maxOpacitySuppression = midPointMaxOpacity - lowestMaxOpacity; + + // Determine normalized deviation from the midpoint + double deviation = Math.Abs(hsv.Value - midPoint); + double normalizedDeviation = deviation / maxDeviation; + + // If we have saturation, reduce opacity suppression to allow that color to come through more + if (hsv.Saturation > 0) + { + // Dampen opacity suppression based on how much saturation there is + maxOpacitySuppression *= Math.Max(1 - (hsv.Saturation * 2), 0.0); + } + + double opacitySuppression = maxOpacitySuppression * normalizedDeviation; + + opacityModifier = midPointMaxOpacity - opacitySuppression; + } + + return opacityModifier; + } + + private Color GetEffectiveLuminosityColor() + { + double? luminosityOpacity = MaterialOpacity; + + return GetLuminosityColor(luminosityOpacity); + } + + private static byte Trim(double value) + { + value = Math.Min(Math.Floor(value * 256), 255); + + if (value < 0) + { + return 0; + } + else if (value > 255) + { + return 255; + } + + return (byte)value; + } + + private static float RGBMax(Color color) + { + if (color.R > color.G) + return (color.R > color.B) ? color.R : color.B; + else + return (color.G > color.B) ? color.G : color.B; + } + + private static float RGBMin(Color color) + { + if (color.R < color.G) + return (color.R < color.B) ? color.R : color.B; + else + return (color.G < color.B) ? color.G : color.B; + } + + // The tintColor passed into this method should be the original, unmodified color created using user values for TintColor + TintOpacity + private Color GetLuminosityColor(double? luminosityOpacity) + { + // Calculate the HSL lightness value of the color. + var max = (float)RGBMax(TintColor) / 255.0f; + var min = (float)RGBMin(TintColor) / 255.0f; + + var lightness = (max + min) / 2.0; + + lightness = 1 - ((1 - lightness) * luminosityOpacity.Value); + + lightness = 0.13 + (lightness * 0.74); + + var luminosityColor = new Color(255, Trim(lightness), Trim(lightness), Trim(lightness)); + + var compensationMultiplier = 1 - PlatformTransparencyCompensationLevel; + return new Color((byte)(255 * Math.Max(Math.Min(PlatformTransparencyCompensationLevel + (luminosityOpacity.Value * compensationMultiplier), 1.0), 0.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); + } + + /// + /// Marks a property as affecting the brush's visual representation. + /// + /// The properties. + /// + /// After a call to this method in a brush's static constructor, any change to the + /// property will cause the event to be raised on the brush. + /// + protected static void AffectsRender(params AvaloniaProperty[] properties) + where T : ExperimentalAcrylicMaterial + { + static void Invalidate(AvaloniaPropertyChangedEventArgs e) + { + (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty); + } + + foreach (var property in properties) + { + property.Changed.Subscribe(e => Invalidate(e)); + } + } + + /// + /// Raises the event. + /// + /// The event args. + protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e); + + public IExperimentalAcrylicMaterial ToImmutable() + { + return new ImmutableExperimentalAcrylicMaterial(this); + } + } +} diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs index f9410afe6a..bc979c15ee 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -100,7 +100,7 @@ namespace Avalonia.Media return typeface; } - typeface = new Typeface(fontFamily, fontWeight, fontStyle); + typeface = new Typeface(fontFamily, fontStyle, fontWeight); if (_typefaceCache.TryAdd(key, typeface)) { @@ -143,7 +143,7 @@ namespace Avalonia.Media } var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ? - _typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Weight, key.Style)) : + _typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Style, key.Weight)) : null; return matchedTypeface; diff --git a/src/Avalonia.Visuals/Media/FontWeight.cs b/src/Avalonia.Visuals/Media/FontWeight.cs index a9967b6a66..5a4a4963a5 100644 --- a/src/Avalonia.Visuals/Media/FontWeight.cs +++ b/src/Avalonia.Visuals/Media/FontWeight.cs @@ -29,6 +29,11 @@ namespace Avalonia.Media /// Light = 300, + /// + /// Specifies a "semi light" font weight. + /// + SemiLight = 350, + /// /// Specifies a "normal" font weight. /// diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs index 29c9d93560..a32a3e1b6c 100644 --- a/src/Avalonia.Visuals/Media/GlyphRun.cs +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using Avalonia.Platform; -using Avalonia.Utility; +using Avalonia.Utilities; namespace Avalonia.Media { @@ -205,13 +205,16 @@ namespace Avalonia.Media var glyphIndex = FindGlyphIndex(characterHit.FirstCharacterIndex); - var currentCluster = _glyphClusters[glyphIndex]; - - if (characterHit.TrailingLength > 0) + if (!GlyphClusters.IsEmpty) { - while (glyphIndex < _glyphClusters.Length && _glyphClusters[glyphIndex] == currentCluster) + var currentCluster = GlyphClusters[glyphIndex]; + + if (characterHit.TrailingLength > 0) { - glyphIndex++; + while (glyphIndex < GlyphClusters.Length && GlyphClusters[glyphIndex] == currentCluster) + { + glyphIndex++; + } } } @@ -302,7 +305,7 @@ namespace Avalonia.Media } } - var characterHit = FindNearestCharacterHit(GlyphClusters[index], out var width); + var characterHit = FindNearestCharacterHit(GlyphClusters.IsEmpty ? index : GlyphClusters[index], out var width); var offset = GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex)); @@ -370,26 +373,31 @@ namespace Avalonia.Media /// public int FindGlyphIndex(int characterIndex) { + if (GlyphClusters.IsEmpty) + { + return characterIndex; + } + if (IsLeftToRight) { - if (characterIndex < _glyphClusters[0]) + if (characterIndex < GlyphClusters[0]) { return 0; } - if (characterIndex > _glyphClusters[_glyphClusters.Length - 1]) + if (characterIndex > GlyphClusters[GlyphClusters.Length - 1]) { return _glyphClusters.End; } } else { - if (characterIndex < _glyphClusters[_glyphClusters.Length - 1]) + if (characterIndex < GlyphClusters[GlyphClusters.Length - 1]) { return _glyphClusters.End; } - if (characterIndex > _glyphClusters[0]) + if (characterIndex > GlyphClusters[0]) { return 0; } @@ -397,7 +405,7 @@ namespace Avalonia.Media var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer; - var clusters = _glyphClusters.Buffer.Span; + var clusters = GlyphClusters.Buffer.Span; // Find the start of the cluster at the character index. var start = clusters.BinarySearch((ushort)characterIndex, comparer); @@ -418,9 +426,19 @@ namespace Avalonia.Media } } - while (start > 0 && clusters[start - 1] == clusters[start]) + if (IsLeftToRight) { - start--; + while (start > 0 && clusters[start - 1] == clusters[start]) + { + start--; + } + } + else + { + while (start + 1 < clusters.Length && clusters[start + 1] == clusters[start]) + { + start++; + } } return start; @@ -440,34 +458,74 @@ namespace Avalonia.Media var start = FindGlyphIndex(index); - var currentCluster = _glyphClusters[start]; + if (GlyphClusters.IsEmpty) + { + width = GetGlyphWidth(index); + + return new CharacterHit(start, 1); + } - var trailingLength = 0; + var cluster = GlyphClusters[start]; - while (start < _glyphClusters.Length && _glyphClusters[start] == currentCluster) + var nextCluster = cluster; + + var currentIndex = start; + + while (nextCluster == cluster) { - if (GlyphAdvances.IsEmpty) + width += GetGlyphWidth(currentIndex); + + if (IsLeftToRight) { - var glyph = GlyphIndices[start]; + currentIndex++; - width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale; + if (currentIndex == GlyphClusters.Length) + { + break; + } } else { - width += GlyphAdvances[start]; + currentIndex--; + + if (currentIndex < 0) + { + break; + } } - trailingLength++; - start++; + nextCluster = GlyphClusters[currentIndex]; + } + + int trailingLength; + + if (nextCluster == cluster) + { + trailingLength = Characters.Start + Characters.Length - cluster; + } + else + { + trailingLength = nextCluster - cluster; } - if (start == _glyphClusters.Length && - currentCluster + trailingLength != Characters.Start + Characters.Length) + return new CharacterHit(cluster, trailingLength); + } + + /// + /// Gets a glyph's width. + /// + /// The glyph index. + /// The glyph's width. + private double GetGlyphWidth(int index) + { + if (GlyphAdvances.IsEmpty) { - trailingLength = Characters.Start + Characters.Length - currentCluster; + var glyph = GlyphIndices[index]; + + return GlyphTypeface.GetGlyphAdvance(glyph) * Scale; } - return new CharacterHit(currentCluster, trailingLength); + return GlyphAdvances[index]; } /// diff --git a/src/Avalonia.Visuals/Media/IBrush.cs b/src/Avalonia.Visuals/Media/IBrush.cs index 7756e94598..15b7681be4 100644 --- a/src/Avalonia.Visuals/Media/IBrush.cs +++ b/src/Avalonia.Visuals/Media/IBrush.cs @@ -13,4 +13,4 @@ namespace Avalonia.Media /// double Opacity { get; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Media/IExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/IExperimentalAcrylicMaterial.cs new file mode 100644 index 0000000000..e71584258a --- /dev/null +++ b/src/Avalonia.Visuals/Media/IExperimentalAcrylicMaterial.cs @@ -0,0 +1,33 @@ +namespace Avalonia.Media +{ + /// + /// Experimental Interface for producing Acrylic-like materials. + /// + public interface IExperimentalAcrylicMaterial + { + /// + /// Gets the of the material. + /// + AcrylicBackgroundSource BackgroundSource { get; } + + /// + /// Gets the TintColor of the material. + /// + Color TintColor { get; } + + /// + /// Gets the TintOpacity of the material. + /// + double TintOpacity { get; } + + /// + /// Gets the effective material color. + /// + Color MaterialColor { get; } + + /// + /// Gets the fallback color. + /// + Color FallbackColor { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/IMutableExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/IMutableExperimentalAcrylicMaterial.cs new file mode 100644 index 0000000000..fcfe4631a6 --- /dev/null +++ b/src/Avalonia.Visuals/Media/IMutableExperimentalAcrylicMaterial.cs @@ -0,0 +1,14 @@ +namespace Avalonia.Media +{ + /// + /// Represents a mutable brush which can return an immutable clone of itself. + /// + public interface IMutableExperimentalAcrylicMaterial : IExperimentalAcrylicMaterial, IAffectsRender + { + /// + /// Creates an immutable clone of the brush. + /// + /// The immutable clone. + IExperimentalAcrylicMaterial ToImmutable(); + } +} diff --git a/src/Avalonia.Visuals/Media/IMutableTransform.cs b/src/Avalonia.Visuals/Media/IMutableTransform.cs new file mode 100644 index 0000000000..2033c434c0 --- /dev/null +++ b/src/Avalonia.Visuals/Media/IMutableTransform.cs @@ -0,0 +1,12 @@ +using System; + +namespace Avalonia.Media +{ + public interface IMutableTransform : ITransform + { + /// + /// Raised when the transform changes. + /// + event EventHandler Changed; + } +} diff --git a/src/Avalonia.Visuals/Media/ITransform.cs b/src/Avalonia.Visuals/Media/ITransform.cs new file mode 100644 index 0000000000..91577fe38e --- /dev/null +++ b/src/Avalonia.Visuals/Media/ITransform.cs @@ -0,0 +1,10 @@ +using System.ComponentModel; + +namespace Avalonia.Media +{ + [TypeConverter(typeof(TransformConverter))] + public interface ITransform + { + Matrix Value { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicMaterial.cs new file mode 100644 index 0000000000..f46d76cf3f --- /dev/null +++ b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicMaterial.cs @@ -0,0 +1,73 @@ +using System; + +namespace Avalonia.Media +{ + public readonly struct ImmutableExperimentalAcrylicMaterial : IExperimentalAcrylicMaterial, IEquatable + { + public ImmutableExperimentalAcrylicMaterial(IExperimentalAcrylicMaterial brush) + { + BackgroundSource = brush.BackgroundSource; + TintColor = brush.TintColor; + TintOpacity = brush.TintOpacity; + FallbackColor = brush.FallbackColor; + MaterialColor = brush.MaterialColor; + } + + public AcrylicBackgroundSource BackgroundSource { get; } + + public Color TintColor { get; } + + public Color MaterialColor { get; } + + public double TintOpacity { get; } + + public Color FallbackColor { get; } + + public bool Equals(ImmutableExperimentalAcrylicMaterial other) + { + // ReSharper disable once CompareOfFloatsByEqualityOperator + return + TintColor == other.TintColor && + TintOpacity == other.TintOpacity && + BackgroundSource == other.BackgroundSource && + FallbackColor == other.FallbackColor && MaterialColor == other.MaterialColor; + + } + + public override bool Equals(object obj) + { + return obj is ImmutableExperimentalAcrylicMaterial other && Equals(other); + } + + public Color GetEffectiveTintColor() + { + return TintColor; + } + + public override int GetHashCode() + { + unchecked + { + int hash = 17; + + hash = (hash * 23) + TintColor.GetHashCode(); + hash = (hash * 23) + TintOpacity.GetHashCode(); + hash = (hash * 23) + BackgroundSource.GetHashCode(); + hash = (hash * 23) + FallbackColor.GetHashCode(); + hash = (hash * 23) + MaterialColor.GetHashCode(); + + return hash; + } + } + + public static bool operator ==(ImmutableExperimentalAcrylicMaterial left, ImmutableExperimentalAcrylicMaterial right) + { + return left.Equals(right); + } + + public static bool operator !=(ImmutableExperimentalAcrylicMaterial left, ImmutableExperimentalAcrylicMaterial right) + { + return !left.Equals(right); + } + } +} diff --git a/src/Avalonia.Visuals/Media/MaterialExtensions.cs b/src/Avalonia.Visuals/Media/MaterialExtensions.cs new file mode 100644 index 0000000000..c0b445c357 --- /dev/null +++ b/src/Avalonia.Visuals/Media/MaterialExtensions.cs @@ -0,0 +1,22 @@ +using System; + +namespace Avalonia.Media +{ + public static class MaterialExtensions + { + /// + /// Converts a brush to an immutable brush. + /// + /// The brush. + /// + /// The result of calling if the brush is mutable, + /// otherwise . + /// + public static IExperimentalAcrylicMaterial ToImmutable(this IExperimentalAcrylicMaterial material) + { + Contract.Requires(material != null); + + return (material as IMutableExperimentalAcrylicMaterial)?.ToImmutable() ?? material; + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextDecoration.cs b/src/Avalonia.Visuals/Media/TextDecoration.cs index a83555946b..681fc5d499 100644 --- a/src/Avalonia.Visuals/Media/TextDecoration.cs +++ b/src/Avalonia.Visuals/Media/TextDecoration.cs @@ -1,4 +1,5 @@ -using Avalonia.Media.Immutable; +using Avalonia.Collections; +using Avalonia.Media.TextFormatting; namespace Avalonia.Media { @@ -14,28 +15,52 @@ namespace Avalonia.Media AvaloniaProperty.Register(nameof(Location)); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty PenProperty = - AvaloniaProperty.Register(nameof(Pen)); + public static readonly StyledProperty StrokeProperty = + AvaloniaProperty.Register(nameof(Stroke)); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty PenThicknessUnitProperty = - AvaloniaProperty.Register(nameof(PenThicknessUnit)); + public static readonly StyledProperty StrokeThicknessUnitProperty = + AvaloniaProperty.Register(nameof(StrokeThicknessUnit)); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty PenOffsetProperty = - AvaloniaProperty.Register(nameof(PenOffset)); + public static readonly StyledProperty> StrokeDashArrayProperty = + AvaloniaProperty.Register>(nameof(StrokeDashArray)); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty PenOffsetUnitProperty = - AvaloniaProperty.Register(nameof(PenOffsetUnit)); + public static readonly StyledProperty StrokeDashOffsetProperty = + AvaloniaProperty.Register(nameof(StrokeDashOffset)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty StrokeThicknessProperty = + AvaloniaProperty.Register(nameof(StrokeThickness), 1); + + /// + /// Defines the property. + /// + public static readonly StyledProperty StrokeLineCapProperty = + AvaloniaProperty.Register(nameof(StrokeLineCap)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty StrokeOffsetProperty = + AvaloniaProperty.Register(nameof(StrokeOffset)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty StrokeOffsetUnitProperty = + AvaloniaProperty.Register(nameof(StrokeOffsetUnit)); /// /// Gets or sets the location. @@ -50,54 +75,139 @@ namespace Avalonia.Media } /// - /// Gets or sets the pen. + /// Gets or sets the that specifies how the is painted. /// - /// - /// The pen. - /// - public IPen Pen + public IBrush Stroke + { + get { return GetValue(StrokeProperty); } + set { SetValue(StrokeProperty, value); } + } + + /// + /// Gets the units in which the thickness of the is expressed. + /// + public TextDecorationUnit StrokeThicknessUnit + { + get => GetValue(StrokeThicknessUnitProperty); + set => SetValue(StrokeThicknessUnitProperty, value); + } + + /// + /// Gets or sets a collection of values that indicate the pattern of dashes and gaps + /// that is used to draw the . + /// + public AvaloniaList StrokeDashArray + { + get { return GetValue(StrokeDashArrayProperty); } + set { SetValue(StrokeDashArrayProperty, value); } + } + + /// + /// Gets or sets a value that specifies the distance within the dash pattern where a dash begins. + /// + public double StrokeDashOffset + { + get { return GetValue(StrokeDashOffsetProperty); } + set { SetValue(StrokeDashOffsetProperty, value); } + } + + /// + /// Gets or sets the thickness of the . + /// + public double StrokeThickness { - get => GetValue(PenProperty); - set => SetValue(PenProperty, value); + get { return GetValue(StrokeThicknessProperty); } + set { SetValue(StrokeThicknessProperty, value); } } /// - /// Gets the units in which the Thickness of the text decoration's is expressed. + /// Gets or sets a enumeration value that describes the shape at the ends of a line. /// - public TextDecorationUnit PenThicknessUnit + public PenLineCap StrokeLineCap { - get => GetValue(PenThicknessUnitProperty); - set => SetValue(PenThicknessUnitProperty, value); + get { return GetValue(StrokeLineCapProperty); } + set { SetValue(StrokeLineCapProperty, value); } } /// - /// Gets or sets the pen offset. + /// The stroke's offset. /// /// /// The pen offset. /// - public double PenOffset + public double StrokeOffset { - get => GetValue(PenOffsetProperty); - set => SetValue(PenOffsetProperty, value); + get => GetValue(StrokeOffsetProperty); + set => SetValue(StrokeOffsetProperty, value); } /// - /// Gets the units in which the value is expressed. + /// Gets the units in which the value is expressed. /// - public TextDecorationUnit PenOffsetUnit + public TextDecorationUnit StrokeOffsetUnit { - get => GetValue(PenOffsetUnitProperty); - set => SetValue(PenOffsetUnitProperty, value); + get => GetValue(StrokeOffsetUnitProperty); + set => SetValue(StrokeOffsetUnitProperty, value); } /// - /// Creates an immutable clone of the . + /// Draws the at given origin. /// - /// The immutable clone. - public ImmutableTextDecoration ToImmutable() + /// The drawing context. + /// The shaped characters that are decorated. + /// The origin. + internal void Draw(DrawingContext drawingContext, ShapedTextCharacters shapedTextCharacters, Point origin) { - return new ImmutableTextDecoration(Location, Pen?.ToImmutable(), PenThicknessUnit, PenOffset, PenOffsetUnit); + var fontRenderingEmSize = shapedTextCharacters.Properties.FontRenderingEmSize; + var fontMetrics = shapedTextCharacters.FontMetrics; + var thickness = StrokeThickness; + + switch (StrokeThicknessUnit) + { + case TextDecorationUnit.FontRecommended: + switch (Location) + { + case TextDecorationLocation.Underline: + thickness = fontMetrics.UnderlineThickness; + break; + case TextDecorationLocation.Strikethrough: + thickness = fontMetrics.StrikethroughThickness; + break; + } + + break; + case TextDecorationUnit.FontRenderingEmSize: + thickness = fontRenderingEmSize * thickness; + break; + } + + switch (Location) + { + case TextDecorationLocation.Overline: + origin += new Point(0, fontMetrics.Ascent); + break; + case TextDecorationLocation.Strikethrough: + origin += new Point(0, -fontMetrics.StrikethroughPosition); + break; + case TextDecorationLocation.Underline: + origin += new Point(0, -fontMetrics.UnderlinePosition); + break; + } + + switch (StrokeOffsetUnit) + { + case TextDecorationUnit.FontRenderingEmSize: + origin += new Point(0, StrokeOffset * fontRenderingEmSize); + break; + case TextDecorationUnit.Pixel: + origin += new Point(0, StrokeOffset); + break; + } + + var pen = new Pen(Stroke ?? shapedTextCharacters.Properties.ForegroundBrush, thickness, + new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeLineCap); + + drawingContext.DrawLine(pen, origin, origin + new Point(shapedTextCharacters.Bounds.Width, 0)); } } } diff --git a/src/Avalonia.Visuals/Media/TextDecorationCollection.cs b/src/Avalonia.Visuals/Media/TextDecorationCollection.cs index 21e2e2484c..2dced2252e 100644 --- a/src/Avalonia.Visuals/Media/TextDecorationCollection.cs +++ b/src/Avalonia.Visuals/Media/TextDecorationCollection.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using Avalonia.Collections; -using Avalonia.Media.Immutable; using Avalonia.Utilities; namespace Avalonia.Media @@ -11,22 +10,6 @@ namespace Avalonia.Media /// public class TextDecorationCollection : AvaloniaList { - /// - /// Creates an immutable clone of the . - /// - /// The immutable clone. - public ImmutableTextDecoration[] ToImmutable() - { - var immutable = new ImmutableTextDecoration[Count]; - - for (var i = 0; i < Count; i++) - { - immutable[i] = this[i].ToImmutable(); - } - - return immutable; - } - /// /// Parses a string. /// diff --git a/src/Avalonia.Visuals/Media/TextDecorationUnit.cs b/src/Avalonia.Visuals/Media/TextDecorationUnit.cs index dde425bb94..a61983e8d5 100644 --- a/src/Avalonia.Visuals/Media/TextDecorationUnit.cs +++ b/src/Avalonia.Visuals/Media/TextDecorationUnit.cs @@ -1,7 +1,7 @@ namespace Avalonia.Media { /// - /// Specifies the unit type of either a or a thickness value. + /// Specifies the unit type of either a or a value. /// public enum TextDecorationUnit { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs index 4903342cea..56790cc0db 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs @@ -1,6 +1,4 @@ -using Avalonia.Platform; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// A text run that supports drawing content. @@ -17,6 +15,6 @@ namespace Avalonia.Media.TextFormatting /// /// The drawing context. /// The origin. - public abstract void Draw(IDrawingContextImpl drawingContext, Point origin); + public abstract void Draw(DrawingContext drawingContext, Point origin); } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs new file mode 100644 index 0000000000..c4302aecec --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs @@ -0,0 +1,69 @@ +namespace Avalonia.Media.TextFormatting +{ + public class GenericTextParagraphProperties : TextParagraphProperties + { + private TextAlignment _textAlignment; + private TextWrapping _textWrapping; + private TextTrimming _textTrimming; + private double _lineHeight; + + public GenericTextParagraphProperties( + TextRunProperties defaultTextRunProperties, + TextAlignment textAlignment = TextAlignment.Left, + TextWrapping textWrapping = TextWrapping.WrapWithOverflow, + TextTrimming textTrimming = TextTrimming.None, + double lineHeight = 0) + { + DefaultTextRunProperties = defaultTextRunProperties; + + _textAlignment = textAlignment; + + _textWrapping = textWrapping; + + _textTrimming = textTrimming; + + _lineHeight = lineHeight; + } + + public override TextRunProperties DefaultTextRunProperties { get; } + + public override TextAlignment TextAlignment => _textAlignment; + + public override TextWrapping TextWrapping => _textWrapping; + + public override TextTrimming TextTrimming => _textTrimming; + + public override double LineHeight => _lineHeight; + + /// + /// Set text alignment + /// + internal void SetTextAlignment(TextAlignment textAlignment) + { + _textAlignment = textAlignment; + } + + /// + /// Set text wrap + /// + internal void SetTextWrapping(TextWrapping textWrapping) + { + _textWrapping = textWrapping; + } + /// + /// Set text trimming + /// + internal void SetTextTrimming(TextTrimming textTrimming) + { + _textTrimming = textTrimming; + } + + /// + /// Set line height + /// + internal void SetLineHeight(double lineHeight) + { + _lineHeight = lineHeight; + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs new file mode 100644 index 0000000000..3db3589498 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs @@ -0,0 +1,40 @@ +using System.Globalization; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Generic implementation of TextRunProperties + /// + public class GenericTextRunProperties : TextRunProperties + { + public GenericTextRunProperties(Typeface typeface, double fontRenderingEmSize = 12, + TextDecorationCollection textDecorations = null, IBrush foregroundBrush = null, IBrush backgroundBrush = null, + CultureInfo cultureInfo = null) + { + Typeface = typeface; + FontRenderingEmSize = fontRenderingEmSize; + TextDecorations = textDecorations; + ForegroundBrush = foregroundBrush; + BackgroundBrush = backgroundBrush; + CultureInfo = cultureInfo; + } + + /// + public override Typeface Typeface { get; } + + /// + public override double FontRenderingEmSize { get; } + + /// + public override TextDecorationCollection TextDecorations { get; } + + /// + public override IBrush ForegroundBrush { get; } + + /// + public override IBrush BackgroundBrush { get; } + + /// + public override CultureInfo CultureInfo { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs new file mode 100644 index 0000000000..0c6c722941 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs @@ -0,0 +1,23 @@ +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// A group of characters that can be shaped. + /// + public sealed class ShapeableTextCharacters : TextRun + { + public ShapeableTextCharacters(ReadOnlySlice text, TextRunProperties properties) + { + TextSourceLength = text.Length; + Text = text; + Properties = properties; + } + + public override int TextSourceLength { get; } + + public override ReadOnlySlice Text { get; } + + public override TextRunProperties Properties { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs new file mode 100644 index 0000000000..2e7e7aceb1 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs @@ -0,0 +1,164 @@ +using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// A text run that holds shaped characters. + /// + public sealed class ShapedTextCharacters : DrawableTextRun + { + public ShapedTextCharacters(GlyphRun glyphRun, TextRunProperties properties) + { + Text = glyphRun.Characters; + Properties = properties; + TextSourceLength = Text.Length; + FontMetrics = new FontMetrics(Properties.Typeface, Properties.FontRenderingEmSize); + GlyphRun = glyphRun; + } + + /// + public override ReadOnlySlice Text { get; } + + /// + public override TextRunProperties Properties { get; } + + /// + public override int TextSourceLength { get; } + + /// + public override Rect Bounds => GlyphRun.Bounds; + + /// + /// Gets the font metrics. + /// + /// + /// The font metrics. + /// + public FontMetrics FontMetrics { get; } + + /// + /// Gets the glyph run. + /// + /// + /// The glyphs. + /// + public GlyphRun GlyphRun { get; } + + /// + public override void Draw(DrawingContext drawingContext, Point origin) + { + if (GlyphRun.GlyphIndices.Length == 0) + { + return; + } + + if (Properties.Typeface == null) + { + return; + } + + if (Properties.ForegroundBrush == null) + { + return; + } + + if (Properties.BackgroundBrush != null) + { + drawingContext.DrawRectangle(Properties.BackgroundBrush, null, + new Rect(origin.X, origin.Y + FontMetrics.Ascent, Bounds.Width, Bounds.Height)); + } + + drawingContext.DrawGlyphRun(Properties.ForegroundBrush, GlyphRun, origin); + + if (Properties.TextDecorations == null) + { + return; + } + + foreach (var textDecoration in Properties.TextDecorations) + { + textDecoration.Draw(drawingContext, this, origin); + } + } + + /// + /// Splits the at specified length. + /// + /// The length. + /// The split result. + public SplitTextCharactersResult Split(int length) + { + var glyphCount = 0; + + var firstCharacters = GlyphRun.Characters.Take(length); + + var codepointEnumerator = new CodepointEnumerator(firstCharacters); + + while (codepointEnumerator.MoveNext()) + { + glyphCount++; + } + + if (GlyphRun.Characters.Length == length) + { + return new SplitTextCharactersResult(this, null); + } + + if (GlyphRun.GlyphIndices.Length == glyphCount) + { + return new SplitTextCharactersResult(this, null); + } + + var firstGlyphRun = new GlyphRun( + Properties.Typeface.GlyphTypeface, + Properties.FontRenderingEmSize, + GlyphRun.GlyphIndices.Take(glyphCount), + GlyphRun.GlyphAdvances.Take(glyphCount), + GlyphRun.GlyphOffsets.Take(glyphCount), + GlyphRun.Characters.Take(length), + GlyphRun.GlyphClusters.Take(glyphCount)); + + var firstTextRun = new ShapedTextCharacters(firstGlyphRun, Properties); + + var secondGlyphRun = new GlyphRun( + Properties.Typeface.GlyphTypeface, + Properties.FontRenderingEmSize, + GlyphRun.GlyphIndices.Skip(glyphCount), + GlyphRun.GlyphAdvances.Skip(glyphCount), + GlyphRun.GlyphOffsets.Skip(glyphCount), + GlyphRun.Characters.Skip(length), + GlyphRun.GlyphClusters.Skip(glyphCount)); + + var secondTextRun = new ShapedTextCharacters(secondGlyphRun, Properties); + + return new SplitTextCharactersResult(firstTextRun, secondTextRun); + } + + public readonly struct SplitTextCharactersResult + { + public SplitTextCharactersResult(ShapedTextCharacters first, ShapedTextCharacters second) + { + First = first; + + Second = second; + } + + /// + /// Gets the first text run. + /// + /// + /// The first text run. + /// + public ShapedTextCharacters First { get; } + + /// + /// Gets the second text run. + /// + /// + /// The second text run. + /// + public ShapedTextCharacters Second { get; } + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextRun.cs deleted file mode 100644 index 00f9b918cb..0000000000 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextRun.cs +++ /dev/null @@ -1,212 +0,0 @@ -using Avalonia.Media.Immutable; -using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Platform; -using Avalonia.Utility; - -namespace Avalonia.Media.TextFormatting -{ - /// - /// A text run that holds a shaped glyph run. - /// - public sealed class ShapedTextRun : DrawableTextRun - { - public ShapedTextRun(ReadOnlySlice text, TextStyle style) : this( - TextShaper.Current.ShapeText(text, style.TextFormat), style) - { - } - - public ShapedTextRun(GlyphRun glyphRun, TextStyle style) - { - Text = glyphRun.Characters; - Style = style; - GlyphRun = glyphRun; - } - - /// - public override Rect Bounds => GlyphRun.Bounds; - - /// - /// Gets the glyph run. - /// - /// - /// The glyphs. - /// - public GlyphRun GlyphRun { get; } - - /// - public override void Draw(IDrawingContextImpl drawingContext, Point origin) - { - if (GlyphRun.GlyphIndices.Length == 0) - { - return; - } - - if (Style.TextFormat.Typeface == null) - { - return; - } - - if (Style.Foreground == null) - { - return; - } - - drawingContext.DrawGlyphRun(Style.Foreground, GlyphRun, origin); - - if (Style.TextDecorations == null) - { - return; - } - - foreach (var textDecoration in Style.TextDecorations) - { - DrawTextDecoration(drawingContext, textDecoration, origin); - } - } - - /// - /// Draws the at given origin. - /// - /// The drawing context. - /// The text decoration. - /// The origin. - private void DrawTextDecoration(IDrawingContextImpl drawingContext, ImmutableTextDecoration textDecoration, Point origin) - { - var textFormat = Style.TextFormat; - - var fontMetrics = Style.TextFormat.FontMetrics; - - var thickness = textDecoration.Pen?.Thickness ?? 1.0; - - switch (textDecoration.PenThicknessUnit) - { - case TextDecorationUnit.FontRecommended: - switch (textDecoration.Location) - { - case TextDecorationLocation.Underline: - thickness = fontMetrics.UnderlineThickness; - break; - case TextDecorationLocation.Strikethrough: - thickness = fontMetrics.StrikethroughThickness; - break; - } - break; - case TextDecorationUnit.FontRenderingEmSize: - thickness = textFormat.FontRenderingEmSize * thickness; - break; - } - - switch (textDecoration.Location) - { - case TextDecorationLocation.Overline: - origin += new Point(0, textFormat.FontMetrics.Ascent); - break; - case TextDecorationLocation.Strikethrough: - origin += new Point(0, -textFormat.FontMetrics.StrikethroughPosition); - break; - case TextDecorationLocation.Underline: - origin += new Point(0, -textFormat.FontMetrics.UnderlinePosition); - break; - } - - switch (textDecoration.PenOffsetUnit) - { - case TextDecorationUnit.FontRenderingEmSize: - origin += new Point(0, textDecoration.PenOffset * textFormat.FontRenderingEmSize); - break; - case TextDecorationUnit.Pixel: - origin += new Point(0, textDecoration.PenOffset); - break; - } - - var pen = new ImmutablePen( - textDecoration.Pen?.Brush ?? Style.Foreground.ToImmutable(), - thickness, - textDecoration.Pen?.DashStyle?.ToImmutable(), - textDecoration.Pen?.LineCap ?? default, - textDecoration.Pen?.LineJoin ?? PenLineJoin.Miter, - textDecoration.Pen?.MiterLimit ?? 10.0); - - drawingContext.DrawLine(pen, origin, origin + new Point(GlyphRun.Bounds.Width, 0)); - } - - /// - /// Splits the at specified length. - /// - /// The length. - /// The split result. - public SplitTextCharactersResult Split(int length) - { - var glyphCount = 0; - - var firstCharacters = GlyphRun.Characters.Take(length); - - var codepointEnumerator = new CodepointEnumerator(firstCharacters); - - while (codepointEnumerator.MoveNext()) - { - glyphCount++; - } - - if (GlyphRun.Characters.Length == length) - { - return new SplitTextCharactersResult(this, null); - } - - if (GlyphRun.GlyphIndices.Length == glyphCount) - { - return new SplitTextCharactersResult(this, null); - } - - var firstGlyphRun = new GlyphRun( - Style.TextFormat.Typeface.GlyphTypeface, - Style.TextFormat.FontRenderingEmSize, - GlyphRun.GlyphIndices.Take(glyphCount), - GlyphRun.GlyphAdvances.Take(glyphCount), - GlyphRun.GlyphOffsets.Take(glyphCount), - GlyphRun.Characters.Take(length), - GlyphRun.GlyphClusters.Take(length)); - - var firstTextRun = new ShapedTextRun(firstGlyphRun, Style); - - var secondGlyphRun = new GlyphRun( - Style.TextFormat.Typeface.GlyphTypeface, - Style.TextFormat.FontRenderingEmSize, - GlyphRun.GlyphIndices.Skip(glyphCount), - GlyphRun.GlyphAdvances.Skip(glyphCount), - GlyphRun.GlyphOffsets.Skip(glyphCount), - GlyphRun.Characters.Skip(length), - GlyphRun.GlyphClusters.Skip(length)); - - var secondTextRun = new ShapedTextRun(secondGlyphRun, Style); - - return new SplitTextCharactersResult(firstTextRun, secondTextRun); - } - - public readonly struct SplitTextCharactersResult - { - public SplitTextCharactersResult(ShapedTextRun first, ShapedTextRun second) - { - First = first; - - Second = second; - } - - /// - /// Gets the first text run. - /// - /// - /// The first text run. - /// - public ShapedTextRun First { get; } - - /// - /// Gets the second text run. - /// - /// - /// The second text run. - /// - public ShapedTextRun Second { get; } - } - } -} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs b/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs deleted file mode 100644 index f84e45d4c6..0000000000 --- a/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs +++ /dev/null @@ -1,395 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Platform; -using Avalonia.Utility; - -namespace Avalonia.Media.TextFormatting -{ - internal class SimpleTextFormatter : TextFormatter - { - private static readonly ReadOnlySlice s_ellipsis = new ReadOnlySlice(new[] { '\u2026' }); - - /// - public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, - TextParagraphProperties paragraphProperties) - { - var textTrimming = paragraphProperties.TextTrimming; - var textWrapping = paragraphProperties.TextWrapping; - TextLine textLine; - - var textRuns = FormatTextRuns(textSource, firstTextSourceIndex, out var textPointer); - - if (textTrimming != TextTrimming.None) - { - textLine = PerformTextTrimming(textPointer, textRuns, paragraphWidth, paragraphProperties); - } - else - { - if (textWrapping == TextWrapping.Wrap) - { - textLine = PerformTextWrapping(textPointer, textRuns, paragraphWidth, paragraphProperties); - } - else - { - var textLineMetrics = - TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment); - - textLine = new SimpleTextLine(textPointer, textRuns, textLineMetrics); - } - } - - return textLine; - } - - /// - /// Formats text runs with optional text style overrides. - /// - /// The text source. - /// The first text source index. - /// The text pointer that covers the formatted text runs. - /// - /// The formatted text runs. - /// - private List FormatTextRuns(ITextSource textSource, int firstTextSourceIndex, out TextPointer textPointer) - { - var start = -1; - var length = 0; - - var textRuns = new List(); - - while (true) - { - var textRun = textSource.GetTextRun(firstTextSourceIndex + length); - - if (start == -1) - { - start = textRun.Text.Start; - } - - if (textRun is TextEndOfLine) - { - break; - } - - switch (textRun) - { - case TextCharacters textCharacters: - - var runText = textCharacters.Text; - - while (!runText.IsEmpty) - { - var shapableTextStyleRun = CreateShapableTextStyleRun(runText, textRun.Style); - - var shapedRun = new ShapedTextRun(runText.Take(shapableTextStyleRun.TextPointer.Length), - shapableTextStyleRun.Style); - - textRuns.Add(shapedRun); - - runText = runText.Skip(shapedRun.Text.Length); - } - - break; - default: - throw new NotSupportedException("Run type not supported by the formatter."); - } - - length += textRun.Text.Length; - } - - textPointer = new TextPointer(start, length); - - return textRuns; - } - - /// - /// Performs text trimming and returns a trimmed line. - /// - /// A value that specifies the width of the paragraph that the line fills. - /// A value that represents paragraph properties, - /// such as TextWrapping, TextAlignment, or TextStyle. - /// The text runs to perform the trimming on. - /// The text that was used to construct the text runs. - /// - private static TextLine PerformTextTrimming(TextPointer text, IReadOnlyList textRuns, - double paragraphWidth, TextParagraphProperties paragraphProperties) - { - var textTrimming = paragraphProperties.TextTrimming; - var availableWidth = paragraphWidth; - var currentWidth = 0.0; - var runIndex = 0; - - while (runIndex < textRuns.Count) - { - var currentRun = textRuns[runIndex]; - - currentWidth += currentRun.GlyphRun.Bounds.Width; - - if (currentWidth > availableWidth) - { - var ellipsisRun = CreateEllipsisRun(currentRun.Style); - - var measuredLength = MeasureText(currentRun, availableWidth - ellipsisRun.GlyphRun.Bounds.Width); - - if (textTrimming == TextTrimming.WordEllipsis) - { - if (measuredLength < text.End) - { - var currentBreakPosition = 0; - - var lineBreaker = new LineBreakEnumerator(currentRun.Text); - - while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) - { - var nextBreakPosition = lineBreaker.Current.PositionWrap; - - if (nextBreakPosition == 0) - { - break; - } - - if (nextBreakPosition > measuredLength) - { - break; - } - - currentBreakPosition = nextBreakPosition; - } - - measuredLength = currentBreakPosition; - } - } - - var splitResult = SplitTextRuns(textRuns, measuredLength); - - var trimmedRuns = new List(splitResult.First.Count + 1); - - trimmedRuns.AddRange(splitResult.First); - - trimmedRuns.Add(ellipsisRun); - - var textLineMetrics = - TextLineMetrics.Create(trimmedRuns, paragraphWidth, paragraphProperties.TextAlignment); - - return new SimpleTextLine(text.Take(measuredLength), trimmedRuns, textLineMetrics); - } - - availableWidth -= currentRun.GlyphRun.Bounds.Width; - - runIndex++; - } - - return new SimpleTextLine(text, textRuns, - TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment)); - } - - /// - /// Performs text wrapping returns a list of text lines. - /// - /// The text paragraph properties. - /// The text run'S. - /// The text to analyze for break opportunities. - /// - /// - private static TextLine PerformTextWrapping(TextPointer text, IReadOnlyList textRuns, - double paragraphWidth, TextParagraphProperties paragraphProperties) - { - var availableWidth = paragraphWidth; - var currentWidth = 0.0; - var runIndex = 0; - var length = 0; - - while (runIndex < textRuns.Count) - { - var currentRun = textRuns[runIndex]; - - if (currentWidth + currentRun.GlyphRun.Bounds.Width > availableWidth) - { - var measuredLength = MeasureText(currentRun, paragraphWidth - currentWidth); - - if (measuredLength < currentRun.Text.Length) - { - var currentBreakPosition = -1; - - var lineBreaker = new LineBreakEnumerator(currentRun.Text); - - while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) - { - var nextBreakPosition = lineBreaker.Current.PositionWrap; - - if (nextBreakPosition == 0) - { - break; - } - - if (nextBreakPosition > measuredLength) - { - break; - } - - currentBreakPosition = nextBreakPosition; - } - - if (currentBreakPosition != -1) - { - measuredLength = currentBreakPosition; - } - } - - length += measuredLength; - - var splitResult = SplitTextRuns(textRuns, length); - - var textLineMetrics = - TextLineMetrics.Create(splitResult.First, paragraphWidth, paragraphProperties.TextAlignment); - - return new SimpleTextLine(text.Take(length), splitResult.First, textLineMetrics); - } - - currentWidth += currentRun.GlyphRun.Bounds.Width; - - length += currentRun.GlyphRun.Characters.Length; - - runIndex++; - } - - return new SimpleTextLine(text, textRuns, - TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment)); - } - - /// - /// Measures the number of characters that fits into available width. - /// - /// The text run. - /// The available width. - /// - private static int MeasureText(ShapedTextRun textRun, double availableWidth) - { - var glyphRun = textRun.GlyphRun; - - var characterHit = glyphRun.GetCharacterHitFromDistance(availableWidth, out _); - - return characterHit.FirstCharacterIndex + characterHit.TrailingLength - textRun.Text.Start; - } - - /// - /// Creates an ellipsis. - /// - /// The text style. - /// - private static ShapedTextRun CreateEllipsisRun(TextStyle textStyle) - { - var formatterImpl = AvaloniaLocator.Current.GetService(); - - var glyphRun = formatterImpl.ShapeText(s_ellipsis, textStyle.TextFormat); - - return new ShapedTextRun(glyphRun, textStyle); - } - - private readonly struct SplitTextRunsResult - { - public SplitTextRunsResult(IReadOnlyList first, IReadOnlyList second) - { - First = first; - - Second = second; - } - - /// - /// Gets the first text runs. - /// - /// - /// The first text runs. - /// - public IReadOnlyList First { get; } - - /// - /// Gets the second text runs. - /// - /// - /// The second text runs. - /// - public IReadOnlyList Second { get; } - } - - /// - /// Split a sequence of runs into two segments at specified length. - /// - /// The text run's. - /// The length to split at. - /// - private static SplitTextRunsResult SplitTextRuns(IReadOnlyList textRuns, int length) - { - var currentLength = 0; - - for (var i = 0; i < textRuns.Count; i++) - { - var currentRun = textRuns[i]; - - if (currentLength + currentRun.GlyphRun.Characters.Length < length) - { - currentLength += currentRun.GlyphRun.Characters.Length; - continue; - } - - var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i; - - var first = new ShapedTextRun[firstCount]; - - if (firstCount > 1) - { - for (var j = 0; j < i; j++) - { - first[j] = textRuns[j]; - } - } - - var secondCount = textRuns.Count - firstCount; - - if (currentLength + currentRun.GlyphRun.Characters.Length == length) - { - var second = new ShapedTextRun[secondCount]; - - var offset = currentRun.GlyphRun.Characters.Length > 1 ? 1 : 0; - - if (secondCount > 0) - { - for (var j = 0; j < secondCount; j++) - { - second[j] = textRuns[i + j + offset]; - } - } - - first[i] = currentRun; - - return new SplitTextRunsResult(first, second); - } - else - { - secondCount++; - - var second = new ShapedTextRun[secondCount]; - - if (secondCount > 0) - { - for (var j = 1; j < secondCount; j++) - { - second[j] = textRuns[i + j]; - } - } - - var split = currentRun.Split(length - currentLength); - - first[i] = split.First; - - second[0] = split.Second; - - return new SplitTextRunsResult(first, second); - } - } - - return new SplitTextRunsResult(textRuns, null); - } - } -} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextLine.cs deleted file mode 100644 index 11d241bc34..0000000000 --- a/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextLine.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Platform; - -namespace Avalonia.Media.TextFormatting -{ - internal class SimpleTextLine : TextLine - { - private readonly IReadOnlyList _textRuns; - - public SimpleTextLine(TextPointer textPointer, IReadOnlyList textRuns, TextLineMetrics lineMetrics) - { - Text = textPointer; - _textRuns = textRuns; - LineMetrics = lineMetrics; - } - - /// - public override TextPointer Text { get; } - - /// - public override IReadOnlyList TextRuns => _textRuns; - - /// - public override TextLineMetrics LineMetrics { get; } - - /// - public override void Draw(IDrawingContextImpl drawingContext, Point origin) - { - var currentX = origin.X; - - foreach (var textRun in _textRuns) - { - var baselineOrigin = new Point(currentX + LineMetrics.BaselineOrigin.X, - origin.Y + LineMetrics.BaselineOrigin.Y); - - textRun.Draw(drawingContext, baselineOrigin); - - currentX += textRun.Bounds.Width; - } - } - - /// - public override CharacterHit GetCharacterHitFromDistance(double distance) - { - if (distance < 0) - { - // hit happens before the line, return the first position - return new CharacterHit(Text.Start); - } - - // process hit that happens within the line - var characterHit = new CharacterHit(); - - foreach (var run in _textRuns) - { - characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _); - - if (distance <= run.Bounds.Width) - { - break; - } - - distance -= run.Bounds.Width; - } - - return characterHit; - } - - /// - public override double GetDistanceFromCharacterHit(CharacterHit characterHit) - { - return DistanceFromCodepointIndex(characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0)); - } - - /// - public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) - { - int nextVisibleCp; - bool navigableCpFound; - - if (characterHit.TrailingLength == 0) - { - navigableCpFound = FindNextCodepointIndex(characterHit.FirstCharacterIndex, out nextVisibleCp); - - if (navigableCpFound) - { - // Move from leading to trailing edge - return new CharacterHit(nextVisibleCp, 1); - } - } - - navigableCpFound = FindNextCodepointIndex(characterHit.FirstCharacterIndex + 1, out nextVisibleCp); - - if (navigableCpFound) - { - // Move from trailing edge of current character to trailing edge of next - return new CharacterHit(nextVisibleCp, 1); - } - - // Can't move, we're after the last character - return characterHit; - } - - /// - public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) - { - int previousCodepointIndex; - bool codepointIndexFound; - - var cpHit = characterHit.FirstCharacterIndex; - var trailingHit = characterHit.TrailingLength != 0; - - // Input can be right after the end of the current line. Snap it to be at the end of the line. - if (cpHit >= Text.Start + Text.Length) - { - cpHit = Text.Start + Text.Length - 1; - - trailingHit = true; - } - - if (trailingHit) - { - codepointIndexFound = FindPreviousCodepointIndex(cpHit, out previousCodepointIndex); - - if (codepointIndexFound) - { - // Move from trailing to leading edge - return new CharacterHit(previousCodepointIndex, 0); - } - } - - codepointIndexFound = FindPreviousCodepointIndex(cpHit - 1, out previousCodepointIndex); - - if (codepointIndexFound) - { - // Move from leading edge of current character to leading edge of previous - return new CharacterHit(previousCodepointIndex, 0); - } - - // Can't move, we're before the first character - return characterHit; - } - - /// - public override CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit) - { - // same operation as move-to-previous - return GetPreviousCaretCharacterHit(characterHit); - } - - /// - /// Get distance from line start to the specified codepoint index - /// - private double DistanceFromCodepointIndex(int codepointIndex) - { - var currentDistance = 0.0; - - foreach (var textRun in _textRuns) - { - if (codepointIndex > textRun.Text.End) - { - currentDistance += textRun.Bounds.Width; - - continue; - } - - return currentDistance + textRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(codepointIndex)); - } - - return currentDistance; - } - - /// - /// Search forward from the given codepoint index (inclusive) to find the next navigable codepoint index. - /// Return true if one such codepoint index is found, false otherwise. - /// - private bool FindNextCodepointIndex(int codepointIndex, out int nextCodepointIndex) - { - nextCodepointIndex = codepointIndex; - - if (codepointIndex >= Text.Start + Text.Length) - { - return false; // Cannot go forward anymore - } - - GetRunIndexAtCodepointIndex(codepointIndex, out var runIndex, out var cpRunStart); - - while (runIndex < TextRuns.Count) - { - // When navigating forward, only the trailing edge of visible content is - // navigable. - if (runIndex < TextRuns.Count) - { - nextCodepointIndex = Math.Max(cpRunStart, codepointIndex); - return true; - } - - cpRunStart += TextRuns[runIndex++].Text.Length; - } - - return false; - } - - /// - /// Search backward from the given codepoint index (inclusive) to find the previous navigable codepoint index. - /// Return true if one such codepoint is found, false otherwise. - /// - private bool FindPreviousCodepointIndex(int codepointIndex, out int previousCodepointIndex) - { - previousCodepointIndex = codepointIndex; - - if (codepointIndex < Text.Start) - { - return false; // Cannot go backward anymore. - } - - // Position the cpRunEnd at the end of the span that contains the given cp - GetRunIndexAtCodepointIndex(codepointIndex, out var runIndex, out var codepointIndexAtRunEnd); - - codepointIndexAtRunEnd += TextRuns[runIndex].Text.End; - - while (runIndex >= 0) - { - // Visible content has caret stops at its leading edge. - if (runIndex + 1 < TextRuns.Count) - { - previousCodepointIndex = Math.Min(codepointIndexAtRunEnd, codepointIndex); - return true; - } - - // Newline sequence has caret stops at its leading edge. - if (runIndex == TextRuns.Count) - { - // Get the cp index at the beginning of the newline sequence. - previousCodepointIndex = codepointIndexAtRunEnd - TextRuns[runIndex].Text.Length + 1; - return true; - } - - codepointIndexAtRunEnd -= TextRuns[runIndex--].Text.Length; - } - - return false; - } - - private void GetRunIndexAtCodepointIndex(int codepointIndex, out int runIndex, out int codepointIndexAtRunStart) - { - codepointIndexAtRunStart = Text.Start; - runIndex = 0; - - // Find the span that contains the given cp - while (runIndex < TextRuns.Count && - codepointIndexAtRunStart + TextRuns[runIndex].Text.Length <= codepointIndex) - { - codepointIndexAtRunStart += TextRuns[runIndex++].Text.Length; - } - } - } -} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs index d9b27958ab..b35882fc0e 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs @@ -1,4 +1,6 @@ -using Avalonia.Utility; +using System.Collections.Generic; +using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -7,15 +9,182 @@ namespace Avalonia.Media.TextFormatting /// public class TextCharacters : TextRun { - protected TextCharacters() + public TextCharacters(ReadOnlySlice text, TextRunProperties properties) { - + TextSourceLength = text.Length; + Text = text; + Properties = properties; } - public TextCharacters(ReadOnlySlice text, TextStyle style) + /// + public override int TextSourceLength { get; } + + /// + public override ReadOnlySlice Text { get; } + + /// + public override TextRunProperties Properties { get; } + + /// + /// Gets a list of . + /// + /// The shapeable text characters. + internal IList GetShapeableCharacters() { - Text = text; - Style = style; + var shapeableCharacters = new List(2); + + var runText = Text; + + while (!runText.IsEmpty) + { + var shapeableRun = CreateShapeableRun(runText, Properties); + + shapeableCharacters.Add(shapeableRun); + + runText = runText.Skip(shapeableRun.Text.Length); + } + + return shapeableCharacters; + } + + /// + /// Creates a shapeable text run with unique properties. + /// + /// The text to create text runs from. + /// The default text run properties. + /// A list of shapeable text runs. + private ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice text, TextRunProperties defaultProperties) + { + var defaultTypeface = defaultProperties.Typeface; + + var currentTypeface = defaultTypeface; + + if (TryGetRunProperties(text, currentTypeface, defaultTypeface, out var count)) + { + return new ShapeableTextCharacters(text.Take(count), + new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize, + defaultProperties.TextDecorations, defaultProperties.ForegroundBrush)); + + } + + var codepoint = Codepoint.ReadAt(text, count, out _); + + //ToDo: Fix FontFamily fallback + currentTypeface = + FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style, defaultTypeface.FontFamily); + + if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count)) + { + //Fallback found + return new ShapeableTextCharacters(text.Take(count), + new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize, + defaultProperties.TextDecorations, defaultProperties.ForegroundBrush)); + } + + // no fallback found + currentTypeface = defaultTypeface; + + var glyphTypeface = currentTypeface.GlyphTypeface; + + var enumerator = new GraphemeEnumerator(text); + + while (enumerator.MoveNext()) + { + var grapheme = enumerator.Current; + + if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _)) + { + break; + } + + count += grapheme.Text.Length; + } + + return new ShapeableTextCharacters(text.Take(count), + new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize, + defaultProperties.TextDecorations, defaultProperties.ForegroundBrush)); + } + + /// + /// Tries to get run properties. + /// + /// + /// + /// The typeface that is used to find matching characters. + /// + /// + protected bool TryGetRunProperties(ReadOnlySlice text, Typeface typeface, Typeface defaultTypeface, + out int count) + { + if (text.Length == 0) + { + count = 0; + return false; + } + + var isFallback = typeface != defaultTypeface; + + count = 0; + var script = Script.Common; + //var direction = BiDiClass.LeftToRight; + + var font = typeface.GlyphTypeface; + var defaultFont = defaultTypeface.GlyphTypeface; + + var enumerator = new GraphemeEnumerator(text); + + while (enumerator.MoveNext()) + { + var grapheme = enumerator.Current; + + var currentScript = grapheme.FirstCodepoint.Script; + + //var currentDirection = grapheme.FirstCodepoint.BiDiClass; + + //// ToDo: Implement BiDi algorithm + //if (currentScript.HorizontalDirection != direction) + //{ + // if (!UnicodeUtility.IsWhiteSpace(grapheme.FirstCodepoint)) + // { + // break; + // } + //} + + if (currentScript != script) + { + if (currentScript != Script.Inherited && currentScript != Script.Common) + { + if (script == Script.Inherited || script == Script.Common) + { + script = currentScript; + } + else + { + break; + } + } + } + + if (isFallback) + { + if (defaultFont.TryGetGlyph(grapheme.FirstCodepoint, out _)) + { + break; + } + } + + if (!font.TryGetGlyph(grapheme.FirstCodepoint, out _)) + { + if (!grapheme.FirstCodepoint.IsWhiteSpace) + { + break; + } + } + + count += grapheme.Text.Length; + } + + return count > 0; } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormat.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormat.cs deleted file mode 100644 index 18dd6c7c10..0000000000 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormat.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; - -namespace Avalonia.Media.TextFormatting -{ - /// - /// Unique text formatting properties that are used by the . - /// - public readonly struct TextFormat : IEquatable - { - public TextFormat(Typeface typeface, double fontRenderingEmSize) - { - Typeface = typeface; - FontRenderingEmSize = fontRenderingEmSize; - FontMetrics = new FontMetrics(typeface, fontRenderingEmSize); - } - - /// - /// Gets the typeface. - /// - /// - /// The typeface. - /// - public Typeface Typeface { get; } - - /// - /// Gets the font rendering em size. - /// - /// - /// The em rendering size of the font. - /// - public double FontRenderingEmSize { get; } - - /// - /// Gets the font metrics. - /// - /// - /// The metrics of the font. - /// - public FontMetrics FontMetrics { get; } - - public static bool operator ==(TextFormat self, TextFormat other) - { - return self.Equals(other); - } - - public static bool operator !=(TextFormat self, TextFormat other) - { - return !(self == other); - } - - public bool Equals(TextFormat other) - { - return Typeface.Equals(other.Typeface) && FontRenderingEmSize.Equals(other.FontRenderingEmSize); - } - - public override bool Equals(object obj) - { - return obj is TextFormat other && Equals(other); - } - - public override int GetHashCode() - { - unchecked - { - var hashCode = (Typeface != null ? Typeface.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ FontRenderingEmSize.GetHashCode(); - return hashCode; - } - } - } -} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs index 7da39dc5dc..e4c898e2b8 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs @@ -1,5 +1,4 @@ using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Utility; namespace Avalonia.Media.TextFormatting { @@ -22,7 +21,7 @@ namespace Avalonia.Media.TextFormatting return current; } - current = new SimpleTextFormatter(); + current = new TextFormatterImpl(); AvaloniaLocator.CurrentMutable.Bind().ToConstant(current); @@ -38,149 +37,10 @@ namespace Avalonia.Media.TextFormatting /// A value that specifies the width of the paragraph that the line fills. /// A value that represents paragraph properties, /// such as TextWrapping, TextAlignment, or TextStyle. + /// A value that specifies the text formatter state, + /// in terms of where the previous line in the paragraph was broken by the text formatting process. /// The formatted line. public abstract TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, - TextParagraphProperties paragraphProperties); - - /// - /// Creates a text style run with unique properties. - /// - /// The text to create text runs from. - /// - /// A list of text runs. - protected TextStyleRun CreateShapableTextStyleRun(ReadOnlySlice text, TextStyle defaultStyle) - { - var defaultTypeface = defaultStyle.TextFormat.Typeface; - - var currentTypeface = defaultTypeface; - - if (TryGetRunProperties(text, currentTypeface, defaultTypeface, out var count)) - { - return new TextStyleRun(new TextPointer(text.Start, count), new TextStyle(currentTypeface, - defaultStyle.TextFormat.FontRenderingEmSize, - defaultStyle.Foreground, defaultStyle.TextDecorations)); - - } - - var codepoint = Codepoint.ReadAt(text, count, out _); - - //ToDo: Fix FontFamily fallback - currentTypeface = - FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style, defaultStyle.TextFormat.Typeface.FontFamily); - - if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count)) - { - //Fallback found - return new TextStyleRun(new TextPointer(text.Start, count), new TextStyle(currentTypeface, - defaultStyle.TextFormat.FontRenderingEmSize, - defaultStyle.Foreground, defaultStyle.TextDecorations)); - - } - - // no fallback found - currentTypeface = defaultTypeface; - - var glyphTypeface = currentTypeface.GlyphTypeface; - - var enumerator = new GraphemeEnumerator(text); - - while (enumerator.MoveNext()) - { - var grapheme = enumerator.Current; - - if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _)) - { - break; - } - - count += grapheme.Text.Length; - } - - return new TextStyleRun(new TextPointer(text.Start, count), - new TextStyle(currentTypeface, defaultStyle.TextFormat.FontRenderingEmSize, - defaultStyle.Foreground, defaultStyle.TextDecorations)); - } - - /// - /// Tries to get run properties. - /// - /// - /// - /// The typeface that is used to find matching characters. - /// - /// - protected bool TryGetRunProperties(ReadOnlySlice text, Typeface typeface, Typeface defaultTypeface, - out int count) - { - if (text.Length == 0) - { - count = 0; - return false; - } - - var isFallback = typeface != defaultTypeface; - - count = 0; - var script = Script.Common; - //var direction = BiDiClass.LeftToRight; - - var font = typeface.GlyphTypeface; - var defaultFont = defaultTypeface.GlyphTypeface; - - var enumerator = new GraphemeEnumerator(text); - - while (enumerator.MoveNext()) - { - var grapheme = enumerator.Current; - - var currentScript = grapheme.FirstCodepoint.Script; - - //var currentDirection = grapheme.FirstCodepoint.BiDiClass; - - //// ToDo: Implement BiDi algorithm - //if (currentScript.HorizontalDirection != direction) - //{ - // if (!UnicodeUtility.IsWhiteSpace(grapheme.FirstCodepoint)) - // { - // break; - // } - //} - - if (currentScript != script) - { - if (currentScript != Script.Inherited && currentScript != Script.Common) - { - if (script == Script.Inherited || script == Script.Common) - { - script = currentScript; - } - else - { - break; - } - } - } - - if (isFallback) - { - if (defaultFont.TryGetGlyph(grapheme.FirstCodepoint, out _)) - { - break; - } - } - - if (!font.TryGetGlyph(grapheme.FirstCodepoint, out _)) - { - if (!grapheme.FirstCodepoint.IsWhiteSpace) - { - break; - } - } - - count += grapheme.Text.Length; - } - - return count > 0; - } + TextParagraphProperties paragraphProperties, TextLineBreak previousLineBreak = null); } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs new file mode 100644 index 0000000000..793707d0b2 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -0,0 +1,544 @@ +using System.Collections.Generic; +using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + internal class TextFormatterImpl : TextFormatter + { + private static readonly ReadOnlySlice s_ellipsis = new ReadOnlySlice(new[] { '\u2026' }); + + /// + public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, + TextParagraphProperties paragraphProperties, TextLineBreak previousLineBreak = null) + { + var textTrimming = paragraphProperties.TextTrimming; + var textWrapping = paragraphProperties.TextWrapping; + TextLine textLine = null; + + var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, previousLineBreak, out var nextLineBreak); + + var textRange = GetTextRange(textRuns); + + if (textTrimming != TextTrimming.None) + { + textLine = PerformTextTrimming(textRuns, textRange, paragraphWidth, paragraphProperties); + } + else + { + switch (textWrapping) + { + case TextWrapping.NoWrap: + { + var textLineMetrics = + TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties); + + textLine = new TextLineImpl(textRuns, textLineMetrics, nextLineBreak); + break; + } + case TextWrapping.WrapWithOverflow: + case TextWrapping.Wrap: + { + textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties); + break; + } + } + } + + return textLine; + } + + /// + /// Fetches text runs. + /// + /// The text source. + /// The first text source index. + /// Previous line break. Can be null. + /// Next line break. Can be null. + /// + /// The formatted text runs. + /// + private static IReadOnlyList FetchTextRuns(ITextSource textSource, + int firstTextSourceIndex, TextLineBreak previousLineBreak, out TextLineBreak nextLineBreak) + { + nextLineBreak = default; + + var currentLength = 0; + + var textRuns = new List(); + + if (previousLineBreak != null) + { + foreach (var shapedCharacters in previousLineBreak.RemainingCharacters) + { + textRuns.Add(shapedCharacters); + + if (TryGetLineBreak(shapedCharacters, out var runLineBreak)) + { + var splitResult = SplitTextRuns(textRuns, currentLength + runLineBreak.PositionWrap); + + nextLineBreak = new TextLineBreak(splitResult.Second); + + return splitResult.First; + } + + currentLength += shapedCharacters.Text.Length; + } + } + + firstTextSourceIndex += currentLength; + + var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex); + + while (textRunEnumerator.MoveNext()) + { + var textRun = textRunEnumerator.Current; + + switch (textRun) + { + case TextCharacters textCharacters: + { + var shapeableRuns = textCharacters.GetShapeableCharacters(); + + foreach (var run in shapeableRuns) + { + var glyphRun = TextShaper.Current.ShapeText(run.Text, run.Properties.Typeface, + run.Properties.FontRenderingEmSize, run.Properties.CultureInfo); + + var shapedCharacters = new ShapedTextCharacters(glyphRun, textRun.Properties); + + textRuns.Add(shapedCharacters); + } + + break; + } + } + + if (TryGetLineBreak(textRun, out var runLineBreak)) + { + var splitResult = SplitTextRuns(textRuns, currentLength + runLineBreak.PositionWrap); + + nextLineBreak = new TextLineBreak(splitResult.Second); + + return splitResult.First; + } + + currentLength += textRun.Text.Length; + } + + return textRuns; + } + + private static bool TryGetLineBreak(TextRun textRun, out LineBreak lineBreak) + { + lineBreak = default; + + if (textRun.Text.IsEmpty) + { + return false; + } + + var lineBreakEnumerator = new LineBreakEnumerator(textRun.Text); + + while (lineBreakEnumerator.MoveNext()) + { + if (!lineBreakEnumerator.Current.Required) + { + continue; + } + + lineBreak = lineBreakEnumerator.Current; + + if (lineBreak.PositionWrap >= textRun.Text.Length) + { + return true; + } + + //The line breaker isn't treating \n\r as a pair so we have to fix that here. + if (textRun.Text[lineBreak.PositionMeasure] == '\n' + && textRun.Text[lineBreak.PositionWrap] == '\r') + { + lineBreak = new LineBreak(lineBreak.PositionMeasure, lineBreak.PositionWrap + 1, + lineBreak.Required); + } + + return true; + } + + return false; + } + + /// + /// Performs text trimming and returns a trimmed line. + /// + /// The text runs to perform the trimming on. + /// The text range that is covered by the text runs. + /// A value that specifies the width of the paragraph that the line fills. + /// A value that represents paragraph properties, + /// such as TextWrapping, TextAlignment, or TextStyle. + /// + private static TextLine PerformTextTrimming(IReadOnlyList textRuns, TextRange textRange, + double paragraphWidth, TextParagraphProperties paragraphProperties) + { + var textTrimming = paragraphProperties.TextTrimming; + var availableWidth = paragraphWidth; + var currentWidth = 0.0; + var runIndex = 0; + + while (runIndex < textRuns.Count) + { + var currentRun = textRuns[runIndex]; + + currentWidth += currentRun.GlyphRun.Bounds.Width; + + if (currentWidth > availableWidth) + { + var ellipsisRun = CreateEllipsisRun(currentRun.Properties); + + var measuredLength = MeasureText(currentRun, availableWidth - ellipsisRun.GlyphRun.Bounds.Width); + + if (textTrimming == TextTrimming.WordEllipsis) + { + if (measuredLength < textRange.End) + { + var currentBreakPosition = 0; + + var lineBreaker = new LineBreakEnumerator(currentRun.Text); + + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) + { + var nextBreakPosition = lineBreaker.Current.PositionWrap; + + if (nextBreakPosition == 0) + { + break; + } + + if (nextBreakPosition > measuredLength) + { + break; + } + + currentBreakPosition = nextBreakPosition; + } + + measuredLength = currentBreakPosition; + } + } + + var splitResult = SplitTextRuns(textRuns, measuredLength); + + var trimmedRuns = new List(splitResult.First.Count + 1); + + trimmedRuns.AddRange(splitResult.First); + + trimmedRuns.Add(ellipsisRun); + + var textLineMetrics = + TextLineMetrics.Create(trimmedRuns, textRange, paragraphWidth, paragraphProperties); + + return new TextLineImpl(trimmedRuns, textLineMetrics); + } + + availableWidth -= currentRun.GlyphRun.Bounds.Width; + + runIndex++; + } + + return new TextLineImpl(textRuns, + TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties)); + } + + /// + /// Performs text wrapping returns a list of text lines. + /// + /// The text run's. + /// The text range that is covered by the text runs. + /// The paragraph width. + /// The text paragraph properties. + /// The wrapped text line. + private static TextLine PerformTextWrapping(IReadOnlyList textRuns, TextRange textRange, + double paragraphWidth, TextParagraphProperties paragraphProperties) + { + var availableWidth = paragraphWidth; + var currentWidth = 0.0; + var runIndex = 0; + var length = 0; + + while (runIndex < textRuns.Count) + { + var currentRun = textRuns[runIndex]; + + if (currentWidth + currentRun.GlyphRun.Bounds.Width > availableWidth) + { + var measuredLength = MeasureText(currentRun, paragraphWidth - currentWidth); + + if (measuredLength < currentRun.Text.Length) + { + if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow) + { + var lineBreaker = new LineBreakEnumerator(currentRun.Text.Skip(measuredLength)); + + if (lineBreaker.MoveNext()) + { + measuredLength += lineBreaker.Current.PositionWrap; + } + else + { + measuredLength = currentRun.Text.Length; + } + } + else + { + var currentBreakPosition = -1; + + var lineBreaker = new LineBreakEnumerator(currentRun.Text); + + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) + { + var nextBreakPosition = lineBreaker.Current.PositionWrap; + + if (nextBreakPosition == 0) + { + break; + } + + if (nextBreakPosition > measuredLength) + { + break; + } + + currentBreakPosition = nextBreakPosition; + } + + if (currentBreakPosition != -1) + { + measuredLength = currentBreakPosition; + } + + } + } + + length += measuredLength; + + var splitResult = SplitTextRuns(textRuns, length); + + var textLineMetrics = TextLineMetrics.Create(splitResult.First, + new TextRange(textRange.Start, length), paragraphWidth, paragraphProperties); + + var lineBreak = splitResult.Second != null && splitResult.Second.Count > 0 ? + new TextLineBreak(splitResult.Second) : + null; + + return new TextLineImpl(splitResult.First, textLineMetrics, lineBreak); + } + + currentWidth += currentRun.GlyphRun.Bounds.Width; + + length += currentRun.GlyphRun.Characters.Length; + + runIndex++; + } + + return new TextLineImpl(textRuns, + TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties)); + } + + /// + /// Measures the number of characters that fits into available width. + /// + /// The text run. + /// The available width. + /// + private static int MeasureText(ShapedTextCharacters textCharacters, double availableWidth) + { + var glyphRun = textCharacters.GlyphRun; + + var characterHit = glyphRun.GetCharacterHitFromDistance(availableWidth, out _); + + return characterHit.FirstCharacterIndex + characterHit.TrailingLength - textCharacters.Text.Start; + } + + /// + /// Creates an ellipsis. + /// + /// The text run properties. + /// + private static ShapedTextCharacters CreateEllipsisRun(TextRunProperties properties) + { + var formatterImpl = AvaloniaLocator.Current.GetService(); + + var glyphRun = formatterImpl.ShapeText(s_ellipsis, properties.Typeface, properties.FontRenderingEmSize, + properties.CultureInfo); + + return new ShapedTextCharacters(glyphRun, properties); + } + + /// + /// Gets the text range that is covered by the text runs. + /// + /// The text runs. + /// The text range that is covered by the text runs. + private static TextRange GetTextRange(IReadOnlyList textRuns) + { + if (textRuns is null || textRuns.Count == 0) + { + return new TextRange(); + } + + var firstTextRun = textRuns[0]; + + if (textRuns.Count == 1) + { + return new TextRange(firstTextRun.Text.Start, firstTextRun.Text.Length); + } + + var start = firstTextRun.Text.Start; + + var end = textRuns[textRuns.Count - 1].Text.End + 1; + + return new TextRange(start, end - start); + } + + /// + /// Split a sequence of runs into two segments at specified length. + /// + /// The text run's. + /// The length to split at. + /// The split text runs. + private static SplitTextRunsResult SplitTextRuns(IReadOnlyList textRuns, int length) + { + var currentLength = 0; + + for (var i = 0; i < textRuns.Count; i++) + { + var currentRun = textRuns[i]; + + if (currentLength + currentRun.GlyphRun.Characters.Length < length) + { + currentLength += currentRun.GlyphRun.Characters.Length; + continue; + } + + var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i; + + var first = new ShapedTextCharacters[firstCount]; + + if (firstCount > 1) + { + for (var j = 0; j < i; j++) + { + first[j] = textRuns[j]; + } + } + + var secondCount = textRuns.Count - firstCount; + + if (currentLength + currentRun.GlyphRun.Characters.Length == length) + { + var second = new ShapedTextCharacters[secondCount]; + + var offset = currentRun.GlyphRun.Characters.Length > 1 ? 1 : 0; + + if (secondCount > 0) + { + for (var j = 0; j < secondCount; j++) + { + second[j] = textRuns[i + j + offset]; + } + } + + first[i] = currentRun; + + return new SplitTextRunsResult(first, second); + } + else + { + secondCount++; + + var second = new ShapedTextCharacters[secondCount]; + + if (secondCount > 0) + { + for (var j = 1; j < secondCount; j++) + { + second[j] = textRuns[i + j]; + } + } + + var split = currentRun.Split(length - currentLength); + + first[i] = split.First; + + second[0] = split.Second; + + return new SplitTextRunsResult(first, second); + } + } + + return new SplitTextRunsResult(textRuns, null); + } + + private readonly struct SplitTextRunsResult + { + public SplitTextRunsResult(IReadOnlyList first, IReadOnlyList second) + { + First = first; + + Second = second; + } + + /// + /// Gets the first text runs. + /// + /// + /// The first text runs. + /// + public IReadOnlyList First { get; } + + /// + /// Gets the second text runs. + /// + /// + /// The second text runs. + /// + public IReadOnlyList Second { get; } + } + + private struct TextRunEnumerator + { + private readonly ITextSource _textSource; + private int _pos; + + public TextRunEnumerator(ITextSource textSource, int firstTextSourceIndex) + { + _textSource = textSource; + _pos = firstTextSourceIndex; + Current = null; + } + + // ReSharper disable once MemberHidesStaticFromOuterClass + public TextRun Current { get; private set; } + + public bool MoveNext() + { + Current = _textSource.GetTextRun(_pos); + + if (Current is null) + { + return false; + } + + if (Current.TextSourceLength == 0) + { + return false; + } + + _pos += Current.TextSourceLength; + + return !(Current is TextEndOfLine); + } + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index 8d4475d1c3..2e2e4a8c68 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; -using Avalonia.Media.Immutable; using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.Utilities; using Avalonia.Platform; -using Avalonia.Utility; namespace Avalonia.Media.TextFormatting { @@ -13,11 +12,11 @@ namespace Avalonia.Media.TextFormatting /// public class TextLayout { - private static readonly ReadOnlySlice s_empty = new ReadOnlySlice(new[] { '\u200B' }); + private static readonly char[] s_empty = { '\u200B' }; private readonly ReadOnlySlice _text; private readonly TextParagraphProperties _paragraphProperties; - private readonly IReadOnlyList _textStyleOverrides; + private readonly IReadOnlyList> _textStyleOverrides; /// /// Initializes a new instance of the class. @@ -32,6 +31,8 @@ namespace Avalonia.Media.TextFormatting /// The text decorations. /// The maximum width. /// The maximum height. + /// The height of each line of text. + /// The maximum number of text lines. /// The text style overrides. public TextLayout( string text, @@ -44,35 +45,55 @@ namespace Avalonia.Media.TextFormatting TextDecorationCollection textDecorations = null, double maxWidth = double.PositiveInfinity, double maxHeight = double.PositiveInfinity, - IReadOnlyList textStyleOverrides = null) + double lineHeight = double.NaN, + int maxLines = 0, + IReadOnlyList> textStyleOverrides = null) { _text = string.IsNullOrEmpty(text) ? new ReadOnlySlice() : new ReadOnlySlice(text.AsMemory()); _paragraphProperties = - CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, textTrimming, textDecorations?.ToImmutable()); + CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, textTrimming, + textDecorations, lineHeight); _textStyleOverrides = textStyleOverrides; + LineHeight = lineHeight; + MaxWidth = maxWidth; MaxHeight = maxHeight; + MaxLines = maxLines; + UpdateLayout(); } + /// + /// Gets or sets the height of each line of text. + /// + /// + /// A value of NaN (equivalent to an attribute value of "Auto") indicates that the line height + /// is determined automatically from the current font characteristics. The default is NaN. + /// + public double LineHeight { get; } + /// /// Gets the maximum width. /// public double MaxWidth { get; } - /// /// Gets the maximum height. /// public double MaxHeight { get; } + /// + /// Gets the maximum number of text lines. + /// + public int MaxLines { get; } + /// /// Gets the text lines. /// @@ -94,7 +115,7 @@ namespace Avalonia.Media.TextFormatting /// /// The drawing context. /// The origin. - public void Draw(IDrawingContextImpl context, Point origin) + public void Draw(DrawingContext context, Point origin) { if (!TextLines.Any()) { @@ -121,14 +142,16 @@ namespace Avalonia.Media.TextFormatting /// The text wrapping. /// The text trimming. /// The text decorations. + /// The height of each line of text. /// private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize, IBrush foreground, TextAlignment textAlignment, TextWrapping textWrapping, TextTrimming textTrimming, - ImmutableTextDecoration[] textDecorations) + TextDecorationCollection textDecorations, double lineHeight) { - var textRunStyle = new TextStyle(typeface, fontSize, foreground, textDecorations); + var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground); - return new TextParagraphProperties(textRunStyle, textAlignment, textWrapping, textTrimming); + return new GenericTextParagraphProperties(textRunStyle, textAlignment, textWrapping, textTrimming, + lineHeight); } /// @@ -159,14 +182,15 @@ namespace Avalonia.Media.TextFormatting /// The empty text line. private TextLine CreateEmptyTextLine(int startingIndex) { - var textFormat = _paragraphProperties.DefaultTextStyle.TextFormat; + var properties = _paragraphProperties.DefaultTextRunProperties; - var glyphRun = TextShaper.Current.ShapeText(s_empty, textFormat); + var glyphRun = TextShaper.Current.ShapeText(new ReadOnlySlice(s_empty, startingIndex, 1), + properties.Typeface, properties.FontRenderingEmSize, properties.CultureInfo); - var textRuns = new[] { new ShapedTextRun(glyphRun, _paragraphProperties.DefaultTextStyle) }; + var textRuns = new[] { new ShapedTextCharacters(glyphRun, _paragraphProperties.DefaultTextRunProperties) }; - return new SimpleTextLine(new TextPointer(startingIndex, 0), textRuns, - TextLineMetrics.Create(textRuns, MaxWidth, _paragraphProperties.TextAlignment)); + return new TextLineImpl(textRuns, + TextLineMetrics.Create(textRuns, new TextRange(startingIndex, 1), MaxWidth, _paragraphProperties)); } /// @@ -174,7 +198,7 @@ namespace Avalonia.Media.TextFormatting /// private void UpdateLayout() { - if (_text.IsEmpty || Math.Abs(MaxWidth) < double.Epsilon || Math.Abs(MaxHeight) < double.Epsilon) + if (_text.IsEmpty || MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) { var textLine = CreateEmptyTextLine(0); @@ -188,77 +212,38 @@ namespace Avalonia.Media.TextFormatting double left = 0.0, right = 0.0, bottom = 0.0; - var lineBreaker = new LineBreakEnumerator(_text); - var currentPosition = 0; - while (currentPosition < _text.Length) + var textSource = new FormattedTextSource(_text, + _paragraphProperties.DefaultTextRunProperties, _textStyleOverrides); + + TextLineBreak previousLineBreak = null; + + while (currentPosition < _text.Length && (MaxLines == 0 || textLines.Count < MaxLines)) { - int length; + var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth, + _paragraphProperties, previousLineBreak); - if (lineBreaker.MoveNext()) - { - if (!lineBreaker.Current.Required) - { - continue; - } + previousLineBreak = textLine.LineBreak; - length = lineBreaker.Current.PositionWrap - currentPosition; + textLines.Add(textLine); - if (currentPosition + length < _text.Length) - { - //The line breaker isn't treating \n\r as a pair so we have to fix that here. - if (_text[lineBreaker.Current.PositionMeasure] == '\n' - && _text[lineBreaker.Current.PositionWrap] == '\r') - { - length++; - } - } - } - else + UpdateBounds(textLine, ref left, ref right, ref bottom); + + if (!double.IsPositiveInfinity(MaxHeight) && bottom > MaxHeight) { - length = _text.Length - currentPosition; + break; } - var remainingLength = length; + currentPosition += textLine.TextRange.Length; - while (remainingLength > 0) + if (currentPosition != _text.Length || textLine.LineBreak == null) { - var textSlice = _text.AsSlice(currentPosition, remainingLength); - - var textSource = new FormattedTextSource(textSlice, _paragraphProperties.DefaultTextStyle, _textStyleOverrides); - - var textLine = TextFormatter.Current.FormatLine(textSource, 0, MaxWidth, _paragraphProperties); - - UpdateBounds(textLine, ref left, ref right, ref bottom); - - textLines.Add(textLine); - - if (!double.IsPositiveInfinity(MaxHeight) && bottom + textLine.LineMetrics.Size.Height > MaxHeight) - { - currentPosition = _text.Length; - break; - } - - if (_paragraphProperties.TextTrimming != TextTrimming.None) - { - currentPosition += remainingLength; - - break; - } - - remainingLength -= textLine.Text.Length; - - currentPosition += textLine.Text.Length; + continue; } - } - if (lineBreaker.Current.Required && currentPosition == _text.Length) - { var emptyTextLine = CreateEmptyTextLine(currentPosition); - UpdateBounds(emptyTextLine, ref left, ref right, ref bottom); - textLines.Add(emptyTextLine); } @@ -268,22 +253,27 @@ namespace Avalonia.Media.TextFormatting } } - private struct FormattedTextSource : ITextSource + private readonly struct FormattedTextSource : ITextSource { private readonly ReadOnlySlice _text; - private readonly TextStyle _defaultStyle; - private readonly IReadOnlyList _textStyleOverrides; + private readonly TextRunProperties _defaultProperties; + private readonly IReadOnlyList> _textModifier; - public FormattedTextSource(ReadOnlySlice text, TextStyle defaultStyle, - IReadOnlyList textStyleOverrides) + public FormattedTextSource(ReadOnlySlice text, TextRunProperties defaultProperties, + IReadOnlyList> textModifier) { _text = text; - _defaultStyle = defaultStyle; - _textStyleOverrides = textStyleOverrides; + _defaultProperties = defaultProperties; + _textModifier = textModifier; } public TextRun GetTextRun(int textSourceIndex) { + if (textSourceIndex > _text.End) + { + return new TextEndOfLine(); + } + var runText = _text.Skip(textSourceIndex); if (runText.IsEmpty) @@ -291,30 +281,29 @@ namespace Avalonia.Media.TextFormatting return new TextEndOfLine(); } - var textStyleRun = CreateTextStyleRunWithOverride(runText, _defaultStyle, _textStyleOverrides); + var textStyleRun = CreateTextStyleRun(runText, _defaultProperties, _textModifier); - return new TextCharacters(runText.Take(textStyleRun.TextPointer.Length), textStyleRun.Style); + return new TextCharacters(runText.Take(textStyleRun.Length), textStyleRun.Value); } /// - /// Creates a text style run that has overrides applied. Only overrides with equal TextStyle. - /// If optimizeForShaping is true Foreground is ignored. + /// Creates a span of text run properties that has modifier applied. /// - /// The text to create the run for. - /// The default text style for segments that don't have an override. - /// The text style overrides. + /// The text to create the properties for. + /// The default text properties. + /// The text properties modifier. /// /// The created text style run. /// - private static TextStyleRun CreateTextStyleRunWithOverride(ReadOnlySlice text, - TextStyle defaultTextStyle, IReadOnlyList textStyleOverrides) + private static ValueSpan CreateTextStyleRun(ReadOnlySlice text, + TextRunProperties defaultProperties, IReadOnlyList> textModifier) { - if(textStyleOverrides == null || textStyleOverrides.Count == 0) + if (textModifier == null || textModifier.Count == 0) { - return new TextStyleRun(new TextPointer(text.Start, text.Length), defaultTextStyle); + return new ValueSpan(text.Start, text.Length, defaultProperties); } - var currentTextStyle = defaultTextStyle; + var currentProperties = defaultProperties; var hasOverride = false; @@ -322,35 +311,34 @@ namespace Avalonia.Media.TextFormatting var length = 0; - for (; i < textStyleOverrides.Count; i++) + for (; i < textModifier.Count; i++) { - var styleOverride = textStyleOverrides[i]; + var propertiesOverride = textModifier[i]; - var textPointer = styleOverride.TextPointer; + var textRange = new TextRange(propertiesOverride.Start, propertiesOverride.Length); - if (textPointer.End < text.Start) + if (textRange.End < text.Start) { continue; } - if (textPointer.Start > text.End) + if (textRange.Start > text.End) { length = text.Length; break; } - if (textPointer.Start > text.Start) + if (textRange.Start > text.Start) { - if (styleOverride.Style.TextFormat != currentTextStyle.TextFormat || - !currentTextStyle.Foreground.Equals(styleOverride.Style.Foreground)) + if (propertiesOverride.Value != currentProperties) { - length = Math.Min(Math.Abs(textPointer.Start - text.Start), text.Length); + length = Math.Min(Math.Abs(textRange.Start - text.Start), text.Length); break; } } - length += Math.Min(text.Length - length, textPointer.Length); + length += Math.Min(text.Length - length, textRange.Length); if (hasOverride) { @@ -359,13 +347,12 @@ namespace Avalonia.Media.TextFormatting hasOverride = true; - currentTextStyle = styleOverride.Style; + currentProperties = propertiesOverride.Value; } - if (length < text.Length && i == textStyleOverrides.Count) + if (length < text.Length && i == textModifier.Count) { - if (currentTextStyle.Foreground.Equals(defaultTextStyle.Foreground) && - currentTextStyle.TextFormat == defaultTextStyle.TextFormat) + if (currentProperties == defaultProperties) { length = text.Length; } @@ -376,7 +363,7 @@ namespace Avalonia.Media.TextFormatting text = text.Take(length); } - return new TextStyleRun(new TextPointer(text.Start, length), currentTextStyle); + return new ValueSpan(text.Start, length, currentProperties); } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs index a0f7b44882..c3b7dfc77a 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Avalonia.Platform; namespace Avalonia.Media.TextFormatting { @@ -9,12 +8,12 @@ namespace Avalonia.Media.TextFormatting public abstract class TextLine { /// - /// Gets the text. + /// Gets the text range that is covered by the line. /// /// - /// The text pointer. + /// The text range that is covered by the line. /// - public abstract TextPointer Text { get; } + public abstract TextRange TextRange { get; } /// /// Gets the text runs. @@ -32,12 +31,20 @@ namespace Avalonia.Media.TextFormatting /// public abstract TextLineMetrics LineMetrics { get; } + /// + /// Gets the state of the line when broken by line breaking process. + /// + /// + /// A value that represents the line break. + /// + public abstract TextLineBreak LineBreak { get; } + /// /// Draws the at the given origin. /// /// The drawing context. /// The origin. - public abstract void Draw(IDrawingContextImpl drawingContext, Point origin); + public abstract void Draw(DrawingContext drawingContext, Point origin); /// /// Client to get the character hit corresponding to the specified diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs new file mode 100644 index 0000000000..c24454cb76 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Avalonia.Media.TextFormatting +{ + public class TextLineBreak + { + public TextLineBreak(IReadOnlyList remainingCharacters) + { + RemainingCharacters = remainingCharacters; + } + + /// + /// Get the remaining shaped characters that were split up by the during the formatting process. + /// + public IReadOnlyList RemainingCharacters { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs new file mode 100644 index 0000000000..cf00399b8a --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs @@ -0,0 +1,235 @@ +using System.Collections.Generic; + +namespace Avalonia.Media.TextFormatting +{ + internal class TextLineImpl : TextLine + { + private readonly IReadOnlyList _textRuns; + + public TextLineImpl(IReadOnlyList textRuns, TextLineMetrics lineMetrics, + TextLineBreak lineBreak = null) + { + _textRuns = textRuns; + LineMetrics = lineMetrics; + LineBreak = lineBreak; + } + + /// + public override TextRange TextRange => LineMetrics.TextRange; + + /// + public override IReadOnlyList TextRuns => _textRuns; + + /// + public override TextLineMetrics LineMetrics { get; } + + /// + public override TextLineBreak LineBreak { get; } + + /// + public override void Draw(DrawingContext drawingContext, Point origin) + { + var currentX = origin.X; + + foreach (var textRun in _textRuns) + { + var baselineOrigin = new Point(currentX + LineMetrics.BaselineOrigin.X, + origin.Y + LineMetrics.BaselineOrigin.Y); + + textRun.Draw(drawingContext, baselineOrigin); + + currentX += textRun.Bounds.Width; + } + } + + /// + public override CharacterHit GetCharacterHitFromDistance(double distance) + { + if (distance < 0) + { + // hit happens before the line, return the first position + return new CharacterHit(TextRange.Start); + } + + // process hit that happens within the line + var characterHit = new CharacterHit(); + + foreach (var run in _textRuns) + { + characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _); + + if (distance <= run.Bounds.Width) + { + break; + } + + distance -= run.Bounds.Width; + } + + return characterHit; + } + + /// + public override double GetDistanceFromCharacterHit(CharacterHit characterHit) + { + return DistanceFromCodepointIndex(characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0)); + } + + /// + public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) + { + if (TryFindNextCharacterHit(characterHit, out var nextCharacterHit)) + { + return nextCharacterHit; + } + + return new CharacterHit(TextRange.End); // Can't move, we're after the last character + } + + /// + public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) + { + if (TryFindPreviousCharacterHit(characterHit, out var previousCharacterHit)) + { + return previousCharacterHit; + } + + return new CharacterHit(TextRange.Start); // Can't move, we're before the first character + } + + /// + public override CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit) + { + // same operation as move-to-previous + return GetPreviousCaretCharacterHit(characterHit); + } + + /// + /// Get distance from line start to the specified codepoint index. + /// + private double DistanceFromCodepointIndex(int codepointIndex) + { + var currentDistance = 0.0; + + foreach (var textRun in _textRuns) + { + if (codepointIndex > textRun.Text.End) + { + currentDistance += textRun.Bounds.Width; + + continue; + } + + return currentDistance + textRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(codepointIndex)); + } + + return currentDistance; + } + + /// + /// Tries to find the next character hit. + /// + /// The current character hit. + /// The next character hit. + /// + private bool TryFindNextCharacterHit(CharacterHit characterHit, out CharacterHit nextCharacterHit) + { + nextCharacterHit = characterHit; + + var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + + if (codepointIndex >= TextRange.Start + TextRange.Length) + { + return false; // Cannot go forward anymore + } + + var runIndex = GetRunIndexAtCodepointIndex(codepointIndex); + + while (runIndex < TextRuns.Count) + { + var run = _textRuns[runIndex]; + + nextCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); + + if (codepointIndex <= nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength) + { + return true; + } + + runIndex++; + } + + return false; + } + + /// + /// Tries to find the previous character hit. + /// + /// The current character hit. + /// The previous character hit. + /// + private bool TryFindPreviousCharacterHit(CharacterHit characterHit, out CharacterHit previousCharacterHit) + { + previousCharacterHit = characterHit; + + var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + + if (codepointIndex < TextRange.Start) + { + return false; // Cannot go backward anymore. + } + + var runIndex = GetRunIndexAtCodepointIndex(codepointIndex); + + while (runIndex >= 0) + { + var run = _textRuns[runIndex]; + + previousCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _); + + if (previousCharacterHit.FirstCharacterIndex < codepointIndex) + { + return true; + } + + runIndex--; + } + + return false; + } + + /// + /// Gets the run index of the specified codepoint index. + /// + /// The codepoint index. + /// The text run index. + private int GetRunIndexAtCodepointIndex(int codepointIndex) + { + if (codepointIndex >= TextRange.End) + { + return _textRuns.Count - 1; + } + + if (codepointIndex <= 0) + { + return 0; + } + + var runIndex = 0; + + while (runIndex < _textRuns.Count) + { + var run = _textRuns[runIndex]; + + if (run.Text.End > codepointIndex) + { + return runIndex; + } + + runIndex++; + } + + return runIndex; + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs index 096305c09c..d47cc0c394 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -8,38 +9,20 @@ namespace Avalonia.Media.TextFormatting /// public readonly struct TextLineMetrics { - public TextLineMetrics(double width, double xOrigin, double ascent, double descent, double lineGap) + public TextLineMetrics(Size size, Point baselineOrigin, TextRange textRange) { - Ascent = ascent; - Descent = descent; - LineGap = lineGap; - Size = new Size(width, descent - ascent + lineGap); - BaselineOrigin = new Point(xOrigin, -ascent); + Size = size; + BaselineOrigin = baselineOrigin; + TextRange = textRange; } /// - /// Gets the overall recommended distance above the baseline. + /// Gets the text range that is covered by the text line. /// /// - /// The ascent. + /// The text range that is covered by the text line. /// - public double Ascent { get; } - - /// - /// Gets the overall recommended distance under the baseline. - /// - /// - /// The descent. - /// - public double Descent { get; } - - /// - /// Gets the overall recommended additional space between two lines of text. - /// - /// - /// The leading. - /// - public double LineGap { get; } + public TextRange TextRange { get; } /// /// Gets the size of the text line. @@ -61,10 +44,12 @@ namespace Avalonia.Media.TextFormatting /// Creates the text line metrics. /// /// The text runs. + /// The text range that is covered by the text line. /// The paragraph width. - /// The text alignment. + /// The text alignment. /// - public static TextLineMetrics Create(IEnumerable textRuns, double paragraphWidth, TextAlignment textAlignment) + public static TextLineMetrics Create(IEnumerable textRuns, TextRange textRange, double paragraphWidth, + TextParagraphProperties paragraphProperties) { var lineWidth = 0.0; var ascent = 0.0; @@ -73,31 +58,39 @@ namespace Avalonia.Media.TextFormatting foreach (var textRun in textRuns) { - var shapedRun = (ShapedTextRun)textRun; + var shapedRun = (ShapedTextCharacters)textRun; - lineWidth += shapedRun.Bounds.Width; + var fontMetrics = + new FontMetrics(shapedRun.Properties.Typeface, shapedRun.Properties.FontRenderingEmSize); - var textFormat = textRun.Style.TextFormat; + lineWidth += shapedRun.Bounds.Width; - if (ascent > textRun.Style.TextFormat.FontMetrics.Ascent) + if (ascent > fontMetrics.Ascent) { - ascent = textFormat.FontMetrics.Ascent; + ascent = fontMetrics.Ascent; } - if (descent < textFormat.FontMetrics.Descent) + if (descent < fontMetrics.Descent) { - descent = textFormat.FontMetrics.Descent; + descent = fontMetrics.Descent; } - if (lineGap < textFormat.FontMetrics.LineGap) + if (lineGap < fontMetrics.LineGap) { - lineGap = textFormat.FontMetrics.LineGap; + lineGap = fontMetrics.LineGap; } } - var xOrigin = TextLine.GetParagraphOffsetX(lineWidth, paragraphWidth, textAlignment); + var xOrigin = TextLine.GetParagraphOffsetX(lineWidth, paragraphWidth, paragraphProperties.TextAlignment); + + var baselineOrigin = new Point(xOrigin, -ascent); + + var size = new Size(lineWidth, + double.IsNaN(paragraphProperties.LineHeight) || MathUtilities.IsZero(paragraphProperties.LineHeight) ? + descent - ascent + lineGap : + paragraphProperties.LineHeight); - return new TextLineMetrics(lineWidth, xOrigin, ascent, descent, lineGap); + return new TextLineMetrics(size, baselineOrigin, textRange); } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs index 1368f1777a..39eb695404 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs @@ -3,38 +3,37 @@ /// /// Provides a set of properties that are used during the paragraph layout. /// - public readonly struct TextParagraphProperties + public abstract class TextParagraphProperties { - public TextParagraphProperties( - TextStyle defaultTextStyle, - TextAlignment textAlignment = TextAlignment.Left, - TextWrapping textWrapping = TextWrapping.NoWrap, - TextTrimming textTrimming = TextTrimming.None) - { - DefaultTextStyle = defaultTextStyle; - TextAlignment = textAlignment; - TextWrapping = textWrapping; - TextTrimming = textTrimming; - } + /// + /// Gets the text alignment. + /// + public abstract TextAlignment TextAlignment { get; } /// /// Gets the default text style. /// - public TextStyle DefaultTextStyle { get; } + public abstract TextRunProperties DefaultTextRunProperties { get; } /// - /// Gets the text alignment. + /// If not null, text decorations to apply to all runs in the line. This is in addition + /// to any text decorations specified by the TextRunProperties for individual text runs. /// - public TextAlignment TextAlignment { get; } + public virtual TextDecorationCollection TextDecorations => null; /// /// Gets the text wrapping. /// - public TextWrapping TextWrapping { get; } + public abstract TextWrapping TextWrapping { get; } /// /// Gets the text trimming. /// - public TextTrimming TextTrimming { get; } + public abstract TextTrimming TextTrimming { get; } + + /// + /// Paragraph's line height + /// + public abstract double LineHeight { get; } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextPointer.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRange.cs similarity index 73% rename from src/Avalonia.Visuals/Media/TextFormatting/TextPointer.cs rename to src/Avalonia.Visuals/Media/TextFormatting/TextRange.cs index 65d5c04b4c..1177c758f4 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextPointer.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRange.cs @@ -5,9 +5,9 @@ namespace Avalonia.Media.TextFormatting /// /// References a portion of a text buffer. /// - public readonly struct TextPointer + public readonly struct TextRange { - public TextPointer(int start, int length) + public TextRange(int start, int length) { Start = start; Length = length; @@ -41,30 +41,30 @@ namespace Avalonia.Media.TextFormatting /// Returns a specified number of contiguous elements from the start of the slice. /// /// The number of elements to return. - /// A that contains the specified number of elements from the start of this slice. - public TextPointer Take(int length) + /// A that contains the specified number of elements from the start of this slice. + public TextRange Take(int length) { if (length > Length) { throw new ArgumentOutOfRangeException(nameof(length)); } - return new TextPointer(Start, length); + return new TextRange(Start, length); } /// /// Bypasses a specified number of elements in the slice and then returns the remaining elements. /// /// The number of elements to skip before returning the remaining elements. - /// A that contains the elements that occur after the specified index in this slice. - public TextPointer Skip(int length) + /// A that contains the elements that occur after the specified index in this slice. + public TextRange Skip(int length) { if (length > Length) { throw new ArgumentOutOfRangeException(nameof(length)); } - return new TextPointer(Start + length, Length - length); + return new TextRange(Start + length, Length - length); } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs index 28b83333b9..c15a771755 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs @@ -1,5 +1,5 @@ using System.Diagnostics; -using Avalonia.Utility; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -9,15 +9,22 @@ namespace Avalonia.Media.TextFormatting [DebuggerTypeProxy(typeof(TextRunDebuggerProxy))] public abstract class TextRun { + public static readonly int DefaultTextSourceLength = 1; + + /// + /// Gets the text source length. + /// + public virtual int TextSourceLength => DefaultTextSourceLength; + /// /// Gets the text run's text. /// - public ReadOnlySlice Text { get; protected set; } + public virtual ReadOnlySlice Text => default; /// - /// Gets the text run's style. + /// A set of properties shared by every characters in the run /// - public TextStyle Style { get; protected set; } + public virtual TextRunProperties Properties => null; private class TextRunDebuggerProxy { @@ -42,7 +49,7 @@ namespace Avalonia.Media.TextFormatting } } - public TextStyle Style => _textRun.Style; + public TextRunProperties Properties => _textRun.Properties; } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs new file mode 100644 index 0000000000..bbcdfe2d8e --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs @@ -0,0 +1,90 @@ +using System; +using System.Globalization; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Properties that can change from one run to the next, such as typeface or foreground brush. + /// + /// + /// The client provides a concrete implementation of this abstract run properties class. This + /// allows client to implement their run properties the way that fits with their run formatting + /// store. + /// + public abstract class TextRunProperties : IEquatable + { + /// + /// Run typeface + /// + public abstract Typeface Typeface { get; } + + /// + /// Em size of font used to format and display text + /// + public abstract double FontRenderingEmSize { get; } + + /// + /// Run TextDecorations. + /// + public abstract TextDecorationCollection TextDecorations { get; } + + /// + /// Brush used to fill text. + /// + public abstract IBrush ForegroundBrush { get; } + + /// + /// Brush used to paint background of run. + /// + public abstract IBrush BackgroundBrush { get; } + + /// + /// Run text culture. + /// + public abstract CultureInfo CultureInfo { get; } + + public bool Equals(TextRunProperties other) + { + if (ReferenceEquals(null, other)) + return false; + if (ReferenceEquals(this, other)) + return true; + + return Typeface.Equals(other.Typeface) && + FontRenderingEmSize.Equals(other.FontRenderingEmSize) + && Equals(TextDecorations, other.TextDecorations) && + Equals(ForegroundBrush, other.ForegroundBrush) && + Equals(BackgroundBrush, other.BackgroundBrush) && + Equals(CultureInfo, other.CultureInfo); + } + + public override bool Equals(object obj) + { + return ReferenceEquals(this, obj) || obj is TextRunProperties other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = (Typeface != null ? Typeface.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ FontRenderingEmSize.GetHashCode(); + hashCode = (hashCode * 397) ^ (TextDecorations != null ? TextDecorations.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (ForegroundBrush != null ? ForegroundBrush.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (BackgroundBrush != null ? BackgroundBrush.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (CultureInfo != null ? CultureInfo.GetHashCode() : 0); + return hashCode; + } + } + + public static bool operator ==(TextRunProperties left, TextRunProperties right) + { + return Equals(left, right); + } + + public static bool operator !=(TextRunProperties left, TextRunProperties right) + { + return !Equals(left, right); + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs index eb3a4129bc..a02ace408f 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs @@ -1,6 +1,7 @@ using System; +using System.Globalization; using Avalonia.Platform; -using Avalonia.Utility; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -44,9 +45,10 @@ namespace Avalonia.Media.TextFormatting } /// - public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat) + public GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize, + CultureInfo culture) { - return _platformImpl.ShapeText(text, textFormat); + return _platformImpl.ShapeText(text, typeface, fontRenderingEmSize, culture); } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextStyle.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextStyle.cs deleted file mode 100644 index cf52c3ca17..0000000000 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextStyle.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Avalonia.Media.Immutable; - -namespace Avalonia.Media.TextFormatting -{ - /// - /// Unique text formatting properties that effect the styling of a text. - /// - public readonly struct TextStyle - { - public TextStyle(Typeface typeface, double fontRenderingEmSize = 12, IBrush foreground = null, - ImmutableTextDecoration[] textDecorations = null) - : this(new TextFormat(typeface, fontRenderingEmSize), foreground, textDecorations) - { - } - - public TextStyle(TextFormat textFormat, IBrush foreground = null, - ImmutableTextDecoration[] textDecorations = null) - { - TextFormat = textFormat; - Foreground = foreground; - TextDecorations = textDecorations; - } - - /// - /// Gets the text format. - /// - public TextFormat TextFormat { get; } - - /// - /// Gets the foreground. - /// - public IBrush Foreground { get; } - - /// - /// Gets the text decorations. - /// - public ImmutableTextDecoration[] TextDecorations { get; } - } -} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextStyleRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextStyleRun.cs deleted file mode 100644 index 55f8999182..0000000000 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextStyleRun.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Avalonia.Media.TextFormatting -{ - /// - /// Represents a text run's style and is used during the layout process of the . - /// - public readonly struct TextStyleRun - { - public TextStyleRun(TextPointer textPointer, TextStyle style) - { - TextPointer = textPointer; - Style = style; - } - - /// - /// Gets the text pointer. - /// - public TextPointer TextPointer { get; } - - /// - /// Gets the text style. - /// - public TextStyle Style { get; } - } -} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiClass.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiClass.cs index 03576a4c40..ad3cc9141b 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiClass.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiClass.cs @@ -2,6 +2,7 @@ namespace Avalonia.Media.TextFormatting.Unicode { public enum BiDiClass { + LeftToRight, //L ArabicLetter, //AL ArabicNumber, //AN ParagraphSeparator, //B @@ -11,7 +12,6 @@ namespace Avalonia.Media.TextFormatting.Unicode EuropeanSeparator, //ES EuropeanTerminator, //ET FirstStrongIsolate, //FSI - LeftToRight, //L LeftToRightEmbedding, //LRE LeftToRightIsolate, //LRI LeftToRightOverride, //LRO diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BreakPairTable.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BreakPairTable.cs index c13074711e..86d39a4283 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BreakPairTable.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BreakPairTable.cs @@ -4,38 +4,39 @@ namespace Avalonia.Media.TextFormatting.Unicode { private static readonly byte[][] s_breakPairTable = { - new byte[] {4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,3,4,4,4,4,4,4,4,4,4,4}, - new byte[] {0,4,4,1,1,4,4,4,4,1,1,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {0,4,4,1,1,4,4,4,4,1,1,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {4,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,1,0,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {1,4,4,1,1,1,4,4,4,0,0,1,1,1,1,0,1,1,0,0,4,2,4,1,1,1,1,1,0,1,1,1}, - new byte[] {1,4,4,1,1,1,4,4,4,0,0,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {0,4,4,1,0,1,4,4,4,0,0,1,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {0,4,4,1,0,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,4,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,1,1,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,1,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,1,1,1,1,0,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,1,1,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,1,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,1,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,1,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,1,0,1,1,0,0,4,2,4,0,0,0,0,0,0,1,1,1}, + new byte[] {4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,3,4,4,4,4,4,4,4,4,4,4,4}, + new byte[] {0,4,4,1,1,4,4,4,4,1,1,0,0,0,0,4,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {0,4,4,1,1,4,4,4,4,1,1,1,1,1,0,4,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {4,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1,1}, + new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1,1}, + new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,0,1,0,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,0,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {1,4,4,1,1,1,4,4,4,0,0,1,1,1,1,0,1,1,0,0,4,2,4,1,1,1,1,1,0,1,1,1,0}, + new byte[] {1,4,4,1,1,1,4,4,4,0,0,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {0,4,4,1,0,1,4,4,4,0,0,1,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {0,4,4,1,0,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,4,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0}, + new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1,1}, + new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,1,1,0,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,1,0,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,1,1,1,1,0,0,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,1,1,0,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,1,0,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,1,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,1,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {0,4,4,1,1,0,4,4,4,0,0,0,0,0,0,0,0,0,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, }; public static PairBreakType Map(LineBreakClass first, LineBreakClass second) diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs index 94171b7324..20fe345d93 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs @@ -1,4 +1,4 @@ -using Avalonia.Utility; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs index 2ff4952cab..9e1f748ebb 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs @@ -1,4 +1,4 @@ -using Avalonia.Utility; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Grapheme.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Grapheme.cs index a6791b4a53..f268340eb9 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Grapheme.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Grapheme.cs @@ -1,4 +1,4 @@ -using Avalonia.Utility; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeBreakClass.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeBreakClass.cs index 684baae51f..71e4bce106 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeBreakClass.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeBreakClass.cs @@ -2,24 +2,20 @@ namespace Avalonia.Media.TextFormatting.Unicode { public enum GraphemeBreakClass { - Control, //CN - CR, //CR - EBase, //EB - EBaseGAZ, //EBG - EModifier, //EM - Extend, //EX - GlueAfterZwj, //GAZ - L, //L - LF, //LF - LV, //LV - LVT, //LVT - Prepend, //PP - RegionalIndicator, //RI - SpacingMark, //SM - T, //T - V, //V - Other, //XX - ZWJ, //ZWJ - ExtendedPictographic + Other, + CR, + LF, + Control, + Extend, + ZWJ, + RegionalIndicator, + Prepend, + SpacingMark, + L, + V, + T, + LV, + LVT, + ExtendedPictographic, } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeEnumerator.cs index fd7831dfe6..1e4ac8fe0f 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeEnumerator.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeEnumerator.cs @@ -4,7 +4,7 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System.Runtime.InteropServices; -using Avalonia.Utility; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakClass.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakClass.cs index 925706dd4f..8b2e3f41e3 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakClass.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakClass.cs @@ -34,10 +34,11 @@ namespace Avalonia.Media.TextFormatting.Unicode EBase, //EB EModifier, //EM ZWJ, //ZWJ + ContingentBreak, //CB + Unknown, //XX Ambiguous, //AI MandatoryBreak, //BK - ContingentBreak, //CB ConditionalJapaneseStarter, //CJ CarriageReturn, //CR LineFeed, //LF @@ -45,6 +46,5 @@ namespace Avalonia.Media.TextFormatting.Unicode ComplexContext, //SA Surrogate, //SG Space, //SP - Unknown, //XX } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs index a11c008409..26f7721128 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs @@ -16,7 +16,7 @@ // Ported from: https://github.com/foliojs/linebreak // Copied from: https://github.com/toptensoftware/RichTextKit -using Avalonia.Utility; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode { @@ -95,7 +95,8 @@ namespace Avalonia.Media.TextFormatting.Unicode if (_nextClass.Value == LineBreakClass.MandatoryBreak) { - Current = new LineBreak(FindPriorNonWhitespace(_lastPos), _lastPos); + _lastPos = _pos; + Current = new LineBreak(FindPriorNonWhitespace(_lastPos), _lastPos, true); return true; } @@ -108,6 +109,7 @@ namespace Avalonia.Media.TextFormatting.Unicode { case PairBreakType.DI: // Direct break shouldBreak = true; + _lastPos = _pos; break; case PairBreakType.IN: // possible indirect break diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/PropertyValueAliasHelper.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/PropertyValueAliasHelper.cs new file mode 100644 index 0000000000..388a7d257d --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/PropertyValueAliasHelper.cs @@ -0,0 +1,178 @@ +using System.Collections.Generic; + +namespace Avalonia.Media.TextFormatting.Unicode +{ + internal static class PropertyValueAliasHelper + { + private static readonly Dictionary s_scriptToTag = + new Dictionary{ + { Script.Unknown, "Zzzz"}, + { Script.Common, "Zyyy"}, + { Script.Inherited, "Zinh"}, + { Script.Adlam, "Adlm"}, + { Script.CaucasianAlbanian, "Aghb"}, + { Script.Ahom, "Ahom"}, + { Script.Arabic, "Arab"}, + { Script.ImperialAramaic, "Armi"}, + { Script.Armenian, "Armn"}, + { Script.Avestan, "Avst"}, + { Script.Balinese, "Bali"}, + { Script.Bamum, "Bamu"}, + { Script.BassaVah, "Bass"}, + { Script.Batak, "Batk"}, + { Script.Bengali, "Beng"}, + { Script.Bhaiksuki, "Bhks"}, + { Script.Bopomofo, "Bopo"}, + { Script.Brahmi, "Brah"}, + { Script.Braille, "Brai"}, + { Script.Buginese, "Bugi"}, + { Script.Buhid, "Buhd"}, + { Script.Chakma, "Cakm"}, + { Script.CanadianAboriginal, "Cans"}, + { Script.Carian, "Cari"}, + { Script.Cham, "Cham"}, + { Script.Cherokee, "Cher"}, + { Script.Chorasmian, "Chrs"}, + { Script.Coptic, "Copt"}, + { Script.Cypriot, "Cprt"}, + { Script.Cyrillic, "Cyrl"}, + { Script.Devanagari, "Deva"}, + { Script.DivesAkuru, "Diak"}, + { Script.Dogra, "Dogr"}, + { Script.Deseret, "Dsrt"}, + { Script.Duployan, "Dupl"}, + { Script.EgyptianHieroglyphs, "Egyp"}, + { Script.Elbasan, "Elba"}, + { Script.Elymaic, "Elym"}, + { Script.Ethiopic, "Ethi"}, + { Script.Georgian, "Geor"}, + { Script.Glagolitic, "Glag"}, + { Script.GunjalaGondi, "Gong"}, + { Script.MasaramGondi, "Gonm"}, + { Script.Gothic, "Goth"}, + { Script.Grantha, "Gran"}, + { Script.Greek, "Grek"}, + { Script.Gujarati, "Gujr"}, + { Script.Gurmukhi, "Guru"}, + { Script.Hangul, "Hang"}, + { Script.Han, "Hani"}, + { Script.Hanunoo, "Hano"}, + { Script.Hatran, "Hatr"}, + { Script.Hebrew, "Hebr"}, + { Script.Hiragana, "Hira"}, + { Script.AnatolianHieroglyphs, "Hluw"}, + { Script.PahawhHmong, "Hmng"}, + { Script.NyiakengPuachueHmong, "Hmnp"}, + { Script.KatakanaOrHiragana, "Hrkt"}, + { Script.OldHungarian, "Hung"}, + { Script.OldItalic, "Ital"}, + { Script.Javanese, "Java"}, + { Script.KayahLi, "Kali"}, + { Script.Katakana, "Kana"}, + { Script.Kharoshthi, "Khar"}, + { Script.Khmer, "Khmr"}, + { Script.Khojki, "Khoj"}, + { Script.KhitanSmallScript, "Kits"}, + { Script.Kannada, "Knda"}, + { Script.Kaithi, "Kthi"}, + { Script.TaiTham, "Lana"}, + { Script.Lao, "Laoo"}, + { Script.Latin, "Latn"}, + { Script.Lepcha, "Lepc"}, + { Script.Limbu, "Limb"}, + { Script.LinearA, "Lina"}, + { Script.LinearB, "Linb"}, + { Script.Lisu, "Lisu"}, + { Script.Lycian, "Lyci"}, + { Script.Lydian, "Lydi"}, + { Script.Mahajani, "Mahj"}, + { Script.Makasar, "Maka"}, + { Script.Mandaic, "Mand"}, + { Script.Manichaean, "Mani"}, + { Script.Marchen, "Marc"}, + { Script.Medefaidrin, "Medf"}, + { Script.MendeKikakui, "Mend"}, + { Script.MeroiticCursive, "Merc"}, + { Script.MeroiticHieroglyphs, "Mero"}, + { Script.Malayalam, "Mlym"}, + { Script.Modi, "Modi"}, + { Script.Mongolian, "Mong"}, + { Script.Mro, "Mroo"}, + { Script.MeeteiMayek, "Mtei"}, + { Script.Multani, "Mult"}, + { Script.Myanmar, "Mymr"}, + { Script.Nandinagari, "Nand"}, + { Script.OldNorthArabian, "Narb"}, + { Script.Nabataean, "Nbat"}, + { Script.Newa, "Newa"}, + { Script.Nko, "Nkoo"}, + { Script.Nushu, "Nshu"}, + { Script.Ogham, "Ogam"}, + { Script.OlChiki, "Olck"}, + { Script.OldTurkic, "Orkh"}, + { Script.Oriya, "Orya"}, + { Script.Osage, "Osge"}, + { Script.Osmanya, "Osma"}, + { Script.Palmyrene, "Palm"}, + { Script.PauCinHau, "Pauc"}, + { Script.OldPermic, "Perm"}, + { Script.PhagsPa, "Phag"}, + { Script.InscriptionalPahlavi, "Phli"}, + { Script.PsalterPahlavi, "Phlp"}, + { Script.Phoenician, "Phnx"}, + { Script.Miao, "Plrd"}, + { Script.InscriptionalParthian, "Prti"}, + { Script.Rejang, "Rjng"}, + { Script.HanifiRohingya, "Rohg"}, + { Script.Runic, "Runr"}, + { Script.Samaritan, "Samr"}, + { Script.OldSouthArabian, "Sarb"}, + { Script.Saurashtra, "Saur"}, + { Script.SignWriting, "Sgnw"}, + { Script.Shavian, "Shaw"}, + { Script.Sharada, "Shrd"}, + { Script.Siddham, "Sidd"}, + { Script.Khudawadi, "Sind"}, + { Script.Sinhala, "Sinh"}, + { Script.Sogdian, "Sogd"}, + { Script.OldSogdian, "Sogo"}, + { Script.SoraSompeng, "Sora"}, + { Script.Soyombo, "Soyo"}, + { Script.Sundanese, "Sund"}, + { Script.SylotiNagri, "Sylo"}, + { Script.Syriac, "Syrc"}, + { Script.Tagbanwa, "Tagb"}, + { Script.Takri, "Takr"}, + { Script.TaiLe, "Tale"}, + { Script.NewTaiLue, "Talu"}, + { Script.Tamil, "Taml"}, + { Script.Tangut, "Tang"}, + { Script.TaiViet, "Tavt"}, + { Script.Telugu, "Telu"}, + { Script.Tifinagh, "Tfng"}, + { Script.Tagalog, "Tglg"}, + { Script.Thaana, "Thaa"}, + { Script.Thai, "Thai"}, + { Script.Tibetan, "Tibt"}, + { Script.Tirhuta, "Tirh"}, + { Script.Ugaritic, "Ugar"}, + { Script.Vai, "Vaii"}, + { Script.WarangCiti, "Wara"}, + { Script.Wancho, "Wcho"}, + { Script.OldPersian, "Xpeo"}, + { Script.Cuneiform, "Xsux"}, + { Script.Yezidi, "Yezi"}, + { Script.Yi, "Yiii"}, + { Script.ZanabazarSquare, "Zanb"}, + }; + + public static string GetTag(Script script) + { + if(!s_scriptToTag.ContainsKey(script)) + { + return "Zzzz"; + } + return s_scriptToTag[script]; + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Script.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Script.cs index e9681d4c24..2593a77848 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Script.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Script.cs @@ -2,6 +2,9 @@ namespace Avalonia.Media.TextFormatting.Unicode { public enum Script { + Unknown, //Zzzz + Common, //Zyyy + Inherited, //Zinh Adlam, //Adlm CaucasianAlbanian, //Aghb Ahom, //Ahom @@ -25,10 +28,12 @@ namespace Avalonia.Media.TextFormatting.Unicode Carian, //Cari Cham, //Cham Cherokee, //Cher + Chorasmian, //Chrs Coptic, //Copt Cypriot, //Cprt Cyrillic, //Cyrl Devanagari, //Deva + DivesAkuru, //Diak Dogra, //Dogr Deseret, //Dsrt Duployan, //Dupl @@ -63,6 +68,7 @@ namespace Avalonia.Media.TextFormatting.Unicode Kharoshthi, //Khar Khmer, //Khmr Khojki, //Khoj + KhitanSmallScript, //Kits Kannada, //Knda Kaithi, //Kthi TaiTham, //Lana @@ -151,10 +157,8 @@ namespace Avalonia.Media.TextFormatting.Unicode Wancho, //Wcho OldPersian, //Xpeo Cuneiform, //Xsux + Yezidi, //Yezi Yi, //Yiii ZanabazarSquare, //Zanb - Inherited, //Zinh - Common, //Zyyy - Unknown, //Zzzz } } diff --git a/src/Avalonia.Visuals/Media/TextWrapping.cs b/src/Avalonia.Visuals/Media/TextWrapping.cs index 56df3670bd..d649bda23f 100644 --- a/src/Avalonia.Visuals/Media/TextWrapping.cs +++ b/src/Avalonia.Visuals/Media/TextWrapping.cs @@ -5,6 +5,13 @@ namespace Avalonia.Media /// public enum TextWrapping { + /// + /// Line-breaking occurs if the line overflows the available block width. + /// However, a line may overflow the block width if the line breaking algorithm + /// cannot determine a break opportunity, as in the case of a very long word. + /// + WrapWithOverflow, + /// /// Text should not wrap. /// @@ -15,4 +22,4 @@ namespace Avalonia.Media /// Wrap } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Media/Transform.cs b/src/Avalonia.Visuals/Media/Transform.cs index 70ef1eaaf4..7cf1b35ada 100644 --- a/src/Avalonia.Visuals/Media/Transform.cs +++ b/src/Avalonia.Visuals/Media/Transform.cs @@ -8,11 +8,12 @@ namespace Avalonia.Media /// /// Represents a transform on an . /// - public abstract class Transform : Animatable + public abstract class Transform : Animatable, IMutableTransform { static Transform() { - Animation.Animation.RegisterAnimator(prop => typeof(Transform).IsAssignableFrom(prop.OwnerType)); + Animation.Animation.RegisterAnimator(prop => + typeof(ITransform).IsAssignableFrom(prop.OwnerType)); } /// diff --git a/src/Avalonia.Visuals/Media/TransformConverter.cs b/src/Avalonia.Visuals/Media/TransformConverter.cs new file mode 100644 index 0000000000..e79c0b8b7b --- /dev/null +++ b/src/Avalonia.Visuals/Media/TransformConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using Avalonia.Media.Transformation; + +namespace Avalonia.Media +{ + /// + /// Creates an from a string representation. + /// + public class TransformConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + return TransformOperations.Parse((string)value); + } + } +} diff --git a/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs b/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs new file mode 100644 index 0000000000..1e80eabfc8 --- /dev/null +++ b/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs @@ -0,0 +1,40 @@ +namespace Avalonia.Media.Transformation +{ + internal static class InterpolationUtilities + { + public static double InterpolateScalars(double from, double to, double progress) + { + return from * (1d - progress) + to * progress; + } + + public static Vector InterpolateVectors(Vector from, Vector to, double progress) + { + var x = InterpolateScalars(from.X, to.X, progress); + var y = InterpolateScalars(from.Y, to.Y, progress); + + return new Vector(x, y); + } + + public static Matrix ComposeTransform(Matrix.Decomposed decomposed) + { + // According to https://www.w3.org/TR/css-transforms-1/#recomposing-to-a-2d-matrix + + return Matrix.CreateTranslation(decomposed.Translate) * + Matrix.CreateRotation(decomposed.Angle) * + Matrix.CreateSkew(decomposed.Skew.X, decomposed.Skew.Y) * + Matrix.CreateScale(decomposed.Scale); + } + + public static Matrix.Decomposed InterpolateDecomposedTransforms(ref Matrix.Decomposed from, ref Matrix.Decomposed to, double progres) + { + Matrix.Decomposed result = default; + + result.Translate = InterpolateVectors(from.Translate, to.Translate, progres); + result.Scale = InterpolateVectors(from.Scale, to.Scale, progres); + result.Skew = InterpolateVectors(from.Skew, to.Skew, progres); + result.Angle = InterpolateScalars(from.Angle, to.Angle, progres); + + return result; + } + } +} diff --git a/src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs b/src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs new file mode 100644 index 0000000000..36f5dd98f1 --- /dev/null +++ b/src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs @@ -0,0 +1,230 @@ +using System.Runtime.InteropServices; + +namespace Avalonia.Media.Transformation +{ + /// + /// Represents a single primitive transform (like translation, rotation, scale, etc.). + /// + public struct TransformOperation + { + public OperationType Type; + public Matrix Matrix; + public DataLayout Data; + + public enum OperationType + { + Translate, + Rotate, + Scale, + Skew, + Matrix, + Identity + } + + /// + /// Returns whether operation produces the identity matrix. + /// + public bool IsIdentity => Matrix.IsIdentity; + + /// + /// Bakes this operation to a transform matrix. + /// + public void Bake() + { + Matrix = Matrix.Identity; + + switch (Type) + { + case OperationType.Translate: + { + Matrix = Matrix.CreateTranslation(Data.Translate.X, Data.Translate.Y); + + break; + } + case OperationType.Rotate: + { + Matrix = Matrix.CreateRotation(Data.Rotate.Angle); + + break; + } + case OperationType.Scale: + { + Matrix = Matrix.CreateScale(Data.Scale.X, Data.Scale.Y); + + break; + } + case OperationType.Skew: + { + Matrix = Matrix.CreateSkew(Data.Skew.X, Data.Skew.Y); + + break; + } + } + } + + /// + /// Returns new identity transform operation. + /// + public static TransformOperation Identity => + new TransformOperation { Matrix = Matrix.Identity, Type = OperationType.Identity }; + + /// + /// Attempts to interpolate between two transform operations. + /// + /// Source operation. + /// Target operation. + /// Interpolation progress. + /// Interpolation result that will be filled in when operation was successful. + /// + /// Based upon https://www.w3.org/TR/css-transforms-1/#interpolation-of-transform-functions. + /// + public static bool TryInterpolate(TransformOperation? from, TransformOperation? to, double progress, + ref TransformOperation result) + { + bool fromIdentity = IsOperationIdentity(ref from); + bool toIdentity = IsOperationIdentity(ref to); + + if (fromIdentity && toIdentity) + { + return true; + } + + // ReSharper disable PossibleInvalidOperationException + TransformOperation fromValue = fromIdentity ? Identity : from.Value; + TransformOperation toValue = toIdentity ? Identity : to.Value; + // ReSharper restore PossibleInvalidOperationException + + var interpolationType = toIdentity ? fromValue.Type : toValue.Type; + + result.Type = interpolationType; + + switch (interpolationType) + { + case OperationType.Translate: + { + double fromX = fromIdentity ? 0 : fromValue.Data.Translate.X; + double fromY = fromIdentity ? 0 : fromValue.Data.Translate.Y; + + double toX = toIdentity ? 0 : toValue.Data.Translate.X; + double toY = toIdentity ? 0 : toValue.Data.Translate.Y; + + result.Data.Translate.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress); + result.Data.Translate.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress); + + result.Bake(); + + break; + } + case OperationType.Rotate: + { + double fromAngle = fromIdentity ? 0 : fromValue.Data.Rotate.Angle; + + double toAngle = toIdentity ? 0 : toValue.Data.Rotate.Angle; + + result.Data.Rotate.Angle = InterpolationUtilities.InterpolateScalars(fromAngle, toAngle, progress); + + result.Bake(); + + break; + } + case OperationType.Scale: + { + double fromX = fromIdentity ? 1 : fromValue.Data.Scale.X; + double fromY = fromIdentity ? 1 : fromValue.Data.Scale.Y; + + double toX = toIdentity ? 1 : toValue.Data.Scale.X; + double toY = toIdentity ? 1 : toValue.Data.Scale.Y; + + result.Data.Scale.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress); + result.Data.Scale.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress); + + result.Bake(); + + break; + } + case OperationType.Skew: + { + double fromX = fromIdentity ? 0 : fromValue.Data.Skew.X; + double fromY = fromIdentity ? 0 : fromValue.Data.Skew.Y; + + double toX = toIdentity ? 0 : toValue.Data.Skew.X; + double toY = toIdentity ? 0 : toValue.Data.Skew.Y; + + result.Data.Skew.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress); + result.Data.Skew.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress); + + result.Bake(); + + break; + } + case OperationType.Matrix: + { + var fromMatrix = fromIdentity ? Matrix.Identity : fromValue.Matrix; + var toMatrix = toIdentity ? Matrix.Identity : toValue.Matrix; + + if (!Matrix.TryDecomposeTransform(fromMatrix, out Matrix.Decomposed fromDecomposed) || + !Matrix.TryDecomposeTransform(toMatrix, out Matrix.Decomposed toDecomposed)) + { + return false; + } + + var interpolated = + InterpolationUtilities.InterpolateDecomposedTransforms( + ref fromDecomposed, ref toDecomposed, + progress); + + result.Matrix = InterpolationUtilities.ComposeTransform(interpolated); + + break; + } + case OperationType.Identity: + { + // Do nothing. + break; + } + } + + return true; + } + + private static bool IsOperationIdentity(ref TransformOperation? operation) + { + return !operation.HasValue || operation.Value.IsIdentity; + } + + [StructLayout(LayoutKind.Explicit)] + public struct DataLayout + { + [FieldOffset(0)] public SkewLayout Skew; + + [FieldOffset(0)] public ScaleLayout Scale; + + [FieldOffset(0)] public TranslateLayout Translate; + + [FieldOffset(0)] public RotateLayout Rotate; + + public struct SkewLayout + { + public double X; + public double Y; + } + + public struct ScaleLayout + { + public double X; + public double Y; + } + + public struct TranslateLayout + { + public double X; + public double Y; + } + + public struct RotateLayout + { + public double Angle; + } + } + } +} diff --git a/src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs b/src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs new file mode 100644 index 0000000000..334bb93562 --- /dev/null +++ b/src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.Media.Transformation +{ + /// + /// Contains a list of that represent primitive transforms that will be + /// applied in declared order. + /// + public sealed class TransformOperations : ITransform + { + public static TransformOperations Identity { get; } = new TransformOperations(new List()); + + private readonly List _operations; + + private TransformOperations(List operations) + { + _operations = operations ?? throw new ArgumentNullException(nameof(operations)); + + IsIdentity = CheckIsIdentity(); + + Value = ApplyTransforms(); + } + + /// + /// Returns whether all operations combined together produce the identity matrix. + /// + public bool IsIdentity { get; } + + public IReadOnlyList Operations => _operations; + + public Matrix Value { get; } + + public static TransformOperations Parse(string s) + { + return TransformParser.Parse(s); + } + + public static Builder CreateBuilder(int capacity) + { + return new Builder(capacity); + } + + public static TransformOperations Interpolate(TransformOperations from, TransformOperations to, double progress) + { + TransformOperations result = Identity; + + if (!TryInterpolate(from, to, progress, ref result)) + { + // If the matrices cannot be interpolated, fallback to discrete animation logic. + // See https://drafts.csswg.org/css-transforms/#matrix-interpolation + result = progress < 0.5 ? from : to; + } + + return result; + } + + private Matrix ApplyTransforms(int startOffset = 0) + { + Matrix matrix = Matrix.Identity; + + for (var i = startOffset; i < _operations.Count; i++) + { + TransformOperation operation = _operations[i]; + matrix *= operation.Matrix; + } + + return matrix; + } + + private bool CheckIsIdentity() + { + foreach (TransformOperation operation in _operations) + { + if (!operation.IsIdentity) + { + return false; + } + } + + return true; + } + + private static bool TryInterpolate(TransformOperations from, TransformOperations to, double progress, ref TransformOperations result) + { + bool fromIdentity = from.IsIdentity; + bool toIdentity = to.IsIdentity; + + if (fromIdentity && toIdentity) + { + return true; + } + + int matchingPrefixLength = ComputeMatchingPrefixLength(from, to); + int fromSize = fromIdentity ? 0 : from._operations.Count; + int toSize = toIdentity ? 0 : to._operations.Count; + int numOperations = Math.Max(fromSize, toSize); + + var builder = new Builder(matchingPrefixLength); + + for (int i = 0; i < matchingPrefixLength; i++) + { + TransformOperation interpolated = new TransformOperation + { + Type = TransformOperation.OperationType.Identity + }; + + if (!TransformOperation.TryInterpolate( + i >= fromSize ? default(TransformOperation?) : from._operations[i], + i >= toSize ? default(TransformOperation?) : to._operations[i], + progress, + ref interpolated)) + { + return false; + } + + builder.Append(interpolated); + } + + if (matchingPrefixLength < numOperations) + { + if (!ComputeDecomposedTransform(from, matchingPrefixLength, out Matrix.Decomposed fromDecomposed) || + !ComputeDecomposedTransform(to, matchingPrefixLength, out Matrix.Decomposed toDecomposed)) + { + return false; + } + + var transform = InterpolationUtilities.InterpolateDecomposedTransforms(ref fromDecomposed, ref toDecomposed, progress); + + builder.AppendMatrix(InterpolationUtilities.ComposeTransform(transform)); + } + + result = builder.Build(); + + return true; + } + + private static bool ComputeDecomposedTransform(TransformOperations operations, int startOffset, out Matrix.Decomposed decomposed) + { + Matrix transform = operations.ApplyTransforms(startOffset); + + if (!Matrix.TryDecomposeTransform(transform, out decomposed)) + { + return false; + } + + return true; + } + + private static int ComputeMatchingPrefixLength(TransformOperations from, TransformOperations to) + { + int numOperations = Math.Min(from._operations.Count, to._operations.Count); + + for (int i = 0; i < numOperations; i++) + { + if (from._operations[i].Type != to._operations[i].Type) + { + return i; + } + } + + // If the operations match to the length of the shorter list, then pad its + // length with the matching identity operations. + // https://drafts.csswg.org/css-transforms/#transform-function-lists + return Math.Max(from._operations.Count, to._operations.Count); + } + + public readonly struct Builder + { + private readonly List _operations; + + public Builder(int capacity) + { + _operations = new List(capacity); + } + + public void AppendTranslate(double x, double y) + { + var toAdd = new TransformOperation(); + + toAdd.Type = TransformOperation.OperationType.Translate; + toAdd.Data.Translate.X = x; + toAdd.Data.Translate.Y = y; + + toAdd.Bake(); + + _operations.Add(toAdd); + } + + public void AppendRotate(double angle) + { + var toAdd = new TransformOperation(); + + toAdd.Type = TransformOperation.OperationType.Rotate; + toAdd.Data.Rotate.Angle = angle; + + toAdd.Bake(); + + _operations.Add(toAdd); + } + + public void AppendScale(double x, double y) + { + var toAdd = new TransformOperation(); + + toAdd.Type = TransformOperation.OperationType.Scale; + toAdd.Data.Scale.X = x; + toAdd.Data.Scale.Y = y; + + toAdd.Bake(); + + _operations.Add(toAdd); + } + + public void AppendSkew(double x, double y) + { + var toAdd = new TransformOperation(); + + toAdd.Type = TransformOperation.OperationType.Skew; + toAdd.Data.Skew.X = x; + toAdd.Data.Skew.Y = y; + + toAdd.Bake(); + + _operations.Add(toAdd); + } + + public void AppendMatrix(Matrix matrix) + { + var toAdd = new TransformOperation(); + + toAdd.Type = TransformOperation.OperationType.Matrix; + toAdd.Matrix = matrix; + + _operations.Add(toAdd); + } + + public void AppendIdentity() + { + var toAdd = new TransformOperation(); + + toAdd.Type = TransformOperation.OperationType.Identity; + + _operations.Add(toAdd); + } + + public void Append(TransformOperation toAdd) + { + _operations.Add(toAdd); + } + + public TransformOperations Build() + { + return new TransformOperations(_operations); + } + } + } +} diff --git a/src/Avalonia.Visuals/Media/Transformation/TransformParser.cs b/src/Avalonia.Visuals/Media/Transformation/TransformParser.cs new file mode 100644 index 0000000000..85f4f5fec1 --- /dev/null +++ b/src/Avalonia.Visuals/Media/Transformation/TransformParser.cs @@ -0,0 +1,444 @@ +using System; +using System.Globalization; +using Avalonia.Utilities; + +namespace Avalonia.Media.Transformation +{ + public static class TransformParser + { + private static readonly (string, TransformFunction)[] s_functionMapping = + { + ("translate", TransformFunction.Translate), + ("translateX", TransformFunction.TranslateX), + ("translateY", TransformFunction.TranslateY), + ("scale", TransformFunction.Scale), + ("scaleX", TransformFunction.ScaleX), + ("scaleY", TransformFunction.ScaleY), + ("skew", TransformFunction.Skew), + ("skewX", TransformFunction.SkewX), + ("skewY", TransformFunction.SkewY), + ("rotate", TransformFunction.Rotate), + ("matrix", TransformFunction.Matrix) + }; + + private static readonly (string, Unit)[] s_unitMapping = + { + ("deg", Unit.Degree), + ("grad", Unit.Gradian), + ("rad", Unit.Radian), + ("turn", Unit.Turn), + ("px", Unit.Pixel) + }; + + public static TransformOperations Parse(string s) + { + void ThrowInvalidFormat() + { + throw new FormatException($"Invalid transform string: '{s}'."); + } + + if (string.IsNullOrEmpty(s)) + { + throw new ArgumentException(nameof(s)); + } + + var span = s.AsSpan().Trim(); + + if (span.Equals("none".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + return TransformOperations.Identity; + } + + var builder = TransformOperations.CreateBuilder(0); + + while (true) + { + var beginIndex = span.IndexOf('('); + var endIndex = span.IndexOf(')'); + + if (beginIndex == -1 || endIndex == -1) + { + ThrowInvalidFormat(); + } + + var namePart = span.Slice(0, beginIndex).Trim(); + + var function = ParseTransformFunction(in namePart); + + if (function == TransformFunction.Invalid) + { + ThrowInvalidFormat(); + } + + var valuePart = span.Slice(beginIndex + 1, endIndex - beginIndex - 1).Trim(); + + ParseFunction(in valuePart, function, in builder); + + span = span.Slice(endIndex + 1); + + if (span.IsWhiteSpace()) + { + break; + } + } + + return builder.Build(); + } + + private static void ParseFunction( + in ReadOnlySpan functionPart, + TransformFunction function, + in TransformOperations.Builder builder) + { + static UnitValue ParseValue(ReadOnlySpan part) + { + int unitIndex = -1; + + for (int i = 0; i < part.Length; i++) + { + char c = part[i]; + + if (char.IsDigit(c) || c == '-' || c == '.') + { + continue; + } + + unitIndex = i; + break; + } + + Unit unit = Unit.None; + + if (unitIndex != -1) + { + var unitPart = part.Slice(unitIndex, part.Length - unitIndex); + + unit = ParseUnit(unitPart); + + part = part.Slice(0, unitIndex); + } + + var value = double.Parse(part.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture); + + return new UnitValue(unit, value); + } + + static int ParseValuePair( + in ReadOnlySpan part, + ref UnitValue leftValue, + ref UnitValue rightValue) + { + var commaIndex = part.IndexOf(','); + + if (commaIndex != -1) + { + var leftPart = part.Slice(0, commaIndex).Trim(); + var rightPart = part.Slice(commaIndex + 1, part.Length - commaIndex - 1).Trim(); + + leftValue = ParseValue(leftPart); + rightValue = ParseValue(rightPart); + + return 2; + } + + leftValue = ParseValue(part); + + return 1; + } + + static int ParseCommaDelimitedValues(ReadOnlySpan part, in Span outValues) + { + int valueIndex = 0; + + while (true) + { + if (valueIndex >= outValues.Length) + { + throw new FormatException("Too many provided values."); + } + + var commaIndex = part.IndexOf(','); + + if (commaIndex == -1) + { + if (!part.IsWhiteSpace()) + { + outValues[valueIndex++] = ParseValue(part); + } + + break; + } + + var valuePart = part.Slice(0, commaIndex).Trim(); + + outValues[valueIndex++] = ParseValue(valuePart); + + part = part.Slice(commaIndex + 1, part.Length - commaIndex - 1); + } + + return valueIndex; + } + + switch (function) + { + case TransformFunction.Scale: + case TransformFunction.ScaleX: + case TransformFunction.ScaleY: + { + var scaleX = UnitValue.One; + var scaleY = UnitValue.One; + + int count = ParseValuePair(functionPart, ref scaleX, ref scaleY); + + if (count != 1 && (function == TransformFunction.ScaleX || function == TransformFunction.ScaleY)) + { + ThrowFormatInvalidValueCount(function, 1); + } + + VerifyZeroOrUnit(function, in scaleX, Unit.None); + VerifyZeroOrUnit(function, in scaleY, Unit.None); + + if (function == TransformFunction.ScaleY) + { + scaleY = scaleX; + scaleX = UnitValue.One; + } + else if (function == TransformFunction.Scale && count == 1) + { + scaleY = scaleX; + } + + builder.AppendScale(scaleX.Value, scaleY.Value); + + break; + } + case TransformFunction.Skew: + case TransformFunction.SkewX: + case TransformFunction.SkewY: + { + var skewX = UnitValue.Zero; + var skewY = UnitValue.Zero; + + int count = ParseValuePair(functionPart, ref skewX, ref skewY); + + if (count != 1 && (function == TransformFunction.SkewX || function == TransformFunction.SkewY)) + { + ThrowFormatInvalidValueCount(function, 1); + } + + VerifyZeroOrAngle(function, in skewX); + VerifyZeroOrAngle(function, in skewY); + + if (function == TransformFunction.SkewY) + { + skewY = skewX; + skewX = UnitValue.Zero; + } + + builder.AppendSkew(ToRadians(in skewX), ToRadians(in skewY)); + + break; + } + case TransformFunction.Rotate: + { + var angle = UnitValue.Zero; + UnitValue _ = default; + + int count = ParseValuePair(functionPart, ref angle, ref _); + + if (count != 1) + { + ThrowFormatInvalidValueCount(function, 1); + } + + VerifyZeroOrAngle(function, in angle); + + builder.AppendRotate(ToRadians(in angle)); + + break; + } + case TransformFunction.Translate: + case TransformFunction.TranslateX: + case TransformFunction.TranslateY: + { + var translateX = UnitValue.Zero; + var translateY = UnitValue.Zero; + + int count = ParseValuePair(functionPart, ref translateX, ref translateY); + + if (count != 1 && (function == TransformFunction.TranslateX || function == TransformFunction.TranslateY)) + { + ThrowFormatInvalidValueCount(function, 1); + } + + VerifyZeroOrUnit(function, in translateX, Unit.Pixel); + VerifyZeroOrUnit(function, in translateY, Unit.Pixel); + + if (function == TransformFunction.TranslateY) + { + translateY = translateX; + translateX = UnitValue.Zero; + } + + builder.AppendTranslate(translateX.Value, translateY.Value); + + break; + } + case TransformFunction.Matrix: + { + Span values = stackalloc UnitValue[6]; + + int count = ParseCommaDelimitedValues(functionPart, in values); + + if (count != 6) + { + ThrowFormatInvalidValueCount(function, 6); + } + + foreach (UnitValue value in values) + { + VerifyZeroOrUnit(function, value, Unit.None); + } + + var matrix = new Matrix( + values[0].Value, + values[1].Value, + values[2].Value, + values[3].Value, + values[4].Value, + values[5].Value); + + builder.AppendMatrix(matrix); + + break; + } + } + } + + private static void VerifyZeroOrUnit(TransformFunction function, in UnitValue value, Unit unit) + { + bool isZero = value.Unit == Unit.None && value.Value == 0d; + + if (!isZero && value.Unit != unit) + { + ThrowFormatInvalidValue(function, in value); + } + } + + private static void VerifyZeroOrAngle(TransformFunction function, in UnitValue value) + { + if (value.Value != 0d && !IsAngleUnit(value.Unit)) + { + ThrowFormatInvalidValue(function, in value); + } + } + + private static bool IsAngleUnit(Unit unit) + { + switch (unit) + { + case Unit.Radian: + case Unit.Gradian: + case Unit.Degree: + case Unit.Turn: + { + return true; + } + } + + return false; + } + + private static void ThrowFormatInvalidValue(TransformFunction function, in UnitValue value) + { + var unitString = value.Unit == Unit.None ? string.Empty : value.Unit.ToString(); + + throw new FormatException($"Invalid value {value.Value} {unitString} for {function}"); + } + + private static void ThrowFormatInvalidValueCount(TransformFunction function, int count) + { + throw new FormatException($"Invalid format. {function} expects {count} value(s)."); + } + + private static Unit ParseUnit(in ReadOnlySpan part) + { + foreach (var (name, unit) in s_unitMapping) + { + if (part.Equals(name.AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + return unit; + } + } + + throw new FormatException($"Invalid unit: {part.ToString()}"); + } + + private static TransformFunction ParseTransformFunction(in ReadOnlySpan part) + { + foreach (var (name, transformFunction) in s_functionMapping) + { + if (part.Equals(name.AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + return transformFunction; + } + } + + return TransformFunction.Invalid; + } + + private static double ToRadians(in UnitValue value) + { + return value.Unit switch + { + Unit.Radian => value.Value, + Unit.Gradian => MathUtilities.Grad2Rad(value.Value), + Unit.Degree => MathUtilities.Deg2Rad(value.Value), + Unit.Turn => MathUtilities.Turn2Rad(value.Value), + _ => value.Value + }; + } + + private enum Unit + { + None, + Pixel, + Radian, + Gradian, + Degree, + Turn + } + + private readonly struct UnitValue + { + public readonly Unit Unit; + public readonly double Value; + + public UnitValue(Unit unit, double value) + { + Unit = unit; + Value = value; + } + + public static UnitValue Zero => new UnitValue(Unit.None, 0); + + public static UnitValue One => new UnitValue(Unit.None, 1); + } + + private enum TransformFunction + { + Invalid, + Translate, + TranslateX, + TranslateY, + Scale, + ScaleX, + ScaleY, + Skew, + SkewX, + SkewY, + Rotate, + Matrix + } + } +} diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs index 7618598a3f..677e930804 100644 --- a/src/Avalonia.Visuals/Media/Typeface.cs +++ b/src/Avalonia.Visuals/Media/Typeface.cs @@ -16,11 +16,11 @@ namespace Avalonia.Media /// Initializes a new instance of the class. /// /// The font family. - /// The font weight. /// The font style. + /// The font weight. public Typeface([NotNull]FontFamily fontFamily, - FontWeight weight = FontWeight.Normal, - FontStyle style = FontStyle.Normal) + FontStyle style = FontStyle.Normal, + FontWeight weight = FontWeight.Normal) { if (weight <= 0) { @@ -39,9 +39,9 @@ namespace Avalonia.Media /// The font style. /// The font weight. public Typeface(string fontFamilyName, - FontWeight weight = FontWeight.Normal, - FontStyle style = FontStyle.Normal) - : this(new FontFamily(fontFamilyName), weight, style) + FontStyle style = FontStyle.Normal, + FontWeight weight = FontWeight.Normal) + : this(new FontFamily(fontFamilyName), style, weight) { } diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index 660d10c088..c87946b3ea 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -107,6 +107,12 @@ namespace Avalonia.Platform /// The clip rectangle. void PushClip(Rect clip); + /// + /// Pushes a clip rounded rectangle. + /// + /// The clip rounded rectangle + void PushClip(RoundedRect clip); + /// /// Pops the latest pushed clip rectangle. /// diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextWithAcrylicLikeSupport.cs b/src/Avalonia.Visuals/Platform/IDrawingContextWithAcrylicLikeSupport.cs new file mode 100644 index 0000000000..a3cf0298dd --- /dev/null +++ b/src/Avalonia.Visuals/Platform/IDrawingContextWithAcrylicLikeSupport.cs @@ -0,0 +1,9 @@ +using Avalonia.Media; + +namespace Avalonia.Platform +{ + public interface IDrawingContextWithAcrylicLikeSupport + { + void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect); + } +} diff --git a/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs b/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs index 4d770a6c6e..d915da2603 100644 --- a/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs +++ b/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs @@ -1,6 +1,6 @@ -using Avalonia.Media; -using Avalonia.Media.TextFormatting; -using Avalonia.Utility; +using System.Globalization; +using Avalonia.Media; +using Avalonia.Utilities; namespace Avalonia.Platform { @@ -13,8 +13,10 @@ namespace Avalonia.Platform /// Shapes the specified region within the text and returns a resulting glyph run. /// /// The text. - /// The text format. + /// The typeface. + /// The font rendering em size. + /// The culture. /// A shaped glyph run. - GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat); + GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize, CultureInfo culture); } } diff --git a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs index 6cd6442095..5d802c27b9 100644 --- a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs @@ -6,6 +6,7 @@ using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Imaging")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Transformation")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")] [assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")] diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs index 9de14cf01a..169a170824 100644 --- a/src/Avalonia.Visuals/Rect.cs +++ b/src/Avalonia.Visuals/Rect.cs @@ -211,6 +211,21 @@ namespace Avalonia rect.Width * scale.X, rect.Height * scale.Y); } + + /// + /// Multiplies a rectangle by a scale. + /// + /// The rectangle. + /// The scale. + /// The scaled rectangle. + public static Rect operator *(Rect rect, double scale) + { + return new Rect( + rect.X * scale, + rect.Y * scale, + rect.Width * scale, + rect.Height * scale); + } /// /// Divides a rectangle by a vector. diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 59dd369956..8c020fc073 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -287,7 +287,7 @@ namespace Avalonia.Rendering } catch (RenderTargetCorruptedException ex) { - Logger.TryGet(LogEventLevel.Information)?.Log("Renderer", this, "Render target was corrupted. Exception: {0}", ex); + Logger.TryGet(LogEventLevel.Information, LogArea.Animations)?.Log(this, "Render target was corrupted. Exception: {0}", ex); RenderTarget?.Dispose(); RenderTarget = null; } diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index c1c4b6bc99..9ea1b84311 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -78,7 +78,7 @@ namespace Avalonia.Rendering } catch (RenderTargetCorruptedException ex) { - Logger.TryGet(LogEventLevel.Information)?.Log("Renderer", this, "Render target was corrupted. Exception: {0}", ex); + Logger.TryGet(LogEventLevel.Information, LogArea.Animations)?.Log(this, "Render target was corrupted. Exception: {0}", ex); _renderTarget.Dispose(); _renderTarget = null; } @@ -289,7 +289,11 @@ namespace Avalonia.Rendering using (context.PushPostTransform(m)) using (context.PushOpacity(opacity)) - using (clipToBounds ? context.PushClip(bounds) : default(DrawingContext.PushedState)) + using (clipToBounds + ? visual is IVisualWithRoundRectClip roundClipVisual + ? context.PushClip(new RoundedRect(bounds, roundClipVisual.ClipToBoundsRadius)) + : context.PushClip(bounds) + : default(DrawingContext.PushedState)) using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default(DrawingContext.PushedState)) using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default(DrawingContext.PushedState)) using (context.PushTransformContainer()) diff --git a/src/Avalonia.Visuals/Rendering/RenderLoop.cs b/src/Avalonia.Visuals/Rendering/RenderLoop.cs index c2594658b9..789d028a3a 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLoop.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLoop.cs @@ -120,7 +120,7 @@ namespace Avalonia.Rendering } catch (Exception ex) { - Logger.TryGet(LogEventLevel.Error)?.Log(LogArea.Visual, this, "Exception in render update: {Error}", ex); + Logger.TryGet(LogEventLevel.Error, LogArea.Visual)?.Log(this, "Exception in render update: {Error}", ex); } } } @@ -136,7 +136,7 @@ namespace Avalonia.Rendering } catch (Exception ex) { - Logger.TryGet(LogEventLevel.Error)?.Log(LogArea.Visual, this, "Exception in render loop: {Error}", ex); + Logger.TryGet(LogEventLevel.Error, LogArea.Visual)?.Log(this, "Exception in render loop: {Error}", ex); } finally { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs index 34f042e334..ada04bfefd 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs @@ -16,6 +16,16 @@ namespace Avalonia.Rendering.SceneGraph { Clip = clip; } + + /// + /// Initializes a new instance of the class that represents a + /// clip push. + /// + /// The clip to push. + public ClipNode(RoundedRect clip) + { + Clip = clip; + } /// /// Initializes a new instance of the class that represents a @@ -31,7 +41,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Gets the clip to be pushed or null if the operation represents a pop. /// - public Rect? Clip { get; } + public RoundedRect? Clip { get; } /// public bool HitTest(Point p) => false; @@ -45,7 +55,7 @@ namespace Avalonia.Rendering.SceneGraph /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(Rect? clip) => Clip == clip; + public bool Equals(RoundedRect? clip) => Clip == clip; /// public void Render(IDrawingContextImpl context) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 6ad71ac111..4a364998fd 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -11,7 +11,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A drawing context which builds a scene graph. /// - internal class DeferredDrawingContextImpl : IDrawingContextImpl + internal class DeferredDrawingContextImpl : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport { private readonly ISceneBuilder _sceneBuilder; private VisualNode _node; @@ -163,7 +163,22 @@ namespace Avalonia.Rendering.SceneGraph ++_drawOperationindex; } } - + + /// + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, material, rect)) + { + Add(new ExperimentalAcrylicNode(Transform, material, rect)); + } + else + { + ++_drawOperationindex; + } + } + public void Custom(ICustomDrawOperation custom) { var next = NextDrawAs(); @@ -283,6 +298,21 @@ namespace Avalonia.Rendering.SceneGraph } } + /// + public void PushClip(RoundedRect clip) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(clip)) + { + Add(new ClipNode(clip)); + } + else + { + ++_drawOperationindex; + } + } + /// public void PushGeometryClip(IGeometryImpl clip) { @@ -368,7 +398,7 @@ namespace Avalonia.Rendering.SceneGraph { using (var refCounted = RefCountable.Create(node)) { - Add(refCounted); + Add(refCounted); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ExperimentalAcrylicNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ExperimentalAcrylicNode.cs new file mode 100644 index 0000000000..336d11e3fd --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ExperimentalAcrylicNode.cs @@ -0,0 +1,94 @@ +using System; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Platform; + +namespace Avalonia.Rendering.SceneGraph +{ + /// + /// A node in the scene graph which represents a rectangle draw. + /// + internal class ExperimentalAcrylicNode : DrawOperation + { + /// + /// Initializes a new instance of the class. + /// + /// The transform. + /// The rectangle to draw. + /// The box shadow parameters + /// Child scenes for drawing visual brushes. + public ExperimentalAcrylicNode( + Matrix transform, + IExperimentalAcrylicMaterial material, + RoundedRect rect) + : base(rect.Rect, transform) + { + Transform = transform; + Material = material?.ToImmutable(); + Rect = rect; + } + + /// + /// Gets the transform with which the node will be drawn. + /// + public Matrix Transform { get; } + + public IExperimentalAcrylicMaterial Material { get; } + + /// + /// Gets the rectangle to draw. + /// + public RoundedRect Rect { get; } + + /// + /// Determines if this draw operation equals another. + /// + /// The transform of the other draw operation. + /// The fill of the other draw operation. + /// The rectangle of the other draw operation. + /// True if the draw operations are the same, otherwise false. + /// + /// The properties of the other draw operation are passed in as arguments to prevent + /// allocation of a not-yet-constructed draw operation object. + /// + public bool Equals(Matrix transform, IExperimentalAcrylicMaterial material, RoundedRect rect) + { + return transform == Transform && + Equals(material, Material) && + rect.Equals(Rect); + } + + /// + public override void Render(IDrawingContextImpl context) + { + context.Transform = Transform; + + if(context is IDrawingContextWithAcrylicLikeSupport idc) + { + idc.DrawRectangle(Material, Rect); + } + else + { + context.DrawRectangle(new ImmutableSolidColorBrush(Material.FallbackColor), null, Rect); + } + } + + /// + public override bool HitTest(Point p) + { + // TODO: This doesn't respect CornerRadius yet. + if (Transform.HasInverse) + { + p *= Transform.Invert(); + + if (Material != null) + { + var rect = Rect.Rect; + return rect.Contains(p); + } + } + + return false; + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs index 8b77c37c1f..6d12b5bca4 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs @@ -26,6 +26,11 @@ namespace Avalonia.Rendering.SceneGraph /// Matrix Transform { get; } + /// + /// Gets the corner radius of visual. Contents are clipped to this radius. + /// + CornerRadius ClipToBoundsRadius { get; } + /// /// Gets the bounds of the node's geometry in global coordinates. /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index 5059a6d042..ec1a7753b1 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The fill brush. /// The stroke pen. /// The rectangle to draw. - /// The box shadow parameters + /// The box shadow parameters /// Child scenes for drawing visual brushes. public RectangleNode( Matrix transform, diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 263a491933..872f69c884 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -163,6 +163,10 @@ namespace Avalonia.Rendering.SceneGraph var visual = node.Visual; var opacity = visual.Opacity; var clipToBounds = visual.ClipToBounds; + var clipToBoundsRadius = visual is IVisualWithRoundRectClip roundRectClip ? + roundRectClip.ClipToBoundsRadius : + default; + var bounds = new Rect(visual.Bounds.Size); var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl; @@ -201,6 +205,7 @@ namespace Avalonia.Rendering.SceneGraph node.ClipBounds = clipBounds; node.ClipToBounds = clipToBounds; node.LayoutBounds = globalBounds; + node.ClipToBoundsRadius = clipToBoundsRadius; node.GeometryClip = visual.Clip?.PlatformImpl; node.Opacity = opacity; @@ -389,13 +394,8 @@ namespace Avalonia.Rendering.SceneGraph } } - private static bool ShouldStartLayer(IVisual visual) - { - var o = visual as IAvaloniaObject; - return visual.VisualChildren.Count > 0 && - o != null && - o.IsAnimating(Visual.OpacityProperty); - } + // HACK: Disabled layers because they're broken in current renderer. See #2244. + private static bool ShouldStartLayer(IVisual visual) => false; private static IGeometryImpl CreateLayerGeometryClip(VisualNode node) { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 6f566ff6d6..8fb6b2542a 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -46,6 +46,9 @@ namespace Avalonia.Rendering.SceneGraph /// public IVisualNode Parent { get; } + /// + public CornerRadius ClipToBoundsRadius { get; set; } + /// public Matrix Transform { get; set; } @@ -262,6 +265,7 @@ namespace Avalonia.Rendering.SceneGraph { Transform = Transform, ClipBounds = ClipBounds, + ClipToBoundsRadius = ClipToBoundsRadius, ClipToBounds = ClipToBounds, LayoutBounds = LayoutBounds, GeometryClip = GeometryClip, @@ -301,7 +305,10 @@ namespace Avalonia.Rendering.SceneGraph if (ClipToBounds) { context.Transform = Matrix.Identity; - context.PushClip(ClipBounds); + if (ClipToBoundsRadius.IsEmpty) + context.PushClip(ClipBounds); + else + context.PushClip(new RoundedRect(ClipBounds, ClipToBoundsRadius)); } context.Transform = Transform; diff --git a/src/Avalonia.Visuals/RoundedRect.cs b/src/Avalonia.Visuals/RoundedRect.cs index ad860240f2..3452bc1ff8 100644 --- a/src/Avalonia.Visuals/RoundedRect.cs +++ b/src/Avalonia.Visuals/RoundedRect.cs @@ -27,6 +27,10 @@ namespace Avalonia } } + public static bool operator ==(RoundedRect left, RoundedRect right) => left.Equals(right); + + public static bool operator !=(RoundedRect left, RoundedRect right) => !left.Equals(right); + public Rect Rect { get; } public Vector RadiiTopLeft { get; } public Vector RadiiTopRight { get; } @@ -74,6 +78,13 @@ namespace Avalonia } + public RoundedRect(in Rect bounds, in CornerRadius radius) : this(bounds, + radius.TopLeft, radius.TopRight, + radius.BottomRight, radius.BottomLeft) + { + + } + public static implicit operator RoundedRect(Rect r) => new RoundedRect(r); public bool IsRounded => RadiiTopLeft != default || RadiiTopRight != default || RadiiBottomRight != default || diff --git a/src/Avalonia.Visuals/Size.cs b/src/Avalonia.Visuals/Size.cs index 0383094a5b..f87b336b50 100644 --- a/src/Avalonia.Visuals/Size.cs +++ b/src/Avalonia.Visuals/Size.cs @@ -189,7 +189,7 @@ namespace Avalonia } /// - /// Returns a boolean indicating whether the size is equal to the other given size. + /// Returns a boolean indicating whether the size is equal to the other given size (bitwise). /// /// The other size to test equality against. /// True if this size is equal to other; False otherwise. @@ -201,6 +201,17 @@ namespace Avalonia // ReSharper enable CompareOfFloatsByEqualityOperator } + /// + /// Returns a boolean indicating whether the size is equal to the other given size (numerically). + /// + /// The other size to test equality against. + /// True if this size is equal to other; False otherwise. + public bool NearlyEquals(Size other) + { + return MathUtilities.AreClose(_width, other._width) && + MathUtilities.AreClose(_height, other._height); + } + /// /// Checks for equality between a size and an object. /// diff --git a/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs b/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs similarity index 98% rename from src/Avalonia.Visuals/Utility/ReadOnlySlice.cs rename to src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs index ff2b3b9363..5feaa88e26 100644 --- a/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs +++ b/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs @@ -2,9 +2,8 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; -using Avalonia.Utilities; -namespace Avalonia.Utility +namespace Avalonia.Utilities { /// /// ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region. @@ -47,7 +46,7 @@ namespace Avalonia.Utility public int Length { get; } /// - /// Gets a value that indicates whether this instance of is Empty. + /// Gets a value that indicates whether this instance of is Empty. /// public bool IsEmpty => Length == 0; diff --git a/src/Avalonia.Visuals/Utilities/ValueSpan.cs b/src/Avalonia.Visuals/Utilities/ValueSpan.cs new file mode 100644 index 0000000000..7a10d865ef --- /dev/null +++ b/src/Avalonia.Visuals/Utilities/ValueSpan.cs @@ -0,0 +1,30 @@ +namespace Avalonia.Utilities +{ + /// + /// Pairing of value and positions sharing that value. + /// + public readonly struct ValueSpan + { + public ValueSpan(int start, int length, T value) + { + Start = start; + Length = length; + Value = value; + } + + /// + /// Get's the start of the span. + /// + public int Start { get; } + + /// + /// Get's the length of the span. + /// + public int Length { get; } + + /// + /// Get's the value of the span. + /// + public T Value { get; } + } +} diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index 2b5d79173b..6059dc3971 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -2,7 +2,8 @@ using System; using System.Globalization; using Avalonia.Animation.Animators; using Avalonia.Utilities; -using JetBrains.Annotations; + +#nullable enable namespace Avalonia { @@ -17,20 +18,20 @@ namespace Avalonia } /// - /// The X vector. + /// The X component. /// private readonly double _x; /// - /// The Y vector. + /// The Y component. /// private readonly double _y; /// /// Initializes a new instance of the structure. /// - /// The X vector. - /// The Y vector. + /// The X component. + /// The Y component. public Vector(double x, double y) { _x = x; @@ -38,12 +39,12 @@ namespace Avalonia } /// - /// Gets the X vector. + /// Gets the X component. /// public double X => _x; /// - /// Gets the Y vector. + /// Gets the Y component. /// public double Y => _y; @@ -57,18 +58,18 @@ namespace Avalonia } /// - /// Calculates the dot product of two vectors + /// Calculates the dot product of two vectors. /// - /// First vector - /// Second vector - /// The dot product + /// First vector. + /// Second vector. + /// The dot product. public static double operator *(Vector a, Vector b) => Dot(a, b); /// /// Scales a vector. /// - /// The vector + /// The vector. /// The scaling factor. /// The scaled vector. public static Vector operator *(Vector vector, double scale) @@ -77,7 +78,7 @@ namespace Avalonia /// /// Scales a vector. /// - /// The vector + /// The vector. /// The divisor. /// The scaled vector. public static Vector operator /(Vector vector, double scale) @@ -100,12 +101,12 @@ namespace Avalonia } /// - /// Length of the vector + /// Length of the vector. /// public double Length => Math.Sqrt(SquaredLength); /// - /// Squared Length of the vector + /// Squared Length of the vector. /// public double SquaredLength => _x * _x + _y * _y; @@ -154,9 +155,8 @@ namespace Avalonia /// True if vectors are nearly equal. public bool NearlyEquals(Vector other) { - const float tolerance = float.Epsilon; - - return Math.Abs(_x - other._x) < tolerance && Math.Abs(_y - other._y) < tolerance; + return MathUtilities.AreClose(_x, other._x) && + MathUtilities.AreClose(_y, other._y); } public override bool Equals(object obj) => obj is Vector other && Equals(other); @@ -189,9 +189,9 @@ namespace Avalonia } /// - /// Returns a new vector with the specified X coordinate. + /// Returns a new vector with the specified X component. /// - /// The X coordinate. + /// The X component. /// The new vector. public Vector WithX(double x) { @@ -199,9 +199,9 @@ namespace Avalonia } /// - /// Returns a new vector with the specified Y coordinate. + /// Returns a new vector with the specified Y component. /// - /// The Y coordinate. + /// The Y component. /// The new vector. public Vector WithY(double y) { @@ -311,25 +311,25 @@ namespace Avalonia => new Vector(-vector._x, -vector._y); /// - /// Returnes the vector (0.0, 0.0) + /// Returns the vector (0.0, 0.0). /// public static Vector Zero => new Vector(0, 0); /// - /// Returnes the vector (1.0, 1.0) + /// Returns the vector (1.0, 1.0). /// public static Vector One => new Vector(1, 1); /// - /// Returnes the vector (1.0, 0.0) + /// Returns the vector (1.0, 0.0). /// public static Vector UnitX => new Vector(1, 0); /// - /// Returnes the vector (0.0, 1.0) + /// Returns the vector (0.0, 1.0). /// public static Vector UnitY => new Vector(0, 1); diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index bb9a4cf208..cd6eb6aac7 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -68,8 +68,8 @@ namespace Avalonia /// /// Defines the property. /// - public static readonly StyledProperty RenderTransformProperty = - AvaloniaProperty.Register(nameof(RenderTransform)); + public static readonly StyledProperty RenderTransformProperty = + AvaloniaProperty.Register(nameof(RenderTransform)); /// /// Defines the property. @@ -219,7 +219,7 @@ namespace Avalonia /// /// Gets the render transform of the control. /// - public Transform RenderTransform + public ITransform RenderTransform { get { return GetValue(RenderTransformProperty); } set { SetValue(RenderTransformProperty, value); } @@ -387,13 +387,13 @@ namespace Avalonia /// The event args. protected virtual void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) { - Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Visual, this, "Attached to visual tree"); + Logger.TryGet(LogEventLevel.Verbose, LogArea.Visual)?.Log(this, "Attached to visual tree"); _visualRoot = e.Root; - if (RenderTransform != null) + if (RenderTransform is IMutableTransform mutableTransform) { - RenderTransform.Changed += RenderTransformChanged; + mutableTransform.Changed += RenderTransformChanged; } EnableTransitions(); @@ -424,13 +424,13 @@ namespace Avalonia /// The event args. protected virtual void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) { - Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Visual, this, "Detached from visual tree"); + Logger.TryGet(LogEventLevel.Verbose, LogArea.Visual)?.Log(this, "Detached from visual tree"); _visualRoot = null; - if (RenderTransform != null) + if (RenderTransform is IMutableTransform mutableTransform) { - RenderTransform.Changed -= RenderTransformChanged; + mutableTransform.Changed -= RenderTransformChanged; } DisableTransitions(); @@ -501,8 +501,7 @@ namespace Avalonia return; } - Logger.TryGet(LogEventLevel.Warning)?.Log( - LogArea.Binding, + Logger.TryGet(LogEventLevel.Warning, LogArea.Binding)?.Log( this, "Error in binding to {Target}.{Property}: {Message}", this, diff --git a/src/Avalonia.Visuals/VisualTree/IVisual.cs b/src/Avalonia.Visuals/VisualTree/IVisual.cs index 6f905cc269..50787655d9 100644 --- a/src/Avalonia.Visuals/VisualTree/IVisual.cs +++ b/src/Avalonia.Visuals/VisualTree/IVisual.cs @@ -76,7 +76,7 @@ namespace Avalonia.VisualTree /// /// Gets or sets the render transform of the control. /// - Transform RenderTransform { get; set; } + ITransform RenderTransform { get; set; } /// /// Gets or sets the render transform origin of the control. diff --git a/src/Avalonia.Visuals/VisualTree/IVisualWithRoundRectClip.cs b/src/Avalonia.Visuals/VisualTree/IVisualWithRoundRectClip.cs new file mode 100644 index 0000000000..9ace215d03 --- /dev/null +++ b/src/Avalonia.Visuals/VisualTree/IVisualWithRoundRectClip.cs @@ -0,0 +1,15 @@ +using System; + +namespace Avalonia.VisualTree +{ + [Obsolete("Internal API, will be removed in future versions, you've been warned")] + public interface IVisualWithRoundRectClip + { + /// + /// Gets a value indicating the corner radius of control's clip bounds + /// + [Obsolete("Internal API, will be removed in future versions, you've been warned")] + CornerRadius ClipToBoundsRadius { get; } + + } +} diff --git a/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs b/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs index 52fd6abb8b..3aa0392496 100644 --- a/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs +++ b/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs @@ -76,5 +76,7 @@ namespace Avalonia.VisualTree { return !left.Equals(right); } + + public override string ToString() => $"Bounds: {Bounds} Clip: {Clip} Transform {Transform}"; } } diff --git a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs index 046036fd68..e3250e6733 100644 --- a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs +++ b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs @@ -37,7 +37,7 @@ namespace Avalonia.X11.Glx } catch(Exception e) { - Logger.TryGet(LogEventLevel.Error)?.Log("OpenGL", null, "Unable to initialize GLX-based rendering: {0}", e); + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(null, "Unable to initialize GLX-based rendering: {0}", e); return null; } } diff --git a/src/Avalonia.X11/NativeDialogs/Gtk.cs b/src/Avalonia.X11/NativeDialogs/Gtk.cs index 9a12479fac..82cf3c934f 100644 --- a/src/Avalonia.X11/NativeDialogs/Gtk.cs +++ b/src/Avalonia.X11/NativeDialogs/Gtk.cs @@ -207,6 +207,9 @@ namespace Avalonia.X11.NativeDialogs [DllImport(GtkName)] public static extern void gtk_widget_realize(IntPtr gtkWidget); + + [DllImport(GtkName)] + public static extern void gtk_widget_destroy(IntPtr gtkWidget); [DllImport(GtkName)] public static extern IntPtr gtk_widget_get_window(IntPtr gtkWidget); @@ -219,6 +222,13 @@ namespace Avalonia.X11.NativeDialogs [DllImport(GdkName)] static extern IntPtr gdk_x11_window_foreign_new_for_display(IntPtr display, IntPtr xid); + + [DllImport(GdkName)] + public static extern IntPtr gdk_x11_window_get_xid(IntPtr window); + + + [DllImport(GtkName)] + public static extern IntPtr gtk_container_add(IntPtr container, IntPtr widget); [DllImport(GdkName)] static extern IntPtr gdk_set_allowed_backends(Utf8Buffer backends); diff --git a/src/Avalonia.X11/X11Atoms.cs b/src/Avalonia.X11/X11Atoms.cs index dd16fae386..32f1822e51 100644 --- a/src/Avalonia.X11/X11Atoms.cs +++ b/src/Avalonia.X11/X11Atoms.cs @@ -22,6 +22,7 @@ // using System; +using System.Collections.Generic; using System.Linq; using static Avalonia.X11.XLib; // ReSharper disable FieldCanBeMadeReadOnly.Global @@ -40,8 +41,9 @@ namespace Avalonia.X11 internal class X11Atoms { + private readonly IntPtr _display; -// Our atoms + // Our atoms public readonly IntPtr AnyPropertyType = (IntPtr)0; public readonly IntPtr XA_PRIMARY = (IntPtr)1; public readonly IntPtr XA_SECONDARY = (IntPtr)2; @@ -187,10 +189,13 @@ namespace Avalonia.X11 public readonly IntPtr ATOM_PAIR; public readonly IntPtr MANAGER; public readonly IntPtr _KDE_NET_WM_BLUR_BEHIND_REGION; + public readonly IntPtr INCR; - + private readonly Dictionary _namesToAtoms = new Dictionary(); + private readonly Dictionary _atomsToNames = new Dictionary(); public X11Atoms(IntPtr display) { + _display = display; // make sure this array stays in sync with the statements below @@ -204,7 +209,33 @@ namespace Avalonia.X11 XInternAtoms(display, atomNames, atomNames.Length, true, atoms); for (var c = 0; c < fields.Length; c++) + { + _namesToAtoms[fields[c].Name] = atoms[c]; + _atomsToNames[atoms[c]] = fields[c].Name; fields[c].SetValue(this, atoms[c]); + } + } + + public IntPtr GetAtom(string name) + { + if (_namesToAtoms.TryGetValue(name, out var rv)) + return rv; + var atom = XInternAtom(_display, name, false); + _namesToAtoms[name] = atom; + _atomsToNames[atom] = name; + return atom; + } + + public string GetAtomName(IntPtr atom) + { + if (_atomsToNames.TryGetValue(atom, out var rv)) + return rv; + var name = XLib.GetAtomName(_display, atom); + if (name == null) + return null; + _atomsToNames[atom] = name; + _namesToAtoms[name] = atom; + return name; } } } diff --git a/src/Avalonia.X11/X11Clipboard.cs b/src/Avalonia.X11/X11Clipboard.cs index a431ffcc1a..7023bb3ef3 100644 --- a/src/Avalonia.X11/X11Clipboard.cs +++ b/src/Avalonia.X11/X11Clipboard.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; +using Avalonia.Input; using Avalonia.Input.Platform; using static Avalonia.X11.XLib; namespace Avalonia.X11 @@ -10,12 +12,14 @@ namespace Avalonia.X11 class X11Clipboard : IClipboard { private readonly X11Info _x11; - private string _storedString; + private IDataObject _storedDataObject; private IntPtr _handle; private TaskCompletionSource _requestedFormatsTcs; - private TaskCompletionSource _requestedTextTcs; + private TaskCompletionSource _requestedDataTcs; private readonly IntPtr[] _textAtoms; private readonly IntPtr _avaloniaSaveTargetsAtom; + private readonly Dictionary _formatAtoms = new Dictionary(); + private readonly Dictionary _atomFormats = new Dictionary(); public X11Clipboard(AvaloniaX11Platform platform) { @@ -31,6 +35,11 @@ namespace Avalonia.X11 }.Where(a => a != IntPtr.Zero).ToArray(); } + bool IsStringAtom(IntPtr atom) + { + return _textAtoms.Contains(atom); + } + Encoding GetStringEncoding(IntPtr atom) { return (atom == _x11.Atoms.XA_STRING @@ -75,21 +84,31 @@ namespace Avalonia.X11 Encoding textEnc; if (target == _x11.Atoms.TARGETS) { - var atoms = _textAtoms; - atoms = atoms.Concat(new[] {_x11.Atoms.TARGETS, _x11.Atoms.MULTIPLE}) - .ToArray(); + var atoms = new HashSet { _x11.Atoms.TARGETS, _x11.Atoms.MULTIPLE }; + foreach (var fmt in _storedDataObject.GetDataFormats()) + { + if (fmt == DataFormats.Text) + foreach (var ta in _textAtoms) + atoms.Add(ta); + else + atoms.Add(_x11.Atoms.GetAtom(fmt)); + } + XChangeProperty(_x11.Display, window, property, - _x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, atoms, atoms.Length); + _x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, atoms.ToArray(), atoms.Count); return property; } else if(target == _x11.Atoms.SAVE_TARGETS && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero) { return property; } - else if ((textEnc = GetStringEncoding(target)) != null) + else if ((textEnc = GetStringEncoding(target)) != null + && _storedDataObject?.Contains(DataFormats.Text) == true) { - - var data = textEnc.GetBytes(_storedString ?? ""); + var text = _storedDataObject.GetText(); + if(text == null) + return IntPtr.Zero; + var data = textEnc.GetBytes(text); fixed (void* pdata = data) XChangeProperty(_x11.Display, window, property, target, 8, PropertyMode.Replace, @@ -121,6 +140,23 @@ namespace Avalonia.X11 return property; } + else if(_storedDataObject?.Contains(_x11.Atoms.GetAtomName(target)) == true) + { + var objValue = _storedDataObject.Get(_x11.Atoms.GetAtomName(target)); + + if(!(objValue is byte[] bytes)) + { + if (objValue is string s) + bytes = Encoding.UTF8.GetBytes(s); + else + return IntPtr.Zero; + } + + XChangeProperty(_x11.Display, window, property, target, 8, + PropertyMode.Replace, + bytes, bytes.Length); + return property; + } else return IntPtr.Zero; } @@ -131,15 +167,15 @@ namespace Avalonia.X11 if (sel.property == IntPtr.Zero) { _requestedFormatsTcs?.TrySetResult(null); - _requestedTextTcs?.TrySetResult(null); + _requestedDataTcs?.TrySetResult(null); } XGetWindowProperty(_x11.Display, _handle, sel.property, IntPtr.Zero, new IntPtr (0x7fffffff), true, (IntPtr)Atom.AnyPropertyType, - out var actualAtom, out var actualFormat, out var nitems, out var bytes_after, out var prop); + out var actualTypeAtom, out var actualFormat, out var nitems, out var bytes_after, out var prop); Encoding textEnc = null; if (nitems == IntPtr.Zero) { _requestedFormatsTcs?.TrySetResult(null); - _requestedTextTcs?.TrySetResult(null); + _requestedDataTcs?.TrySetResult(null); } else { @@ -154,10 +190,24 @@ namespace Avalonia.X11 _requestedFormatsTcs?.TrySetResult(formats); } } - else if ((textEnc = GetStringEncoding(sel.property)) != null) + else if ((textEnc = GetStringEncoding(actualTypeAtom)) != null) { var text = textEnc.GetString((byte*)prop.ToPointer(), nitems.ToInt32()); - _requestedTextTcs?.TrySetResult(text); + _requestedDataTcs?.TrySetResult(text); + } + else + { + if (actualTypeAtom == _x11.Atoms.INCR) + { + // TODO: Actually implement that monstrosity + _requestedDataTcs.TrySetResult(null); + } + else + { + var data = new byte[(int)nitems * (actualFormat / 8)]; + Marshal.Copy(prop, data, 0, data.Length); + _requestedDataTcs?.TrySetResult(data); + } } } @@ -174,17 +224,19 @@ namespace Avalonia.X11 return _requestedFormatsTcs.Task; } - Task SendTextRequest(IntPtr format) + Task SendDataRequest(IntPtr format) { - if (_requestedTextTcs == null || _requestedFormatsTcs.Task.IsCompleted) - _requestedTextTcs = new TaskCompletionSource(); + if (_requestedDataTcs == null || _requestedFormatsTcs.Task.IsCompleted) + _requestedDataTcs = new TaskCompletionSource(); XConvertSelection(_x11.Display, _x11.Atoms.CLIPBOARD, format, format, _handle, IntPtr.Zero); - return _requestedTextTcs.Task; + return _requestedDataTcs.Task; } + + bool HasOwner => XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD) != IntPtr.Zero; public async Task GetTextAsync() { - if (XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD) == IntPtr.Zero) + if (!HasOwner) return null; var res = await SendFormatRequest(); var target = _x11.Atoms.UTF8_STRING; @@ -199,7 +251,7 @@ namespace Avalonia.X11 } } - return await SendTextRequest(target); + return (string)await SendDataRequest(target); } void StoreAtomsInClipboardManager(IntPtr[] atoms) @@ -220,15 +272,52 @@ namespace Avalonia.X11 public Task SetTextAsync(string text) { - _storedString = text; + var data = new DataObject(); + data.Set(DataFormats.Text, text); + return SetDataObjectAsync(data); + } + + public Task ClearAsync() + { + return SetTextAsync(null); + } + + public Task SetDataObjectAsync(IDataObject data) + { + _storedDataObject = data; XSetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD, _handle, IntPtr.Zero); StoreAtomsInClipboardManager(_textAtoms); return Task.CompletedTask; } - public Task ClearAsync() + public async Task GetFormatsAsync() { - return SetTextAsync(null); + if (!HasOwner) + return null; + var res = await SendFormatRequest(); + if (res == null) + return null; + var rv = new List(); + if (_textAtoms.Any(res.Contains)) + rv.Add(DataFormats.Text); + foreach (var t in res) + rv.Add(_x11.Atoms.GetAtomName(t)); + return rv.ToArray(); + } + + public async Task GetDataAsync(string format) + { + if (!HasOwner) + return null; + if (format == DataFormats.Text) + return await GetTextAsync(); + + var formatAtom = _x11.Atoms.GetAtom(format); + var res = await SendFormatRequest(); + if (!res.Contains(formatAtom)) + return null; + + return await SendDataRequest(formatAtom); } } } diff --git a/src/Avalonia.X11/X11NativeControlHost.cs b/src/Avalonia.X11/X11NativeControlHost.cs new file mode 100644 index 0000000000..23fb27f72b --- /dev/null +++ b/src/Avalonia.X11/X11NativeControlHost.cs @@ -0,0 +1,198 @@ +using System; +using Avalonia.Controls.Platform; +using Avalonia.Platform; +using Avalonia.VisualTree; +using static Avalonia.X11.XLib; +namespace Avalonia.X11 +{ + // TODO: Actually implement XEmbed instead of simply using XReparentWindow + class X11NativeControlHost : INativeControlHostImpl + { + private readonly AvaloniaX11Platform _platform; + public X11Window Window { get; } + + public X11NativeControlHost(AvaloniaX11Platform platform, X11Window window) + { + _platform = platform; + Window = window; + } + + public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent) + { + var ch = new DumbWindow(_platform.Info); + XSync(_platform.Display, false); + return ch; + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func create) + { + var holder = new DumbWindow(_platform.Info, Window.Handle.Handle); + Attachment attachment = null; + try + { + var child = create(holder); + // ReSharper disable once UseObjectOrCollectionInitializer + // It has to be assigned to the variable before property setter is called so we dispose it on exception + attachment = new Attachment(_platform.Display, holder, _platform.OrphanedWindow, child); + attachment.AttachedTo = this; + return attachment; + } + catch + { + attachment?.Dispose(); + holder?.Destroy(); + throw; + } + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle) + { + if (!IsCompatibleWith(handle)) + throw new ArgumentException(handle.HandleDescriptor + " is not compatible with the current window", + nameof(handle)); + var attachment = new Attachment(_platform.Display, new DumbWindow(_platform.Info, Window.Handle.Handle), + _platform.OrphanedWindow, handle) { AttachedTo = this }; + return attachment; + } + + public bool IsCompatibleWith(IPlatformHandle handle) => handle.HandleDescriptor == "XID"; + + class DumbWindow : INativeControlHostDestroyableControlHandle + { + private readonly IntPtr _display; + + public DumbWindow(X11Info x11, IntPtr? parent = null) + { + _display = x11.Display; + /*Handle = XCreateSimpleWindow(x11.Display, XLib.XDefaultRootWindow(_display), + 0, 0, 1, 1, 0, IntPtr.Zero, IntPtr.Zero);*/ + var attr = new XSetWindowAttributes + { + backing_store = 1, + bit_gravity = Gravity.NorthWestGravity, + win_gravity = Gravity.NorthWestGravity, + + }; + + parent = parent ?? XDefaultRootWindow(x11.Display); + + Handle = XCreateWindow(_display, parent.Value, 0, 0, + 1,1, 0, 0, + (int)CreateWindowArgs.InputOutput, + IntPtr.Zero, + new UIntPtr((uint)(SetWindowValuemask.BorderPixel | SetWindowValuemask.BitGravity | + SetWindowValuemask.BackPixel | + SetWindowValuemask.WinGravity | SetWindowValuemask.BackingStore)), ref attr); + } + + public IntPtr Handle { get; private set; } + public string HandleDescriptor => "XID"; + public void Destroy() + { + if (Handle != IntPtr.Zero) + { + XDestroyWindow(_display, Handle); + Handle = IntPtr.Zero; + } + } + } + + class Attachment : INativeControlHostControlTopLevelAttachment + { + private readonly IntPtr _display; + private readonly IntPtr _orphanedWindow; + private DumbWindow _holder; + private IPlatformHandle _child; + private X11NativeControlHost _attachedTo; + private bool _mapped; + + public Attachment(IntPtr display, DumbWindow holder, IntPtr orphanedWindow, IPlatformHandle child) + { + _display = display; + _orphanedWindow = orphanedWindow; + _holder = holder; + _child = child; + XReparentWindow(_display, child.Handle, holder.Handle, 0, 0); + XMapWindow(_display, child.Handle); + } + + public void Dispose() + { + if (_child != null) + { + XReparentWindow(_display, _child.Handle, _orphanedWindow, 0, 0); + _child = null; + } + + _holder?.Destroy(); + _holder = null; + _attachedTo = null; + } + + void CheckDisposed() + { + if (_child == null) + throw new ObjectDisposedException("X11 INativeControlHostControlTopLevelAttachment"); + } + + public INativeControlHostImpl AttachedTo + { + get => _attachedTo; + set + { + CheckDisposed(); + _attachedTo = (X11NativeControlHost)value; + if (value == null) + { + _mapped = false; + XUnmapWindow(_display, _holder.Handle); + XReparentWindow(_display, _holder.Handle, _orphanedWindow, 0, 0); + } + else + { + XReparentWindow(_display, _holder.Handle, _attachedTo.Window.Handle.Handle, 0, 0); + } + } + } + + public bool IsCompatibleWith(INativeControlHostImpl host) => host is X11NativeControlHost; + + public void HideWithSize(Size size) + { + if(_attachedTo == null || _child == null) + return; + if (_mapped) + { + _mapped = false; + XUnmapWindow(_display, _holder.Handle); + } + + size *= _attachedTo.Window.Scaling; + XResizeWindow(_display, _child.Handle, + Math.Max(1, (int)size.Width), Math.Max(1, (int)size.Height)); + } + + + + public void ShowInBounds(Rect bounds) + { + CheckDisposed(); + if (_attachedTo == null) + throw new InvalidOperationException("The control isn't currently attached to a toplevel"); + bounds *= _attachedTo.Window.Scaling; + + var pixelRect = new PixelRect((int)bounds.X, (int)bounds.Y, Math.Max(1, (int)bounds.Width), + Math.Max(1, (int)bounds.Height)); + XMoveResizeWindow(_display, _child.Handle, 0, 0, pixelRect.Width, pixelRect.Height); + XMoveResizeWindow(_display, _holder.Handle, pixelRect.X, pixelRect.Y, pixelRect.Width, + pixelRect.Height); + if (!_mapped) + { + XMapWindow(_display, _holder.Handle); + XRaiseWindow(_display, _holder.Handle); + _mapped = true; + } + } + } + } +} diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index c2b3926ffd..9e9c4fd30e 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -26,6 +26,7 @@ namespace Avalonia.X11 public IX11Screens X11Screens { get; private set; } public IScreenImpl Screens { get; private set; } public X11PlatformOptions Options { get; private set; } + public IntPtr OrphanedWindow { get; private set; } public X11Globals Globals { get; private set; } public void Initialize(X11PlatformOptions options) { @@ -33,6 +34,8 @@ namespace Avalonia.X11 XInitThreads(); Display = XOpenDisplay(IntPtr.Zero); DeferredDisplay = XOpenDisplay(IntPtr.Zero); + OrphanedWindow = XCreateSimpleWindow(Display, XDefaultRootWindow(Display), 0, 0, 1, 1, 0, IntPtr.Zero, + IntPtr.Zero); if (Display == IntPtr.Zero) throw new Exception("XOpenDisplay failed"); XError.Init(); @@ -82,7 +85,7 @@ namespace Avalonia.X11 return new X11Window(this, null); } - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public IWindowImpl CreateEmbeddableWindow() { throw new NotSupportedException(); } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 4899f0efc0..2a13999e8d 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -21,7 +21,9 @@ using static Avalonia.X11.XLib; // ReSharper disable StringLiteralTypo namespace Avalonia.X11 { - unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client, ITopLevelImplWithNativeMenuExporter + unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client, + ITopLevelImplWithNativeMenuExporter, + ITopLevelImplWithNativeControlHost { private readonly AvaloniaX11Platform _platform; private readonly IWindowImpl _popupParent; @@ -185,6 +187,7 @@ namespace Avalonia.X11 PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize)); if (platform.Options.UseDBusMenu) NativeMenuExporter = DBusMenuExporter.TryCreate(_handle); + NativeControlHost = new X11NativeControlHost(_platform, this); } class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo @@ -309,10 +312,19 @@ namespace Avalonia.X11 { get => _transparencyHelper.TransparencyLevelChanged; set => _transparencyHelper.TransparencyLevelChanged = value; - } + } + + public Action ExtendClientAreaToDecorationsChanged { get; set; } + + public Thickness ExtendedMargins { get; } = new Thickness(); + + public Thickness OffScreenMargin { get; } = new Thickness(); + + public bool IsClientAreaExtendedToDecorations { get; } public Action Closed { get; set; } public Action PositionChanged { get; set; } + public Action LostFocus { get; set; } public IRenderer CreateRenderer(IRenderRoot root) { @@ -922,7 +934,7 @@ namespace Avalonia.X11 public IScreenImpl Screen => _platform.Screens; - public Size MaxClientSize => _platform.X11Screens.Screens.Select(s => s.Bounds.Size.ToSize(s.PixelDensity)) + public Size MaxAutoSizeHint => _platform.X11Screens.Screens.Select(s => s.Bounds.Size.ToSize(s.PixelDensity)) .OrderByDescending(x => x.Width + x.Height).FirstOrDefault(); @@ -1035,6 +1047,18 @@ namespace Avalonia.X11 _disabled = !enable; } + public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint) + { + } + + public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) + { + } + + public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) + { + } + public Action GotInputWhenDisabled { get; set; } public void SetIcon(IWindowIconImpl icon) @@ -1094,10 +1118,18 @@ namespace Avalonia.X11 public IPopupPositioner PopupPositioner { get; } public ITopLevelNativeMenuExporter NativeMenuExporter { get; } - + public INativeControlHostImpl NativeControlHost { get; } public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) => _transparencyHelper.SetTransparencyRequest(transparencyLevel); + public void SetWindowManagerAddShadowHint(bool enabled) + { + } + public WindowTransparencyLevel TransparencyLevel => _transparencyHelper.CurrentLevel; + + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0.8); + + public bool NeedsManagedDecorations => false; } } diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs index 0734532d92..742973e0da 100644 --- a/src/Avalonia.X11/XI2Manager.cs +++ b/src/Avalonia.X11/XI2Manager.cs @@ -97,7 +97,7 @@ namespace Avalonia.X11 { _platform = platform; _x11 = platform.Info; - _multitouch = platform.Options?.EnableMultiTouch ?? false; + _multitouch = platform.Options?.EnableMultiTouch ?? true; var devices =(XIDeviceInfo*) XIQueryDevice(_x11.Display, (int)XiPredefinedDeviceId.XIAllMasterDevices, out int num); for (var c = 0; c < num; c++) @@ -237,6 +237,22 @@ namespace Avalonia.X11 RawPointerEventType.Move, ev.Position, ev.Modifiers)); } + if (ev.Type == XiEventType.XI_ButtonPress && ev.Button >= 4 && ev.Button <= 7 && !ev.Emulated) + { + var scrollDelta = ev.Button switch + { + 4 => new Vector(0, 1), + 5 => new Vector(0, -1), + 6 => new Vector(1, 0), + 7 => new Vector(-1, 0), + _ => (Vector?)null + }; + + if (scrollDelta.HasValue) + client.ScheduleXI2Input(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp, + client.InputRoot, ev.Position, scrollDelta.Value, ev.Modifiers)); + } + if (ev.Type == XiEventType.XI_ButtonPress || ev.Type == XiEventType.XI_ButtonRelease) { var down = ev.Type == XiEventType.XI_ButtonPress; diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs index 3c41f7bdde..85aa4862b7 100644 --- a/src/Avalonia.X11/XLib.cs +++ b/src/Avalonia.X11/XLib.cs @@ -236,6 +236,10 @@ namespace Avalonia.X11 public static extern int XChangeProperty(IntPtr display, IntPtr window, IntPtr property, IntPtr type, int format, PropertyMode mode, ref IntPtr value, int nelements); + [DllImport(libX11)] + public static extern int XChangeProperty(IntPtr display, IntPtr window, IntPtr property, IntPtr type, + int format, PropertyMode mode, byte[] data, int nelements); + [DllImport(libX11)] public static extern int XChangeProperty(IntPtr display, IntPtr window, IntPtr property, IntPtr type, int format, PropertyMode mode, uint[] data, int nelements); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index d4a29e39c6..b8ae2eb4d8 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -10,7 +10,7 @@ using Avalonia.Rendering; namespace Avalonia.LinuxFramebuffer { - class FramebufferToplevelImpl : IEmbeddableWindowImpl, IScreenInfoProvider + class FramebufferToplevelImpl : ITopLevelImpl, IScreenInfoProvider { private readonly IOutputBackend _outputBackend; private readonly IInputBackend _inputBackend; @@ -75,16 +75,14 @@ namespace Avalonia.LinuxFramebuffer public Action TransparencyLevelChanged { get; set; } public Action Closed { get; set; } - public event Action LostFocus - { - add {} - remove {} - } + public Action LostFocus { get; set; } public Size ScaledSize => _outputBackend.PixelSize.ToSize(Scaling); public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { } public WindowTransparencyLevel TransparencyLevel { get; private set; } + + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/Drm.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/Drm.cs index e266c5ee54..787a2e4cb8 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/Drm.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/Drm.cs @@ -176,7 +176,13 @@ namespace Avalonia.LinuxFramebuffer.Output [DllImport(libdrm, SetLastError = true)] public static extern int drmModeAddFB(int fd, uint width, uint height, byte depth, byte bpp, uint pitch, uint bo_handle, - out uint buf_id); + out uint buf_id); + + [DllImport(libdrm, SetLastError = true)] + public static extern int drmModeAddFB2(int fd, uint width, uint height, + uint pixel_format, uint[] bo_handles, uint[] pitches, + uint[] offsets, out uint buf_id, uint flags); + [DllImport(libdrm, SetLastError = true)] public static extern int drmModeSetCrtc(int fd, uint crtcId, uint bufferId, uint x, uint y, uint *connectors, int count, @@ -261,6 +267,8 @@ namespace Avalonia.LinuxFramebuffer.Output [DllImport(libgbm, SetLastError = true)] public static extern uint gbm_bo_get_stride(IntPtr bo); + [DllImport(libgbm, SetLastError = true)] + public static extern uint gbm_bo_get_format(IntPtr bo); [StructLayout(LayoutKind.Explicit)] public struct GbmBoHandle @@ -278,7 +286,7 @@ namespace Avalonia.LinuxFramebuffer.Output } [DllImport(libgbm, SetLastError = true)] - public static extern ulong gbm_bo_get_handle(IntPtr bo); + public static extern GbmBoHandle gbm_bo_get_handle(IntPtr bo); public static class GbmColorFormats { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index 0f5b66befb..7a5d20fc83 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -7,6 +7,8 @@ using Avalonia.OpenGL; using Avalonia.Platform.Interop; using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods; using static Avalonia.LinuxFramebuffer.Output.LibDrm; +using static Avalonia.LinuxFramebuffer.Output.LibDrm.GbmColorFormats; + namespace Avalonia.LinuxFramebuffer.Output { public unsafe class DrmOutput : IOutputBackend, IGlPlatformSurface, IWindowingPlatformGlFeature @@ -71,11 +73,26 @@ namespace Avalonia.LinuxFramebuffer.Output var w = gbm_bo_get_width(bo); var h = gbm_bo_get_height(bo); var stride = gbm_bo_get_stride(bo); - var handle = gbm_bo_get_handle(bo); + var handle = gbm_bo_get_handle(bo).u32; + var format = gbm_bo_get_format(bo); + + // prepare for the new ioctl call + var handles = new uint[] {handle, 0, 0, 0}; + var pitches = new uint[] {stride, 0, 0, 0}; + var offsets = new uint[] {}; + + var ret = drmModeAddFB2(_card.Fd, w, h, format, handles, pitches, + offsets, out var fbHandle, 0); - var ret = drmModeAddFB(_card.Fd, w, h, 24, 32, stride, (uint)handle, out var fbHandle); if (ret != 0) - throw new Win32Exception(ret, "drmModeAddFb failed"); + { + // legacy fallback + ret = drmModeAddFB(_card.Fd, w, h, 24, 32, stride, (uint)handle, + out fbHandle); + + if (ret != 0) + throw new Win32Exception(ret, $"drmModeAddFb failed {ret}"); + } gbm_bo_set_user_data(bo, new IntPtr((int)fbHandle), FbDestroyDelegate); diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 06c5375520..da345835c8 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -1,7 +1,7 @@  netstandard2.0 - PCL;NETSTANDARD;NETSTANDARD2_0;HAS_TYPE_CONVERTER;HAS_CUSTOM_ATTRIBUTE_PROVIDER;XAMLIL_INTERNAL + PCL;NETSTANDARD;NETSTANDARD2_0;HAS_TYPE_CONVERTER;HAS_CUSTOM_ATTRIBUTE_PROVIDER;XAMLX_INTERNAL false $(DefineConstants);RUNTIME_XAML_CECIL False @@ -11,10 +11,19 @@ + + + + + + + + + @@ -24,7 +33,7 @@ - + @@ -36,13 +45,21 @@ - + + + + + + + + + @@ -53,14 +70,18 @@ + + + - - + + + diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs index cd67b27ff3..45ca1c4adc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs @@ -39,8 +39,7 @@ namespace Avalonia.Markup.Xaml.Converters !property.IsAttached && !registry.IsRegistered(targetType, property)) { - Logger.TryGet(LogEventLevel.Warning)?.Log( - LogArea.Property, + Logger.TryGet(LogEventLevel.Warning, LogArea.Property)?.Log( this, "Property '{Owner}.{Name}' is not registered on '{Type}'.", effectiveOwner, diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/ColorToBrushConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/ColorToBrushConverter.cs new file mode 100644 index 0000000000..5b44e9eb50 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/ColorToBrushConverter.cs @@ -0,0 +1,88 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace Avalonia.Markup.Xaml.Converters +{ + /// + /// Converts a to an . + /// + public class ColorToBrushConverter : IValueConverter + { + /// + /// Converts a to an if the arguments are of the + /// correct type. + /// + /// The value. + /// The target type. + /// Not used. + /// Not used. + /// + /// If is a and + /// is then converts the color to a solid color brush. + /// + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return Convert(value, targetType); + } + + /// + /// Converts an to a if the arguments are of the + /// correct type. + /// + /// The value. + /// The target type. + /// Not used. + /// Not used. + /// + /// If is an and + /// is then converts the solid color brush to a color. + /// + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return ConvertBack(value, targetType); + } + + /// + /// Converts a to an if the arguments are of the + /// correct type. + /// + /// The value. + /// The target type. + /// + /// If is a and + /// is then converts the color to a solid color brush. + /// + public static object Convert(object value, Type targetType) + { + if (targetType == typeof(IBrush) && value is Color c) + { + return new ImmutableSolidColorBrush(c); + } + + return value; + } + + /// + /// Converts an to a if the arguments are of the + /// correct type. + /// + /// The value. + /// The target type. + /// + /// If is an and + /// is then converts the solid color brush to a color. + /// + public static object ConvertBack(object value, Type targetType) + { + if (targetType == typeof(Color) && value is ISolidColorBrush brush) + { + return brush.Color; + } + + return value; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs new file mode 100644 index 0000000000..aab733cb43 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs @@ -0,0 +1,79 @@ +using System; +using Avalonia.Data; +using Avalonia.Controls; +using Avalonia.Styling; +using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings; +using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; + +namespace Avalonia.Markup.Xaml.MarkupExtensions +{ + public class CompiledBindingExtension : BindingBase + { + public CompiledBindingExtension() + { + Path = new CompiledBindingPath(); + } + + public CompiledBindingExtension(CompiledBindingPath path) + { + Path = path; + } + + public CompiledBindingExtension ProvideValue(IServiceProvider provider) + { + return new CompiledBindingExtension + { + Path = Path, + Converter = Converter, + FallbackValue = FallbackValue, + Mode = Mode, + Priority = Priority, + StringFormat = StringFormat, + DefaultAnchor = new WeakReference(GetDefaultAnchor(provider)) + }; + } + + private static object GetDefaultAnchor(IServiceProvider provider) + { + // If the target is not a control, so we need to find an anchor that will let us look + // up named controls and style resources. First look for the closest IControl in + // the context. + object anchor = provider.GetFirstParent(); + + // If a control was not found, then try to find the highest-level style as the XAML + // file could be a XAML file containing only styles. + return anchor ?? + provider.GetService()?.RootObject as IStyle ?? + provider.GetLastParent(); + } + + protected override ExpressionObserver CreateExpressionObserver(IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation) + { + if (Path.RawSource != null) + { + return CreateSourceObserver( + Path.RawSource, + Path.BuildExpression(enableDataValidation)); + } + + if (Path.SourceMode == SourceMode.Data) + { + return CreateDataContextObserver( + target, + Path.BuildExpression(enableDataValidation), + targetProperty == StyledElement.DataContextProperty, + anchor); + } + else + { + return CreateSourceObserver( + (target as IStyledElement) ?? (anchor as IStyledElement), + Path.BuildExpression(enableDataValidation)); + } + } + + [ConstructorArgument("path")] + public CompiledBindingPath Path { get; set; } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ArrayElementPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ArrayElementPlugin.cs new file mode 100644 index 0000000000..7cbe4942e5 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ArrayElementPlugin.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Data; +using Avalonia.Data.Core.Plugins; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + class ArrayElementPlugin : IPropertyAccessorPlugin + { + private readonly int[] _indices; + private readonly Type _elementType; + + public ArrayElementPlugin(int[] indices, Type elementType) + { + _indices = indices; + _elementType = elementType; + } + + public bool Match(object obj, string propertyName) + { + throw new InvalidOperationException("The ArrayElementPlugin does not support dynamic matching"); + } + + public IPropertyAccessor Start(WeakReference reference, string propertyName) + { + if (reference.TryGetTarget(out var target) && target is Array arr) + { + return new Accessor(new WeakReference(arr), _indices, _elementType); + } + return null; + } + + class Accessor : PropertyAccessorBase + { + private readonly int[] _indices; + private readonly WeakReference _reference; + + public Accessor(WeakReference reference, int[] indices, Type elementType) + { + _reference = reference; + _indices = indices; + PropertyType = elementType; + } + + public override Type PropertyType { get; } + + public override object Value => _reference.TryGetTarget(out var arr) ? arr.GetValue(_indices) : null; + + public override bool SetValue(object value, BindingPriority priority) + { + if (_reference.TryGetTarget(out var arr)) + { + arr.SetValue(value, _indices); + return true; + } + return false; + } + + protected override void SubscribeCore() + { + PublishValue(Value); + } + + protected override void UnsubscribeCore() + { + } + } + } + +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs new file mode 100644 index 0000000000..d627fe3cd3 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Parsers; +using Avalonia.Markup.Parsers.Nodes; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + public class CompiledBindingPath + { + private readonly List _elements = new List(); + + public CompiledBindingPath() { } + + internal CompiledBindingPath(IEnumerable bindingPath, object rawSource) + { + _elements = new List(bindingPath); + RawSource = rawSource; + } + + public ExpressionNode BuildExpression(bool enableValidation) + { + ExpressionNode pathRoot = null; + ExpressionNode path = null; + foreach (var element in _elements) + { + ExpressionNode node = null; + switch (element) + { + case NotExpressionPathElement _: + node = new LogicalNotNode(); + break; + case PropertyElement prop: + node = new PropertyAccessorNode(prop.Property.Name, enableValidation, new PropertyInfoAccessorPlugin(prop.Property, prop.AccessorFactory)); + break; + case ArrayElementPathElement arr: + node = new PropertyAccessorNode(CommonPropertyNames.IndexerName, enableValidation, new ArrayElementPlugin(arr.Indices, arr.ElementType)); + break; + case VisualAncestorPathElement visualAncestor: + node = new FindVisualAncestorNode(visualAncestor.AncestorType, visualAncestor.Level); + break; + case AncestorPathElement ancestor: + node = new FindAncestorNode(ancestor.AncestorType, ancestor.Level); + break; + case SelfPathElement _: + node = new SelfNode(); + break; + case ElementNameElement name: + node = new ElementNameNode(name.NameScope, name.Name); + break; + case IStronglyTypedStreamElement stream: + node = new StreamNode(stream.CreatePlugin()); + break; + default: + throw new InvalidOperationException($"Unknown binding path element type {element.GetType().FullName}"); + } + + path = pathRoot is null ? (pathRoot = node) : path.Next = node; + } + + return pathRoot ?? new EmptyExpressionNode(); + } + + internal SourceMode SourceMode => _elements.Count > 0 && _elements[0] is IControlSourceBindingPathElement ? SourceMode.Control : SourceMode.Data; + + internal object RawSource { get; } + } + + public class CompiledBindingPathBuilder + { + private object _rawSource; + private List _elements = new List(); + + public CompiledBindingPathBuilder Not() + { + _elements.Add(new NotExpressionPathElement()); + return this; + } + + public CompiledBindingPathBuilder Property(IPropertyInfo info, Func, IPropertyInfo, IPropertyAccessor> accessorFactory) + { + _elements.Add(new PropertyElement(info, accessorFactory)); + return this; + } + + public CompiledBindingPathBuilder StreamTask() + { + _elements.Add(new TaskStreamPathElement()); + return this; + } + + public CompiledBindingPathBuilder StreamObservable() + { + _elements.Add(new ObservableStreamPathElement()); + return this; + } + + public CompiledBindingPathBuilder Self() + { + _elements.Add(new SelfPathElement()); + return this; + } + + public CompiledBindingPathBuilder Ancestor(Type ancestorType, int level) + { + _elements.Add(new AncestorPathElement(ancestorType, level)); + return this; + } + public CompiledBindingPathBuilder VisualAncestor(Type ancestorType, int level) + { + _elements.Add(new VisualAncestorPathElement(ancestorType, level)); + return this; + } + + public CompiledBindingPathBuilder ElementName(INameScope nameScope, string name) + { + _elements.Add(new ElementNameElement(nameScope, name)); + return this; + } + + public CompiledBindingPathBuilder ArrayElement(int[] indices, Type elementType) + { + _elements.Add(new ArrayElementPathElement(indices, elementType)); + return this; + } + + public CompiledBindingPathBuilder SetRawSource(object rawSource) + { + _rawSource = rawSource; + return this; + } + + public CompiledBindingPath Build() => new CompiledBindingPath(_elements, _rawSource); + } + + public interface ICompiledBindingPathElement + { + } + + internal interface IControlSourceBindingPathElement { } + + internal class NotExpressionPathElement : ICompiledBindingPathElement + { + public static readonly NotExpressionPathElement Instance = new NotExpressionPathElement(); + } + + internal class PropertyElement : ICompiledBindingPathElement + { + public PropertyElement(IPropertyInfo property, Func, IPropertyInfo, IPropertyAccessor> accessorFactory) + { + Property = property; + AccessorFactory = accessorFactory; + } + + public IPropertyInfo Property { get; } + + public Func, IPropertyInfo, IPropertyAccessor> AccessorFactory { get; } + } + + internal interface IStronglyTypedStreamElement : ICompiledBindingPathElement + { + IStreamPlugin CreatePlugin(); + } + + internal class TaskStreamPathElement : IStronglyTypedStreamElement + { + public static readonly TaskStreamPathElement Instance = new TaskStreamPathElement(); + + public IStreamPlugin CreatePlugin() => new TaskStreamPlugin(); + } + + internal class ObservableStreamPathElement : IStronglyTypedStreamElement + { + public static readonly ObservableStreamPathElement Instance = new ObservableStreamPathElement(); + + public IStreamPlugin CreatePlugin() => new ObservableStreamPlugin(); + } + + internal class SelfPathElement : ICompiledBindingPathElement, IControlSourceBindingPathElement + { + public static readonly SelfPathElement Instance = new SelfPathElement(); + } + + internal class AncestorPathElement : ICompiledBindingPathElement, IControlSourceBindingPathElement + { + public AncestorPathElement(Type ancestorType, int level) + { + AncestorType = ancestorType; + Level = level; + } + + public Type AncestorType { get; } + public int Level { get; } + } + + internal class VisualAncestorPathElement : ICompiledBindingPathElement, IControlSourceBindingPathElement + { + public VisualAncestorPathElement(Type ancestorType, int level) + { + AncestorType = ancestorType; + Level = level; + } + + public Type AncestorType { get; } + public int Level { get; } + } + + internal class ElementNameElement : ICompiledBindingPathElement, IControlSourceBindingPathElement + { + public ElementNameElement(INameScope nameScope, string name) + { + NameScope = nameScope; + Name = name; + } + + public INameScope NameScope { get; } + public string Name { get; } + } + + internal class ArrayElementPathElement : ICompiledBindingPathElement + { + public ArrayElementPathElement(int[] indices, Type elementType) + { + Indices = indices; + ElementType = elementType; + } + + public int[] Indices { get; } + public Type ElementType { get; } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/FindVisualAncestorNode.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/FindVisualAncestorNode.cs new file mode 100644 index 0000000000..5820f47fbb --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/FindVisualAncestorNode.cs @@ -0,0 +1,52 @@ +using System; +using Avalonia.Data.Core; +using Avalonia.VisualTree; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + class FindVisualAncestorNode : ExpressionNode + { + private readonly int _level; + private readonly Type _ancestorType; + private IDisposable _subscription; + + public FindVisualAncestorNode(Type ancestorType, int level) + { + _level = level; + _ancestorType = ancestorType; + } + + public override string Description + { + get + { + if (_ancestorType == null) + { + return $"$visualparent[{_level}]"; + } + else + { + return $"$visualparent[{_ancestorType.Name}, {_level}]"; + } + } + } + + protected override void StartListeningCore(WeakReference reference) + { + if (reference.TryGetTarget(out object target) && target is IVisual visual) + { + _subscription = VisualLocator.Track(visual, _level, _ancestorType).Subscribe(ValueChanged); + } + else + { + _subscription = null; + } + } + + protected override void StopListeningCore() + { + _subscription?.Dispose(); + _subscription = null; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs new file mode 100644 index 0000000000..3e3174123a --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Text; +using Avalonia.Data.Core.Plugins; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + class ObservableStreamPlugin : IStreamPlugin + { + public bool Match(WeakReference reference) + { + return reference.TryGetTarget(out var target) && target is IObservable; + } + + public IObservable Start(WeakReference reference) + { + if (!(reference.TryGetTarget(out var target) && target is IObservable obs)) + { + return Observable.Empty(); + } + else if (target is IObservable obj) + { + return obj; + } + + return obs.Select(x => (object)x); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs new file mode 100644 index 0000000000..b3f78bfbe3 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Text; +using Avalonia.Data; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; +using Avalonia.Utilities; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + public static class PropertyInfoAccessorFactory + { + public static IPropertyAccessor CreateInpcPropertyAccessor(WeakReference target, IPropertyInfo property) + => new InpcPropertyAccessor(target, property); + + public static IPropertyAccessor CreateAvaloniaPropertyAccessor(WeakReference target, IPropertyInfo property) + => new AvaloniaPropertyAccessor(new WeakReference((AvaloniaObject)(target.TryGetTarget(out var o) ? o : null)), (AvaloniaProperty)property); + + public static IPropertyAccessor CreateIndexerPropertyAccessor(WeakReference target, IPropertyInfo property, int argument) + => new IndexerAccessor(target, property, argument); + } + + internal class AvaloniaPropertyAccessor : PropertyAccessorBase + { + private readonly WeakReference _reference; + private readonly AvaloniaProperty _property; + private IDisposable _subscription; + + public AvaloniaPropertyAccessor(WeakReference reference, AvaloniaProperty property) + { + Contract.Requires(reference != null); + Contract.Requires(property != null); + + _reference = reference; + _property = property; + } + + public AvaloniaObject Instance + { + get + { + _reference.TryGetTarget(out var result); + return result; + } + } + + public override Type PropertyType => _property.PropertyType; + public override object Value => Instance?.GetValue(_property); + + public override bool SetValue(object value, BindingPriority priority) + { + if (!_property.IsReadOnly) + { + Instance.SetValue(_property, value, priority); + return true; + } + + return false; + } + + protected override void SubscribeCore() + { + _subscription = Instance?.GetObservable(_property).Subscribe(PublishValue); + } + + protected override void UnsubscribeCore() + { + _subscription?.Dispose(); + _subscription = null; + } + } + + internal class InpcPropertyAccessor : PropertyAccessorBase + { + protected readonly WeakReference _reference; + private readonly IPropertyInfo _property; + + public InpcPropertyAccessor(WeakReference reference, IPropertyInfo property) + { + Contract.Requires(reference != null); + Contract.Requires(property != null); + + _reference = reference; + _property = property; + } + + public override Type PropertyType => _property.PropertyType; + + public override object Value + { + get + { + return _reference.TryGetTarget(out var o) ? _property.Get(o) : null; + } + } + + public override bool SetValue(object value, BindingPriority priority) + { + if (_property.CanSet && _reference.TryGetTarget(out var o)) + { + _property.Set(o, value); + + SendCurrentValue(); + + return true; + } + + return false; + } + + void OnNotifyPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) + { + SendCurrentValue(); + } + } + + protected override void SubscribeCore() + { + SendCurrentValue(); + SubscribeToChanges(); + } + + protected override void UnsubscribeCore() + { + if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc) + { + WeakEventHandlerManager.Unsubscribe( + inpc, + nameof(INotifyPropertyChanged.PropertyChanged), + OnNotifyPropertyChanged); + } + } + + protected void SendCurrentValue() + { + try + { + var value = Value; + PublishValue(value); + } + catch { } + } + + private void SubscribeToChanges() + { + if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc) + { + WeakEventHandlerManager.Subscribe( + inpc, + nameof(INotifyPropertyChanged.PropertyChanged), + OnNotifyPropertyChanged); + } + } + } + + internal class IndexerAccessor : InpcPropertyAccessor + { + private int _index; + + public IndexerAccessor(WeakReference target, IPropertyInfo basePropertyInfo, int argument) + :base(target, basePropertyInfo) + { + _index = argument; + } + + + protected override void SubscribeCore() + { + base.SubscribeCore(); + if (_reference.TryGetTarget(out var o) && o is INotifyCollectionChanged incc) + { + WeakEventHandlerManager.Subscribe( + incc, + nameof(INotifyCollectionChanged.CollectionChanged), + OnNotifyCollectionChanged); + } + } + + protected override void UnsubscribeCore() + { + base.UnsubscribeCore(); + if (_reference.TryGetTarget(out var o) && o is INotifyCollectionChanged incc) + { + WeakEventHandlerManager.Unsubscribe( + incc, + nameof(INotifyCollectionChanged.CollectionChanged), + OnNotifyCollectionChanged); + } + } + + void OnNotifyCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) + { + if (ShouldNotifyListeners(args)) + { + SendCurrentValue(); + } + } + + bool ShouldNotifyListeners(NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + return _index >= e.NewStartingIndex; + case NotifyCollectionChangedAction.Remove: + return _index >= e.OldStartingIndex; + case NotifyCollectionChangedAction.Replace: + return _index >= e.NewStartingIndex && + _index < e.NewStartingIndex + e.NewItems.Count; + case NotifyCollectionChangedAction.Move: + return (_index >= e.NewStartingIndex && + _index < e.NewStartingIndex + e.NewItems.Count) || + (_index >= e.OldStartingIndex && + _index < e.OldStartingIndex + e.OldItems.Count); + case NotifyCollectionChangedAction.Reset: + return true; + } + return false; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorPlugin.cs new file mode 100644 index 0000000000..6e7463776b --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorPlugin.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using Avalonia.Data; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + class PropertyInfoAccessorPlugin : IPropertyAccessorPlugin + { + private readonly IPropertyInfo _propertyInfo; + private readonly Func, IPropertyInfo, IPropertyAccessor> _accessorFactory; + + public PropertyInfoAccessorPlugin(IPropertyInfo propertyInfo, Func, IPropertyInfo, IPropertyAccessor> accessorFactory) + { + _propertyInfo = propertyInfo; + _accessorFactory = accessorFactory; + } + + public bool Match(object obj, string propertyName) + { + throw new InvalidOperationException("The PropertyInfoAccessorPlugin does not support dynamic matching"); + } + + public IPropertyAccessor Start(WeakReference reference, string propertyName) + { + Debug.Assert(_propertyInfo.Name == propertyName); + return _accessorFactory(reference, _propertyInfo); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs new file mode 100644 index 0000000000..164d38e3ae --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs @@ -0,0 +1,53 @@ +using System; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Threading.Tasks; +using Avalonia.Data; +using Avalonia.Data.Core.Plugins; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + class TaskStreamPlugin : IStreamPlugin + { + public bool Match(WeakReference reference) + { + return reference.TryGetTarget(out var target) && target is Task; + } + + public IObservable Start(WeakReference reference) + { + if(!(reference.TryGetTarget(out var target) && target is Task task)) + { + return Observable.Empty(); + } + + switch (task.Status) + { + case TaskStatus.RanToCompletion: + case TaskStatus.Faulted: + return HandleCompleted(task); + default: + var subject = new Subject(); + task.ContinueWith( + x => HandleCompleted(task).Subscribe(subject), + TaskScheduler.FromCurrentSynchronizationContext()) + .ConfigureAwait(false); + return subject; + } + } + + + private static IObservable HandleCompleted(Task task) + { + switch (task.Status) + { + case TaskStatus.RanToCompletion: + return Observable.Return((object)task.Result); + case TaskStatus.Faulted: + return Observable.Return(new BindingNotification(task.Exception, BindingErrorType.Error)); + default: + throw new AvaloniaInternalException("HandleCompleted called for non-completed Task."); + } + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index d0d4aae9b8..03fd2e60dd 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -1,6 +1,8 @@ using System; using Avalonia.Controls; using Avalonia.Data; +using Avalonia.Markup.Xaml.Converters; +using Avalonia.Media; #nullable enable @@ -54,11 +56,23 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions if (control != null) { - return InstancedBinding.OneWay(control.GetResourceObservable(ResourceKey)); + var source = control.GetResourceObservable(ResourceKey, GetConverter(targetProperty)); + return InstancedBinding.OneWay(source); } else if (_resourceProvider is object) { - return InstancedBinding.OneWay(_resourceProvider.GetResourceObservable(ResourceKey)); + var source = _resourceProvider.GetResourceObservable(ResourceKey, GetConverter(targetProperty)); + return InstancedBinding.OneWay(source); + } + + return null; + } + + private Func? GetConverter(AvaloniaProperty targetProperty) + { + if (targetProperty?.PropertyType == typeof(IBrush)) + { + return x => ColorToBrushConverter.Convert(x, typeof(IBrush)); } return null; diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs similarity index 95% rename from src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs rename to src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs index cc66efa5fd..10770365a3 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs @@ -9,13 +9,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions using Avalonia.Styling; using System.ComponentModel; - public class BindingExtension + public class ReflectionBindingExtension { - public BindingExtension() + public ReflectionBindingExtension() { } - public BindingExtension(string path) + public ReflectionBindingExtension(string path) { Path = path; } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs index 87e2ba3a2e..0cedf4f364 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs @@ -13,6 +13,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions { private Uri? _baseUri; private IResourceDictionary? _loaded; + private bool _isLoading; /// /// Gets the loaded resource dictionary. @@ -23,8 +24,10 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions { if (_loaded == null) { + _isLoading = true; var loader = new AvaloniaXamlLoader(); _loaded = (IResourceDictionary)loader.Load(Source, _baseUri); + _isLoading = false; } return _loaded; @@ -48,7 +51,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions bool IResourceNode.TryGetResource(object key, out object? value) { - return Loaded.TryGetResource(key, out value); + if (!_isLoading) + { + return Loaded.TryGetResource(key, out value); + } + + value = null; + return false; } void IResourceProvider.AddOwner(IResourceHost owner) => Loaded.AddOwner(owner); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index 6d9fac638c..4ad68fc63e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Reflection; using Avalonia.Controls; using Avalonia.Markup.Data; +using Avalonia.Markup.Xaml.Converters; using Avalonia.Markup.Xaml.XamlIl.Runtime; namespace Avalonia.Markup.Xaml.MarkupExtensions @@ -24,6 +24,14 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public object ProvideValue(IServiceProvider serviceProvider) { var stack = serviceProvider.GetService(); + var provideTarget = serviceProvider.GetService(); + + var targetType = provideTarget.TargetProperty switch + { + AvaloniaProperty ap => ap.PropertyType, + PropertyInfo pi => pi.PropertyType, + _ => null, + }; // Look upwards though the ambient context for IResourceHosts and IResourceProviders // which might be able to give us the resource. @@ -33,30 +41,27 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions if (e is IResourceHost host && host.TryGetResource(ResourceKey, out value)) { - return value; + return ColorToBrushConverter.Convert(value, targetType); } else if (e is IResourceProvider provider && provider.TryGetResource(ResourceKey, out value)) { - return value; + return ColorToBrushConverter.Convert(value, targetType); } } - // The resource still hasn't been found, so add a delayed one-time binding. - var provideTarget = serviceProvider.GetService(); - if (provideTarget.TargetObject is IControl target && provideTarget.TargetProperty is PropertyInfo property) { - DelayedBinding.Add(target, property, GetValue); + DelayedBinding.Add(target, property, x => GetValue(x, targetType)); return AvaloniaProperty.UnsetValue; } throw new KeyNotFoundException($"Static resource '{ResourceKey}' not found."); } - private object GetValue(IStyledElement control) + private object GetValue(IStyledElement control, Type targetType) { - return control.FindResource(ResourceKey); + return ColorToBrushConverter.Convert(control.FindResource(ResourceKey), targetType); } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 2b39263ee9..ea9042f779 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -14,6 +14,7 @@ namespace Avalonia.Markup.Xaml.Styling { private readonly Uri _baseUri; private IStyle[]? _loaded; + private bool _isLoading; /// /// Initializes a new instance of the class. @@ -49,9 +50,11 @@ namespace Avalonia.Markup.Xaml.Styling { if (_loaded == null) { + _isLoading = true; var loader = new AvaloniaXamlLoader(); var loaded = (IStyle)loader.Load(Source, _baseUri); _loaded = new[] { loaded }; + _isLoading = false; } return _loaded?[0]!; @@ -84,7 +87,7 @@ namespace Avalonia.Markup.Xaml.Styling public bool TryGetResource(object key, out object? value) { - if (Loaded is IResourceProvider p) + if (!_isLoading && Loaded is IResourceProvider p) { return p.TryGetResource(key, out value); } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs index 5a5da518d0..d9b4cd70da 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs @@ -10,12 +10,15 @@ using System.Runtime.InteropServices; using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions; using Avalonia.Markup.Xaml.XamlIl.Runtime; using Avalonia.Platform; -using XamlIl.Transform; -using XamlIl.TypeSystem; +using XamlX.Transform; +using XamlX.TypeSystem; +using XamlX.IL; +using XamlX.Emit; #if RUNTIME_XAML_CECIL using TypeAttributes = Mono.Cecil.TypeAttributes; using Mono.Cecil; -using XamlIl.Ast; +using XamlX.Ast; +using XamlX.IL.Cecil; #endif namespace Avalonia.Markup.Xaml.XamlIl { @@ -24,9 +27,10 @@ namespace Avalonia.Markup.Xaml.XamlIl #if !RUNTIME_XAML_CECIL private static SreTypeSystem _sreTypeSystem; private static ModuleBuilder _sreBuilder; - private static IXamlIlType _sreContextType; - private static XamlIlLanguageTypeMappings _sreMappings; - private static XamlIlXmlnsMappings _sreXmlns; + private static IXamlType _sreContextType; + private static XamlLanguageTypeMappings _sreMappings; + private static XamlLanguageEmitMappings _sreEmitMappings; + private static XamlXmlnsMappings _sreXmlns; private static AssemblyBuilder _sreAsm; private static bool _sreCanSave; @@ -82,13 +86,14 @@ namespace Avalonia.Markup.Xaml.XamlIl } if (_sreMappings == null) - _sreMappings = AvaloniaXamlIlLanguage.Configure(_sreTypeSystem); + (_sreMappings, _sreEmitMappings) = AvaloniaXamlIlLanguage.Configure(_sreTypeSystem); if (_sreXmlns == null) - _sreXmlns = XamlIlXmlnsMappings.Resolve(_sreTypeSystem, _sreMappings); + _sreXmlns = XamlXmlnsMappings.Resolve(_sreTypeSystem, _sreMappings); if (_sreContextType == null) - _sreContextType = XamlIlContextDefinition.GenerateContextClass( + _sreContextType = XamlILContextDefinition.GenerateContextClass( _sreTypeSystem.CreateTypeBuilder( - _sreBuilder.DefineType("XamlIlContext")), _sreTypeSystem, _sreMappings); + _sreBuilder.DefineType("XamlIlContext")), _sreTypeSystem, _sreMappings, + _sreEmitMappings); } @@ -114,13 +119,19 @@ namespace Avalonia.Markup.Xaml.XamlIl InitializeSre(); var asm = localAssembly == null ? null : _sreTypeSystem.GetAssembly(localAssembly); + var tb = _sreBuilder.DefineType("Builder_" + Guid.NewGuid().ToString("N") + "_" + uri); + var clrPropertyBuilder = tb.DefineNestedType("ClrProperties_" + Guid.NewGuid().ToString("N")); + var indexerClosureType = _sreBuilder.DefineType("IndexerClosure_" + Guid.NewGuid().ToString("N")); - var compiler = new AvaloniaXamlIlCompiler(new XamlIlTransformerConfiguration(_sreTypeSystem, asm, - _sreMappings, _sreXmlns, AvaloniaXamlIlLanguage.CustomValueConverter), + var compiler = new AvaloniaXamlIlCompiler(new AvaloniaXamlIlCompilerConfiguration(_sreTypeSystem, asm, + _sreMappings, _sreXmlns, AvaloniaXamlIlLanguage.CustomValueConverter, + new XamlIlClrPropertyInfoEmitter(_sreTypeSystem.CreateTypeBuilder(clrPropertyBuilder)), + new XamlIlPropertyInfoAccessorFactoryEmitter(_sreTypeSystem.CreateTypeBuilder(indexerClosureType))), + _sreEmitMappings, _sreContextType) { EnableIlVerification = true }; - var tb = _sreBuilder.DefineType("Builder_" + Guid.NewGuid().ToString("N") + "_" + uri); + - IXamlIlType overrideType = null; + IXamlType overrideType = null; if (rootInstance != null) { overrideType = _sreTypeSystem.GetType(rootInstance.GetType()); @@ -129,6 +140,7 @@ namespace Avalonia.Markup.Xaml.XamlIl compiler.IsDesignMode = isDesignMode; compiler.ParseAndCompile(xaml, uri?.ToString(), null, _sreTypeSystem.CreateTypeBuilder(tb), overrideType); var created = tb.CreateTypeInfo(); + clrPropertyBuilder.CreateTypeInfo(); return LoadOrPopulate(created, rootInstance); } @@ -203,6 +215,7 @@ namespace Avalonia.Markup.Xaml.XamlIl private static string _cecilEmitDir; private static CecilTypeSystem _cecilTypeSystem; private static XamlIlLanguageTypeMappings _cecilMappings; + private static XamlLanguageEmitMappings _cecilEmitMappings; private static XamlIlXmlnsMappings _cecilXmlns; private static bool _cecilInitialized; @@ -215,7 +228,7 @@ namespace Avalonia.Markup.Xaml.XamlIl Directory.CreateDirectory(_cecilEmitDir); var refs = new[] {path}.Concat(File.ReadAllLines(path + ".refs")); _cecilTypeSystem = new CecilTypeSystem(refs); - _cecilMappings = AvaloniaXamlIlLanguage.Configure(_cecilTypeSystem); + (_cecilMappings, _cecilEmitMappings) = AvaloniaXamlIlLanguage.Configure(_cecilTypeSystem); _cecilXmlns = XamlIlXmlnsMappings.Resolve(_cecilTypeSystem, _cecilMappings); _cecilInitialized = true; } @@ -226,7 +239,7 @@ namespace Avalonia.Markup.Xaml.XamlIl if (uri == null) throw new InvalidOperationException("Please, go away"); InitializeCecil(); - IXamlIlType overrideType = null; + IXamlType overrideType = null; if (rootInstance != null) { overrideType = _cecilTypeSystem.GetType(rootInstance.GetType().FullName); @@ -261,6 +274,7 @@ namespace Avalonia.Markup.Xaml.XamlIl localAssembly == null ? null : _cecilTypeSystem.FindAssembly(localAssembly.GetName().Name), _cecilMappings, XamlIlXmlnsMappings.Resolve(_cecilTypeSystem, _cecilMappings), AvaloniaXamlIlLanguage.CustomValueConverter), + _cecilEmitMappings, _cecilTypeSystem.CreateTypeBuilder(contextDef)); compiler.ParseAndCompile(xaml, uri.ToString(), tb, overrideType); var asmPath = Path.Combine(_cecilEmitDir, safeUri + ".dll"); diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs index b84f50fa8d..f3b8559d45 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -1,29 +1,33 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; -using XamlIl; -using XamlIl.Ast; -using XamlIl.Parsers; -using XamlIl.Transform; -using XamlIl.Transform.Transformers; -using XamlIl.TypeSystem; +using XamlX; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Parsers; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { - class AvaloniaXamlIlCompiler : XamlIlCompiler + class AvaloniaXamlIlCompiler : XamlILCompiler { - private readonly XamlIlTransformerConfiguration _configuration; - private readonly IXamlIlType _contextType; + private readonly TransformerConfiguration _configuration; + private readonly IXamlType _contextType; private readonly AvaloniaXamlIlDesignPropertiesTransformer _designTransformer; + private readonly AvaloniaBindingExtensionTransformer _bindingTransformer; - private AvaloniaXamlIlCompiler(XamlIlTransformerConfiguration configuration) : base(configuration, true) + private AvaloniaXamlIlCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings emitMappings) + : base(configuration, emitMappings, true) { _configuration = configuration; - void InsertAfter(params IXamlIlAstTransformer[] t) + void InsertAfter(params IXamlAstTransformer[] t) => Transformers.InsertRange(Transformers.FindIndex(x => x is T) + 1, t); - void InsertBefore(params IXamlIlAstTransformer[] t) + void InsertBefore(params IXamlAstTransformer[] t) => Transformers.InsertRange(Transformers.FindIndex(x => x is T), t); @@ -32,40 +36,54 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions Transformers.Insert(0, new XNameTransformer()); Transformers.Insert(1, new IgnoredDirectivesTransformer()); Transformers.Insert(2, _designTransformer = new AvaloniaXamlIlDesignPropertiesTransformer()); - Transformers.Insert(3, new AvaloniaBindingExtensionHackTransformer()); + Transformers.Insert(3, _bindingTransformer = new AvaloniaBindingExtensionTransformer()); // Targeted - InsertBefore(new AvaloniaXamlIlTransformInstanceAttachedProperties()); - InsertAfter(new AvaloniaXamlIlAvaloniaPropertyResolver()); + InsertBefore( + new AvaloniaXamlIlTransformInstanceAttachedProperties(), + new AvaloniaXamlIlTransformSyntheticCompiledBindingMembers()); + InsertAfter(new AvaloniaXamlIlAvaloniaPropertyResolver()); - InsertBefore( + InsertBefore( + new AvaloniaXamlIlBindingPathParser(), new AvaloniaXamlIlSelectorTransformer(), - new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), + new AvaloniaXamlIlPropertyPathTransformer(), + new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(), new AvaloniaXamlIlTransitionsTypeMetadataTransformer() ); - + // After everything else - - Transformers.Add(new AddNameScopeRegistration()); + InsertBefore( + new AddNameScopeRegistration(), + new AvaloniaXamlIlDataContextTypeTransformer(), + new AvaloniaXamlIlBindingPathTransformer(), + new AvaloniaXamlIlCompiledBindingsMetadataRemover() + ); + Transformers.Add(new AvaloniaXamlIlMetadataRemover()); + Transformers.Add(new AvaloniaXamlIlRootObjectScope()); + Emitters.Add(new AvaloniaNameScopeRegistrationXamlIlNodeEmitter()); + Emitters.Add(new AvaloniaXamlIlRootObjectScope.Emitter()); } - - public AvaloniaXamlIlCompiler(XamlIlTransformerConfiguration configuration, - IXamlIlTypeBuilder contextTypeBuilder) : this(configuration) + public AvaloniaXamlIlCompiler(TransformerConfiguration configuration, + XamlLanguageEmitMappings emitMappings, + IXamlTypeBuilder contextTypeBuilder) + : this(configuration, emitMappings) { _contextType = CreateContextType(contextTypeBuilder); } - public AvaloniaXamlIlCompiler(XamlIlTransformerConfiguration configuration, - IXamlIlType contextType) : this(configuration) + public AvaloniaXamlIlCompiler(TransformerConfiguration configuration, + XamlLanguageEmitMappings emitMappings, + IXamlType contextType) : this(configuration, emitMappings) { _contextType = contextType; } @@ -79,37 +97,41 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions set => _designTransformer.IsDesignMode = value; } - public void ParseAndCompile(string xaml, string baseUri, IFileSource fileSource, IXamlIlTypeBuilder tb, IXamlIlType overrideRootType) + public bool DefaultCompileBindings { - var parsed = XDocumentXamlIlParser.Parse(xaml, new Dictionary + get => _bindingTransformer.CompileBindingsByDefault; + set => _bindingTransformer.CompileBindingsByDefault = value; + } + + public void ParseAndCompile(string xaml, string baseUri, IFileSource fileSource, IXamlTypeBuilder tb, IXamlType overrideRootType) + { + var parsed = XDocumentXamlParser.Parse(xaml, new Dictionary { {XamlNamespaces.Blend2008, XamlNamespaces.Blend2008} }); - var rootObject = (XamlIlAstObjectNode)parsed.Root; + var rootObject = (XamlAstObjectNode)parsed.Root; var classDirective = rootObject.Children - .OfType().FirstOrDefault(x => + .OfType().FirstOrDefault(x => x.Namespace == XamlNamespaces.Xaml2006 && x.Name == "Class"); var rootType = classDirective != null ? - new XamlIlAstClrTypeReference(classDirective, - _configuration.TypeSystem.GetType(((XamlIlAstTextNode)classDirective.Values[0]).Text), + new XamlAstClrTypeReference(classDirective, + _configuration.TypeSystem.GetType(((XamlAstTextNode)classDirective.Values[0]).Text), false) : - XamlIlTypeReferenceResolver.ResolveType(CreateTransformationContext(parsed, true), - (XamlIlAstXmlTypeReference)rootObject.Type, true); + TypeReferenceResolver.ResolveType(CreateTransformationContext(parsed, true), + (XamlAstXmlTypeReference)rootObject.Type, true); if (overrideRootType != null) { - - if (!rootType.Type.IsAssignableFrom(overrideRootType)) - throw new XamlIlLoadException( + throw new XamlX.XamlLoadException( $"Unable to substitute {rootType.Type.GetFqn()} with {overrideRootType.GetFqn()}", rootObject); - rootType = new XamlIlAstClrTypeReference(rootObject, overrideRootType, false); + rootType = new XamlAstClrTypeReference(rootObject, overrideRootType, false); } OverrideRootType(parsed, rootType); @@ -119,17 +141,17 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } - public void OverrideRootType(XamlIlDocument doc, IXamlIlAstTypeReference newType) + public void OverrideRootType(XamlDocument doc, IXamlAstTypeReference newType) { - var root = (XamlIlAstObjectNode)doc.Root; + var root = (XamlAstObjectNode)doc.Root; var oldType = root.Type; if (oldType.Equals(newType)) return; root.Type = newType; - foreach (var child in root.Children.OfType()) + foreach (var child in root.Children.OfType()) { - if (child.Property is XamlIlAstNamePropertyReference prop) + if (child.Property is XamlAstNamePropertyReference prop) { if (prop.DeclaringType.Equals(oldType)) prop.DeclaringType = newType; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs new file mode 100644 index 0000000000..0c0dcb1634 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs @@ -0,0 +1,26 @@ +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions +{ + class AvaloniaXamlIlCompilerConfiguration : TransformerConfiguration + { + public XamlIlClrPropertyInfoEmitter ClrPropertyEmitter { get; } + public XamlIlPropertyInfoAccessorFactoryEmitter AccessorFactoryEmitter { get; } + + public AvaloniaXamlIlCompilerConfiguration(IXamlTypeSystem typeSystem, + IXamlAssembly defaultAssembly, + XamlLanguageTypeMappings typeMappings, + XamlXmlnsMappings xmlnsMappings, + XamlValueConverter customValueConverter, + XamlIlClrPropertyInfoEmitter clrPropertyEmitter, + XamlIlPropertyInfoAccessorFactoryEmitter accessorFactoryEmitter) + : base(typeSystem, defaultAssembly, typeMappings, xmlnsMappings, customValueConverter) + { + ClrPropertyEmitter = clrPropertyEmitter; + AccessorFactoryEmitter = accessorFactoryEmitter; + AddExtra(ClrPropertyEmitter); + AddExtra(AccessorFactoryEmitter); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs index a1fe6976b7..99ec3744bf 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -2,10 +2,12 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; -using XamlIl; -using XamlIl.Ast; -using XamlIl.Transform; -using XamlIl.TypeSystem; +using XamlX; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { @@ -17,12 +19,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions class AvaloniaXamlIlLanguage { - public static XamlIlLanguageTypeMappings Configure(IXamlIlTypeSystem typeSystem) + public static (XamlLanguageTypeMappings language, XamlLanguageEmitMappings emit) Configure(IXamlTypeSystem typeSystem) { var runtimeHelpers = typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.XamlIlRuntimeHelpers"); var assignBindingAttribute = typeSystem.GetType("Avalonia.Data.AssignBindingAttribute"); var bindingType = typeSystem.GetType("Avalonia.Data.IBinding"); - var rv = new XamlIlLanguageTypeMappings(typeSystem) + var rv = new XamlLanguageTypeMappings(typeSystem) { SupportInitialize = typeSystem.GetType("System.ComponentModel.ISupportInitialize"), XmlnsAttributes = @@ -51,18 +53,22 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions }, InnerServiceProviderFactoryMethod = runtimeHelpers.FindMethod(m => m.Name == "CreateInnerServiceProviderV1"), - ProvideValueTargetPropertyEmitter = XamlIlAvaloniaPropertyHelper.Emit, }; rv.CustomAttributeResolver = new AttributeResolver(typeSystem, rv); - rv.ContextTypeBuilderCallback = (b, c) => EmitNameScopeField(rv, typeSystem, b, c); - return rv; + + var emit = new XamlLanguageEmitMappings + { + ProvideValueTargetPropertyEmitter = XamlIlAvaloniaPropertyHelper.EmitProvideValueTarget, + ContextTypeBuilderCallback = (b, c) => EmitNameScopeField(rv, typeSystem, b, c) + }; + return (rv, emit); } public const string ContextNameScopeFieldName = "AvaloniaNameScope"; - private static void EmitNameScopeField(XamlIlLanguageTypeMappings mappings, - IXamlIlTypeSystem typeSystem, - IXamlIlTypeBuilder typebuilder, IXamlIlEmitter constructor) + private static void EmitNameScopeField(XamlLanguageTypeMappings mappings, + IXamlTypeSystem typeSystem, + IXamlTypeBuilder typebuilder, IXamlILEmitter constructor) { var nameScopeType = typeSystem.FindType("Avalonia.Controls.INameScope"); @@ -78,23 +84,23 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } - class AttributeResolver : IXamlIlCustomAttributeResolver + class AttributeResolver : IXamlCustomAttributeResolver { - private readonly IXamlIlType _typeConverterAttribute; + private readonly IXamlType _typeConverterAttribute; - private readonly List> _converters = - new List>(); + private readonly List> _converters = + new List>(); - private readonly IXamlIlType _avaloniaList; - private readonly IXamlIlType _avaloniaListConverter; + private readonly IXamlType _avaloniaList; + private readonly IXamlType _avaloniaListConverter; - public AttributeResolver(IXamlIlTypeSystem typeSystem, XamlIlLanguageTypeMappings mappings) + public AttributeResolver(IXamlTypeSystem typeSystem, XamlLanguageTypeMappings mappings) { _typeConverterAttribute = mappings.TypeConverterAttributes.First(); - void AddType(IXamlIlType type, IXamlIlType conv) - => _converters.Add(new KeyValuePair(type, conv)); + void AddType(IXamlType type, IXamlType conv) + => _converters.Add(new KeyValuePair(type, conv)); void Add(string type, string conv) => AddType(typeSystem.GetType(type), typeSystem.GetType(conv)); @@ -113,7 +119,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions _avaloniaListConverter = typeSystem.GetType("Avalonia.Collections.AvaloniaListConverter`1"); } - IXamlIlType LookupConverter(IXamlIlType type) + IXamlType LookupConverter(IXamlType type) { foreach(var p in _converters) if (p.Key.Equals(type)) @@ -123,15 +129,15 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return null; } - class ConstructedAttribute : IXamlIlCustomAttribute + class ConstructedAttribute : IXamlCustomAttribute { - public bool Equals(IXamlIlCustomAttribute other) => false; + public bool Equals(IXamlCustomAttribute other) => false; - public IXamlIlType Type { get; } + public IXamlType Type { get; } public List Parameters { get; } public Dictionary Properties { get; } - public ConstructedAttribute(IXamlIlType type, List parameters, Dictionary properties) + public ConstructedAttribute(IXamlType type, List parameters, Dictionary properties) { Type = type; Parameters = parameters ?? new List(); @@ -139,7 +145,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } } - public IXamlIlCustomAttribute GetCustomAttribute(IXamlIlType type, IXamlIlType attributeType) + public IXamlCustomAttribute GetCustomAttribute(IXamlType type, IXamlType attributeType) { if (attributeType.Equals(_typeConverterAttribute)) { @@ -151,25 +157,25 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return null; } - public IXamlIlCustomAttribute GetCustomAttribute(IXamlIlProperty property, IXamlIlType attributeType) + public IXamlCustomAttribute GetCustomAttribute(IXamlProperty property, IXamlType attributeType) { return null; } } - public static bool CustomValueConverter(XamlIlAstTransformationContext context, - IXamlIlAstValueNode node, IXamlIlType type, out IXamlIlAstValueNode result) + public static bool CustomValueConverter(AstTransformationContext context, + IXamlAstValueNode node, IXamlType type, out IXamlAstValueNode result) { if (type.FullName == "System.TimeSpan" - && node is XamlIlAstTextNode tn + && node is XamlAstTextNode tn && !tn.Text.Contains(":")) { var seconds = double.Parse(tn.Text, CultureInfo.InvariantCulture); - result = new XamlIlStaticOrTargetedReturnMethodCallNode(tn, + result = new XamlStaticOrTargetedReturnMethodCallNode(tn, type.FindMethod("FromSeconds", type, false, context.Configuration.WellKnownTypes.Double), new[] { - new XamlIlConstantNode(tn, context.Configuration.WellKnownTypes.Double, seconds) + new XamlConstantNode(tn, context.Configuration.WellKnownTypes.Double, seconds) }); return true; } @@ -178,9 +184,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { var scope = context.ParentNodes().OfType().FirstOrDefault(); if (scope == null) - throw new XamlIlLoadException("Unable to find the parent scope for AvaloniaProperty lookup", node); - if (!(node is XamlIlAstTextNode text)) - throw new XamlIlLoadException("Property should be a text node", node); + throw new XamlX.XamlLoadException("Unable to find the parent scope for AvaloniaProperty lookup", node); + if (!(node is XamlAstTextNode text)) + throw new XamlX.XamlLoadException("Property should be a text node", node); result = XamlIlAvaloniaPropertyHelper.CreateNode(context, text.Text, scope.TargetType, text); return true; } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs index 805b733feb..cb36851bab 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs @@ -1,139 +1,120 @@ using System; using System.Linq; -using XamlIl.Ast; -using XamlIl.Transform; -using XamlIl.TypeSystem; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.TypeSystem; +using XamlX.Emit; +using XamlX.IL; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { - class AddNameScopeRegistration : IXamlIlAstTransformer + class AddNameScopeRegistration : IXamlAstTransformer { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (node is XamlIlPropertyAssignmentNode pa - && pa.Property.Name == "Name" - && pa.Property.DeclaringType.FullName == "Avalonia.StyledElement") + if (node is XamlPropertyAssignmentNode pa) { - if (context.ParentNodes().FirstOrDefault() is XamlIlManipulationGroupNode mg - && mg.Children.OfType().Any()) - return node; - - IXamlIlAstValueNode value = null; - for (var c = 0; c < pa.Values.Count; c++) - if (pa.Values[c].Type.GetClrType().Equals(context.Configuration.WellKnownTypes.String)) - { - value = pa.Values[c]; - if (!(value is XamlIlAstTextNode)) + if (pa.Property.Name == "Name" + && pa.Property.DeclaringType.FullName == "Avalonia.StyledElement") + { + if (context.ParentNodes().FirstOrDefault() is XamlManipulationGroupNode mg + && mg.Children.OfType().Any()) + return node; + + IXamlAstValueNode value = null; + for (var c = 0; c < pa.Values.Count; c++) + if (pa.Values[c].Type.GetClrType().Equals(context.Configuration.WellKnownTypes.String)) { - var local = new XamlIlAstCompilerLocalNode(value); - // Wrap original in local initialization - pa.Values[c] = new XamlIlAstLocalInitializationNodeEmitter(value, value, local); - // Use local - value = local; - } + value = pa.Values[c]; + if (!(value is XamlAstTextNode)) + { + var local = new XamlAstCompilerLocalNode(value); + // Wrap original in local initialization + pa.Values[c] = new XamlAstLocalInitializationNodeEmitter(value, value, local); + // Use local + value = local; + } - break; - } + break; + } - if (value != null) - return new XamlIlManipulationGroupNode(pa) + if (value != null) { - Children = + var objectType = context.ParentNodes().OfType().FirstOrDefault()?.Type.GetClrType(); + return new XamlManipulationGroupNode(pa) { - pa, - new AvaloniaNameScopeRegistrationXamlIlNode(value, context.GetAvaloniaTypes()) - } - }; + Children = + { + pa, + new AvaloniaNameScopeRegistrationXamlIlNode(value, objectType) + } + }; + } + } + else if (pa.Property.CustomAttributes.Select(attr => attr.Type).Intersect(context.Configuration.TypeMappings.DeferredContentPropertyAttributes).Any()) + { + pa.Values[pa.Values.Count - 1] = + new NestedScopeMetadataNode(pa.Values[pa.Values.Count - 1]); + } } - if (!context.ParentNodes().Any() - && node is XamlIlValueWithManipulationNode mnode) - { - mnode.Manipulation = new XamlIlManipulationGroupNode(mnode, - new[] - { - mnode.Manipulation, - new HandleRootObjectScopeNode(mnode, context.GetAvaloniaTypes()) - }); - } return node; } + } - class HandleRootObjectScopeNode : XamlIlAstNode, IXamlIlAstManipulationNode, IXamlIlAstEmitableNode + class NestedScopeMetadataNode : XamlValueWithSideEffectNodeBase + { + public NestedScopeMetadataNode(IXamlAstValueNode value) : base(value, value) { - private readonly AvaloniaXamlIlWellKnownTypes _types; - - public HandleRootObjectScopeNode(IXamlIlLineInfo lineInfo, - AvaloniaXamlIlWellKnownTypes types) : base(lineInfo) - { - _types = types; - } - - public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) - { - var next = codeGen.DefineLabel(); - var scopeField = context.RuntimeContext.ContextType.Fields.First(f => - f.Name == AvaloniaXamlIlLanguage.ContextNameScopeFieldName); - using (var local = codeGen.LocalsPool.GetLocal(_types.StyledElement)) - { - codeGen - .Isinst(_types.StyledElement) - .Dup() - .Stloc(local.Local) - .Brfalse(next) - .Ldloc(local.Local) - .Ldloc(context.ContextLocal) - .Ldfld(scopeField) - .EmitCall(_types.NameScopeSetNameScope, true) - .MarkLabel(next) - .Ldloc(context.ContextLocal) - .Ldfld(scopeField) - .EmitCall(_types.INameScopeComplete, true); - } - - return XamlIlNodeEmitResult.Void(1); - - } } } - class AvaloniaNameScopeRegistrationXamlIlNode : XamlIlAstNode, IXamlIlAstManipulationNode, IXamlIlAstEmitableNode + class AvaloniaNameScopeRegistrationXamlIlNode : XamlAstNode, IXamlAstManipulationNode { - private readonly AvaloniaXamlIlWellKnownTypes _types; - public IXamlIlAstValueNode Name { get; set; } + public IXamlAstValueNode Name { get; set; } + public IXamlType TargetType { get; } - public AvaloniaNameScopeRegistrationXamlIlNode(IXamlIlAstValueNode name, AvaloniaXamlIlWellKnownTypes types) : base(name) + public AvaloniaNameScopeRegistrationXamlIlNode(IXamlAstValueNode name, IXamlType targetType) : base(name) { - _types = types; + TargetType = targetType; Name = name; } - public override void VisitChildren(IXamlIlAstVisitor visitor) - => Name = (IXamlIlAstValueNode)Name.Visit(visitor); + public override void VisitChildren(IXamlAstVisitor visitor) + => Name = (IXamlAstValueNode)Name.Visit(visitor); + } - public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + class AvaloniaNameScopeRegistrationXamlIlNodeEmitter : IXamlAstLocalsNodeEmitter + { + public XamlILNodeEmitResult Emit(IXamlAstNode node, XamlEmitContextWithLocals context, IXamlILEmitter codeGen) { - var scopeField = context.RuntimeContext.ContextType.Fields.First(f => - f.Name == AvaloniaXamlIlLanguage.ContextNameScopeFieldName); - - using (var targetLoc = context.GetLocal(context.Configuration.WellKnownTypes.Object)) + if (node is AvaloniaNameScopeRegistrationXamlIlNode registration) { - codeGen - // var target = {pop} - .Stloc(targetLoc.Local) - // _context.NameScope.Register(Name, target) - .Ldloc(context.ContextLocal) - .Ldfld(scopeField); - - context.Emit(Name, codeGen, Name.Type.GetClrType()); - - codeGen - .Ldloc(targetLoc.Local) - .EmitCall(_types.INameScopeRegister, true); - } + var scopeField = context.RuntimeContext.ContextType.Fields.First(f => + f.Name == AvaloniaXamlIlLanguage.ContextNameScopeFieldName); + + using (var targetLoc = context.GetLocalOfType(context.Configuration.WellKnownTypes.Object)) + { + + codeGen + // var target = {pop} + .Stloc(targetLoc.Local) + // _context.NameScope.Register(Name, target) + .Ldloc(context.ContextLocal) + .Ldfld(scopeField); - return XamlIlNodeEmitResult.Void(1); + context.Emit(registration.Name, codeGen, registration.Name.Type.GetClrType()); + + codeGen + .Ldloc(targetLoc.Local) + .EmitCall(context.GetAvaloniaTypes().INameScopeRegister, true); + } + + return XamlILNodeEmitResult.Void(1); + } + return default; } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaBindingExtensionHackTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaBindingExtensionHackTransformer.cs deleted file mode 100644 index c89106312f..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaBindingExtensionHackTransformer.cs +++ /dev/null @@ -1,20 +0,0 @@ -using XamlIl.Ast; -using XamlIl.Transform; - -namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers -{ - class AvaloniaBindingExtensionHackTransformer : IXamlIlAstTransformer - { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) - { - // Our code base expects XAML parser to prefer `FooExtension` to `Foo` even with `` syntax - // This is the legacy of Portable.Xaml, so we emulate that behavior here - - if (node is XamlIlAstXmlTypeReference tref - && tref.Name == "Binding" - && tref.XmlNamespace == "https://github.com/avaloniaui") - tref.IsMarkupExtension = true; - return node; - } - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaBindingExtensionTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaBindingExtensionTransformer.cs new file mode 100644 index 0000000000..de2c0eab96 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaBindingExtensionTransformer.cs @@ -0,0 +1,75 @@ +using System.Linq; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; + + +using XamlParseException = XamlX.XamlParseException; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaBindingExtensionTransformer : IXamlAstTransformer + { + public bool CompileBindingsByDefault { get; set; } + + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (context.ParentNodes().FirstOrDefault() is AvaloniaXamlIlCompileBindingsNode) + { + return node; + } + + if (node is XamlAstObjectNode obj) + { + foreach (var item in obj.Children) + { + if (item is XamlAstXmlDirective directive) + { + if (directive.Namespace == XamlNamespaces.Xaml2006 + && directive.Name == "CompileBindings" + && directive.Values.Count == 1) + { + if (!(directive.Values[0] is XamlAstTextNode text + && bool.TryParse(text.Text, out var compileBindings))) + { + throw new XamlParseException("The value of x:CompileBindings must be a literal boolean value.", directive.Values[0]); + } + + obj.Children.Remove(directive); + + return new AvaloniaXamlIlCompileBindingsNode(obj, compileBindings); + } + } + } + } + + // Convert the tag to either a CompiledBinding or ReflectionBinding tag. + + if (node is XamlAstXmlTypeReference tref + && tref.Name == "Binding" + && tref.XmlNamespace == "https://github.com/avaloniaui") + { + tref.IsMarkupExtension = true; + + var compileBindings = context.ParentNodes() + .OfType() + .FirstOrDefault() + ?.CompileBindings ?? CompileBindingsByDefault; + + tref.Name = compileBindings ? "CompiledBinding" : "ReflectionBinding"; + } + return node; + } + } + + internal class AvaloniaXamlIlCompileBindingsNode : XamlValueWithSideEffectNodeBase + { + public AvaloniaXamlIlCompileBindingsNode(IXamlAstValueNode value, bool compileBindings) + : base(value, value) + { + CompileBindings = compileBindings; + } + + public bool CompileBindings { get; } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlAvaloniaPropertyResolver.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlAvaloniaPropertyResolver.cs index 74f5c29f6a..9219a39ddc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlAvaloniaPropertyResolver.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlAvaloniaPropertyResolver.cs @@ -1,14 +1,14 @@ using System.Linq; -using XamlIl.Ast; -using XamlIl.Transform; +using XamlX.Ast; +using XamlX.Transform; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { - class AvaloniaXamlIlAvaloniaPropertyResolver : IXamlIlAstTransformer + class AvaloniaXamlIlAvaloniaPropertyResolver : IXamlAstTransformer { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (node is XamlIlAstClrProperty prop) + if (node is XamlAstClrProperty prop) { var n = prop.Name + "Property"; var field = diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs new file mode 100644 index 0000000000..7944d8b569 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs @@ -0,0 +1,332 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Avalonia.Markup.Parsers; +using Avalonia.Utilities; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; + +using XamlParseException = XamlX.XamlParseException; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlBindingPathParser : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is XamlAstObjectNode binding && binding.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)) + { + var convertedNode = ConvertLongFormPropertiesToBindingExpressionNode(context, binding); + + if (binding.Arguments.Count > 0 && binding.Arguments[0] is XamlAstTextNode bindingPathText) + { + var reader = new CharacterReader(bindingPathText.Text.AsSpan()); + var (nodes, _) = BindingExpressionGrammar.Parse(ref reader); + + if (convertedNode != null) + { + nodes.Insert(nodes.TakeWhile(x => x is BindingExpressionGrammar.ITransformNode).Count(), convertedNode); + } + + binding.Arguments[0] = new ParsedBindingPathNode(bindingPathText, context.GetAvaloniaTypes().CompiledBindingPath, nodes); + } + else + { + var bindingPathAssignment = binding.Children.OfType() + .FirstOrDefault(v => v.Property.GetClrProperty().Name == "Path"); + + if (bindingPathAssignment != null && bindingPathAssignment.Values[0] is XamlAstTextNode pathValue) + { + var reader = new CharacterReader(pathValue.Text.AsSpan()); + var (nodes, _) = BindingExpressionGrammar.Parse(ref reader); + + if (convertedNode != null) + { + nodes.Insert(nodes.TakeWhile(x => x is BindingExpressionGrammar.ITransformNode).Count(), convertedNode); + } + + bindingPathAssignment.Values[0] = new ParsedBindingPathNode(pathValue, context.GetAvaloniaTypes().CompiledBindingPath, nodes); + } + } + } + + return node; + } + + private static BindingExpressionGrammar.INode ConvertLongFormPropertiesToBindingExpressionNode( + AstTransformationContext context, + XamlAstObjectNode binding) + { + BindingExpressionGrammar.INode convertedNode = null; + + var syntheticCompiledBindingProperties = binding.Children.OfType() + .Where(v => v.Property is AvaloniaSyntheticCompiledBindingProperty) + .ToList(); + + var elementNameProperty = syntheticCompiledBindingProperties + .FirstOrDefault(v => + v.Property is AvaloniaSyntheticCompiledBindingProperty prop + && prop.Name == SyntheticCompiledBindingPropertyName.ElementName); + + var sourceProperty = syntheticCompiledBindingProperties + .FirstOrDefault(v => + v.Property is AvaloniaSyntheticCompiledBindingProperty prop + && prop.Name == SyntheticCompiledBindingPropertyName.Source); + + var relativeSourceProperty = syntheticCompiledBindingProperties + .FirstOrDefault(v => + v.Property is AvaloniaSyntheticCompiledBindingProperty prop + && prop.Name == SyntheticCompiledBindingPropertyName.RelativeSource); + + if (elementNameProperty?.Values[0] is XamlAstTextNode elementName) + { + convertedNode = new BindingExpressionGrammar.NameNode { Name = elementName.Text }; + } + else if (elementNameProperty != null) + { + throw new XamlParseException($"Invalid ElementName '{elementNameProperty.Values[0]}'.", elementNameProperty.Values[0]); + } + + if (sourceProperty?.Values[0] != null) + { + if (convertedNode != null) + { + throw new XamlParseException("Only one of ElementName, Source, or RelativeSource specified as a binding source. Only one property is allowed.", binding); + } + + convertedNode = new RawSourceBindingExpressionNode(sourceProperty?.Values[0]); + } + + if (GetRelativeSourceObjectFromAssignment( + context, + relativeSourceProperty, + out var relativeSourceObject)) + { + if (convertedNode != null) + { + throw new XamlParseException("Only one of ElementName, Source, or RelativeSource specified as a binding source. Only one property is allowed.", binding); + } + + var mode = relativeSourceObject.Children + .OfType() + .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Mode") + ?.Values[0] is XamlAstTextNode modeAssignedValue ? modeAssignedValue.Text : null; + if (relativeSourceObject.Arguments.Count == 0 && mode == null) + { + mode = "FindAncestor"; + } + + if (mode == "FindAncestor") + { + var ancestorLevel = relativeSourceObject.Children + .OfType() + .FirstOrDefault(x => x.Property.GetClrProperty().Name == "FindAncestor") + ?.Values[0] is XamlAstTextNode ancestorLevelText ? int.Parse(ancestorLevelText.Text) - 1 : 0; + + var treeType = relativeSourceObject.Children + .OfType() + .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Tree") + ?.Values[0] is XamlAstTextNode treeTypeValue ? treeTypeValue.Text : "Visual"; + + var ancestorTypeName = relativeSourceObject.Children + .OfType() + .FirstOrDefault(x => x.Property.GetClrProperty().Name == "AncestorType") + ?.Values[0] as XamlAstTextNode; + + IXamlType ancestorType = null; + if (ancestorTypeName is null) + { + if (treeType == "Visual") + { + throw new XamlParseException("AncestorType must be set for RelativeSourceMode.FindAncestor when searching the visual tree.", relativeSourceObject); + } + else if (treeType == "Logical") + { + var styledElementType = context.GetAvaloniaTypes().StyledElement; + ancestorType = context + .ParentNodes() + .OfType() + .Where(x => styledElementType.IsAssignableFrom(x.Type.GetClrType())) + .ElementAtOrDefault(ancestorLevel) + ?.Type.GetClrType(); + + if (ancestorType is null) + { + throw new XamlX.XamlParseException("Unable to resolve implicit ancestor type based on XAML tree.", relativeSourceObject); + } + } + } + else + { + ancestorType = TypeReferenceResolver.ResolveType( + context, + ancestorTypeName.Text, + false, + ancestorTypeName, + true).GetClrType(); + } + + if (treeType == "Visual") + { + convertedNode = new VisualAncestorBindingExpressionNode + { + Type = ancestorType, + Level = ancestorLevel + }; + } + else if (treeType == "Logical") + { + convertedNode = new LogicalAncestorBindingExpressionNode + { + Type = ancestorType, + Level = ancestorLevel + }; + } + else + { + throw new XamlParseException($"Unknown tree type '{treeType}'.", binding); + } + } + else if (mode == "DataContext") + { + convertedNode = null; + } + else if (mode == "Self") + { + convertedNode = new BindingExpressionGrammar.SelfNode(); + } + else if (mode == "TemplatedParent") + { + var parentType = context.ParentNodes().OfType() + .FirstOrDefault(x => + x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate) + ?.TargetType.GetClrType(); + + if (parentType is null) + { + throw new XamlParseException("A binding with a TemplatedParent RelativeSource has to be in a ControlTemplate.", binding); + } + + convertedNode = new TemplatedParentBindingExpressionNode { Type = parentType }; + } + else + { + throw new XamlParseException($"Unknown RelativeSource mode '{mode}'.", binding); + } + } + + if (elementNameProperty != null) + { + binding.Children.Remove(elementNameProperty); + } + if (sourceProperty != null) + { + binding.Children.Remove(sourceProperty); + } + if (relativeSourceProperty != null) + { + binding.Children.Remove(relativeSourceProperty); + } + + return convertedNode; + } + + private static bool GetRelativeSourceObjectFromAssignment( + AstTransformationContext context, + XamlAstXamlPropertyValueNode relativeSourceProperty, + out XamlAstObjectNode relativeSourceObject) + { + relativeSourceObject = null; + if (relativeSourceProperty is null) + { + return false; + } + + if (relativeSourceProperty.Values[0] is XamlMarkupExtensionNode me) + { + if (me.Type.GetClrType() != context.GetAvaloniaTypes().RelativeSource) + { + throw new XamlParseException($"Expected an object of type 'Avalonia.Data.RelativeSource'. Found a object of type '{me.Type.GetClrType().GetFqn()}'", me); + } + + relativeSourceObject = (XamlAstObjectNode)me.Value; + return true; + } + + if (relativeSourceProperty.Values[0] is XamlAstObjectNode on) + { + if (on.Type.GetClrType() != context.GetAvaloniaTypes().RelativeSource) + { + throw new XamlParseException($"Expected an object of type 'Avalonia.Data.RelativeSource'. Found a object of type '{on.Type.GetClrType().GetFqn()}'", on); + } + + relativeSourceObject = on; + return true; + } + + return false; + } + } + + class ParsedBindingPathNode : XamlAstNode, IXamlAstValueNode + { + public ParsedBindingPathNode(IXamlLineInfo lineInfo, IXamlType compiledBindingType, IList path) + : base(lineInfo) + { + Type = new XamlAstClrTypeReference(lineInfo, compiledBindingType, false); + Path = path; + } + + public IXamlAstTypeReference Type { get; } + + public IList Path { get; } + + public override void VisitChildren(IXamlAstVisitor visitor) + { + for (int i = 0; i < Path.Count; i++) + { + if (Path[i] is IXamlAstNode ast) + { + Path[i] = (BindingExpressionGrammar.INode)ast.Visit(visitor); + } + } + } + } + + class VisualAncestorBindingExpressionNode : BindingExpressionGrammar.INode + { + public IXamlType Type { get; set; } + public int Level { get; set; } + } + + class LogicalAncestorBindingExpressionNode : BindingExpressionGrammar.INode + { + public IXamlType Type { get; set; } + public int Level { get; set; } + } + + class TemplatedParentBindingExpressionNode : BindingExpressionGrammar.INode + { + public IXamlType Type { get; set; } + } + + class RawSourceBindingExpressionNode : XamlAstNode, BindingExpressionGrammar.INode + { + public RawSourceBindingExpressionNode(IXamlAstValueNode rawSource) + : base(rawSource) + { + RawSource = rawSource; + } + + public IXamlAstValueNode RawSource { get; private set; } + + public override void VisitChildren(IXamlAstVisitor visitor) + { + RawSource = (IXamlAstValueNode)RawSource.Visit(visitor); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs new file mode 100644 index 0000000000..fc32084687 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlBindingPathTransformer : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is XamlAstConstructableObjectNode binding && binding.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)) + { + IXamlType startType; + var parentDataContextNode = context.ParentNodes().OfType().FirstOrDefault(); + if (parentDataContextNode is null) + { + throw new XamlX.XamlParseException("Cannot parse a compiled binding without an explicit x:DataType directive to give a starting data type for bindings.", binding); + } + + startType = parentDataContextNode.DataContextType; + + XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, binding, startType); + } + + return node; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlCompiledBindingsMetadataRemover.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlCompiledBindingsMetadataRemover.cs new file mode 100644 index 0000000000..70bd15ea12 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlCompiledBindingsMetadataRemover.cs @@ -0,0 +1,24 @@ +using System.Linq; +using XamlX.Ast; +using XamlX.Transform; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlCompiledBindingsMetadataRemover : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + while (true) + { + if (node is NestedScopeMetadataNode nestedScope) + node = nestedScope.Value; + else if (node is AvaloniaXamlIlDataContextTypeMetadataNode dataContextType) + node = dataContextType.Value; + else if (node is AvaloniaXamlIlCompileBindingsNode compileBindings) + node = compileBindings.Value; + else + return node; + } + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs index 7e8f296b44..35e2624ff9 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs @@ -1,15 +1,17 @@ using System.Linq; -using XamlIl.Ast; -using XamlIl.Transform; -using XamlIl.TypeSystem; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { - class AvaloniaXamlIlConstructorServiceProviderTransformer : IXamlIlAstTransformer + class AvaloniaXamlIlConstructorServiceProviderTransformer : IXamlAstTransformer { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (node is XamlIlAstObjectNode on && on.Arguments.Count == 0) + if (node is XamlAstObjectNode on && on.Arguments.Count == 0) { var ctors = on.Type.GetClrType().Constructors; if (!ctors.Any(c => c.IsPublic && !c.IsStatic && c.Parameters.Count == 0)) @@ -27,20 +29,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers return node; } - class InjectServiceProviderNode : XamlIlAstNode, IXamlIlAstValueNode,IXamlIlAstNodeNeedsParentStack, - IXamlIlAstEmitableNode + class InjectServiceProviderNode : XamlAstNode, IXamlAstValueNode,IXamlAstNodeNeedsParentStack, + IXamlAstEmitableNode { - public InjectServiceProviderNode(IXamlIlType type, IXamlIlLineInfo lineInfo) : base(lineInfo) + public InjectServiceProviderNode(IXamlType type, IXamlLineInfo lineInfo) : base(lineInfo) { - Type = new XamlIlAstClrTypeReference(lineInfo, type, false); + Type = new XamlAstClrTypeReference(lineInfo, type, false); } - public IXamlIlAstTypeReference Type { get; } + public IXamlAstTypeReference Type { get; } public bool NeedsParentStack => true; - public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + public XamlILNodeEmitResult Emit(XamlEmitContext context, IXamlILEmitter codeGen) { codeGen.Ldloc(context.ContextLocal); - return XamlIlNodeEmitResult.Type(0, Type.GetClrType()); + return XamlILNodeEmitResult.Type(0, Type.GetClrType()); } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs index 40386924c3..f95d086bf6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs @@ -1,29 +1,29 @@ using System.Linq; -using XamlIl.Ast; -using XamlIl.Transform; -using XamlIl.TypeSystem; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { - class AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer : IXamlIlAstTransformer + class AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer : IXamlAstTransformer { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (!(node is XamlIlAstObjectNode on + if (!(node is XamlAstObjectNode on && on.Type.GetClrType().FullName == "Avalonia.Markup.Xaml.Templates.ControlTemplate")) return node; - var tt = on.Children.OfType().FirstOrDefault(ch => + var tt = on.Children.OfType().FirstOrDefault(ch => ch.Property.GetClrProperty().Name == "TargetType"); if (context.ParentNodes().FirstOrDefault() is AvaloniaXamlIlTargetTypeMetadataNode) // Deja vu. I've just been in this place before return node; - IXamlIlAstTypeReference targetType; + IXamlAstTypeReference targetType; var templatableBaseType = context.Configuration.TypeSystem.GetType("Avalonia.Controls.Control"); - if ((tt?.Values.FirstOrDefault() is XamlIlTypeExtensionNode tn)) + if ((tt?.Values.FirstOrDefault() is XamlTypeExtensionNode tn)) { targetType = tn.Value; } @@ -33,11 +33,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers .FirstOrDefault(); if (parentScope?.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style) targetType = parentScope.TargetType; - else if (context.ParentNodes().Skip(1).FirstOrDefault() is XamlIlAstObjectNode directParentNode + else if (context.ParentNodes().Skip(1).FirstOrDefault() is XamlAstObjectNode directParentNode && templatableBaseType.IsAssignableFrom(directParentNode.Type.GetClrType())) targetType = directParentNode.Type; else - targetType = new XamlIlAstClrTypeReference(node, + targetType = new XamlAstClrTypeReference(node, templatableBaseType, false); } @@ -48,9 +48,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } } - class AvaloniaXamlIlTargetTypeMetadataNode : XamlIlValueWithSideEffectNodeBase + class AvaloniaXamlIlTargetTypeMetadataNode : XamlValueWithSideEffectNodeBase { - public IXamlIlAstTypeReference TargetType { get; set; } + public IXamlAstTypeReference TargetType { get; set; } public ScopeTypes ScopeType { get; } public enum ScopeTypes @@ -60,7 +60,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers Transitions } - public AvaloniaXamlIlTargetTypeMetadataNode(IXamlIlAstValueNode value, IXamlIlAstTypeReference targetType, + public AvaloniaXamlIlTargetTypeMetadataNode(IXamlAstValueNode value, IXamlAstTypeReference targetType, ScopeTypes type) : base(value, value) { diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs new file mode 100644 index 0000000000..5a0d6bac8d --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using Avalonia.Markup.Parsers; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using Avalonia.Utilities; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlDataContextTypeTransformer : IXamlAstTransformer + { + private const string AvaloniaNs = "https://github.com/avaloniaui"; + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (context.ParentNodes().FirstOrDefault() is AvaloniaXamlIlDataContextTypeMetadataNode) + { + // We've already resolved the data context type for this node. + return node; + } + + if (node is XamlAstConstructableObjectNode on) + { + AvaloniaXamlIlDataContextTypeMetadataNode inferredDataContextTypeNode = null; + AvaloniaXamlIlDataContextTypeMetadataNode directiveDataContextTypeNode = null; + bool isDataTemplate = on.Type.GetClrType().Equals(context.GetAvaloniaTypes().DataTemplate); + + for (int i = 0; i < on.Children.Count; ++i) + { + var child = on.Children[i]; + if (child is XamlAstXmlDirective directive) + { + if (directive.Namespace == XamlNamespaces.Xaml2006 + && directive.Name == "DataType" + && directive.Values.Count == 1) + { + on.Children.RemoveAt(i); + i--; + if (directive.Values[0] is XamlAstTextNode text) + { + directiveDataContextTypeNode = new AvaloniaXamlIlDataContextTypeMetadataNode(on, + TypeReferenceResolver.ResolveType(context, text.Text, isMarkupExtension: false, text, strict: true).Type); + } + else + { + throw new XamlX.XamlParseException("x:DataType should be set to a type name.", directive.Values[0]); + } + } + } + else if (child is XamlPropertyAssignmentNode pa) + { + if (pa.Property.Name == "DataContext" + && pa.Property.DeclaringType.Equals(context.GetAvaloniaTypes().StyledElement) + && pa.Values[0] is XamlMarkupExtensionNode ext + && ext.Value is XamlAstConstructableObjectNode obj) + { + inferredDataContextTypeNode = ParseDataContext(context, on, obj); + } + else if(isDataTemplate + && pa.Property.Name == "DataType" + && pa.Values[0] is XamlTypeExtensionNode dataTypeNode) + { + inferredDataContextTypeNode = new AvaloniaXamlIlDataContextTypeMetadataNode(on, dataTypeNode.Value.GetClrType()); + } + } + } + + // If there is no x:DataType directive, + // do more specialized inference + if (directiveDataContextTypeNode is null) + { + if (isDataTemplate && inferredDataContextTypeNode is null) + { + // Infer data type from collection binding on a control that displays items. + var parentObject = context.ParentNodes().OfType().FirstOrDefault(); + if (parentObject != null && context.GetAvaloniaTypes().IItemsPresenterHost.IsDirectlyAssignableFrom(parentObject.Type.GetClrType())) + { + inferredDataContextTypeNode = InferDataContextOfPresentedItem(context, on, parentObject); + } + else + { + inferredDataContextTypeNode = new AvaloniaXamlIlUninferrableDataContextMetadataNode(on); + } + } + } + + return directiveDataContextTypeNode ?? inferredDataContextTypeNode ?? node; + } + + return node; + } + + private static AvaloniaXamlIlDataContextTypeMetadataNode InferDataContextOfPresentedItem(AstTransformationContext context, XamlAstConstructableObjectNode on, XamlAstConstructableObjectNode parentObject) + { + var parentItemsValue = parentObject + .Children.OfType() + .FirstOrDefault(pa => pa.Property.Name == "Items") + ?.Values[0]; + if (parentItemsValue is null) + { + // We can't infer the collection type and the currently calculated type is definitely wrong. + // Notify the user that we were unable to infer the data context type if they use a compiled binding. + return new AvaloniaXamlIlUninferrableDataContextMetadataNode(on); + } + + IXamlType itemsCollectionType = null; + if (context.GetAvaloniaTypes().IBinding.IsAssignableFrom(parentItemsValue.Type.GetClrType())) + { + if (parentItemsValue.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension) + && parentItemsValue is XamlMarkupExtensionNode ext && ext.Value is XamlAstConstructableObjectNode parentItemsBinding) + { + var parentItemsDataContext = context.ParentNodes().SkipWhile(n => n != parentObject).OfType().FirstOrDefault(); + if (parentItemsDataContext != null) + { + itemsCollectionType = XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, parentItemsBinding, parentItemsDataContext.DataContextType); + } + } + } + else + { + itemsCollectionType = parentItemsValue.Type.GetClrType(); + } + + if (itemsCollectionType != null) + { + var elementType = itemsCollectionType + .GetAllInterfaces() + .FirstOrDefault(i => + i.GenericTypeDefinition?.Equals(context.Configuration.WellKnownTypes.IEnumerableT) == true) + .GenericArguments[0]; + return new AvaloniaXamlIlDataContextTypeMetadataNode(on, elementType); + } + // We can't infer the collection type and the currently calculated type is definitely wrong. + // Notify the user that we were unable to infer the data context type if they use a compiled binding. + return new AvaloniaXamlIlUninferrableDataContextMetadataNode(on); + } + + private static AvaloniaXamlIlDataContextTypeMetadataNode ParseDataContext(AstTransformationContext context, XamlAstConstructableObjectNode on, XamlAstConstructableObjectNode obj) + { + var bindingType = context.GetAvaloniaTypes().IBinding; + if (!bindingType.IsAssignableFrom(obj.Type.GetClrType()) && !obj.Type.GetClrType().Equals(context.GetAvaloniaTypes().ReflectionBindingExtension)) + { + return new AvaloniaXamlIlDataContextTypeMetadataNode(on, obj.Type.GetClrType()); + } + else if (obj.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)) + { + IXamlType startType; + var parentDataContextNode = context.ParentNodes().OfType().FirstOrDefault(); + if (parentDataContextNode is null) + { + throw new XamlX.XamlParseException("Cannot parse a compiled binding without an explicit x:DataType directive to give a starting data type for bindings.", obj); + } + + startType = parentDataContextNode.DataContextType; + + var bindingResultType = XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, obj, startType); + return new AvaloniaXamlIlDataContextTypeMetadataNode(on, bindingResultType); + } + + return new AvaloniaXamlIlUninferrableDataContextMetadataNode(on); + } + } + + [DebuggerDisplay("DataType = {DataContextType}")] + class AvaloniaXamlIlDataContextTypeMetadataNode : XamlValueWithSideEffectNodeBase + { + public virtual IXamlType DataContextType { get; } + + public AvaloniaXamlIlDataContextTypeMetadataNode(IXamlAstValueNode value, IXamlType targetType) + : base(value, value) + { + DataContextType = targetType; + } + } + + [DebuggerDisplay("DataType = Unknown")] + class AvaloniaXamlIlUninferrableDataContextMetadataNode : AvaloniaXamlIlDataContextTypeMetadataNode + { + public AvaloniaXamlIlUninferrableDataContextMetadataNode(IXamlAstValueNode value) + : base(value, null) + { + } + + public override IXamlType DataContextType => throw new XamlTransformException("Unable to infer DataContext type for compiled bindings nested within this element.", Value); + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDesignPropertiesTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDesignPropertiesTransformer.cs index 9cc4c5cf11..3096be522f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDesignPropertiesTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDesignPropertiesTransformer.cs @@ -1,12 +1,12 @@ using System.Collections.Generic; using System.Linq; -using XamlIl; -using XamlIl.Ast; -using XamlIl.Transform; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { - class AvaloniaXamlIlDesignPropertiesTransformer : IXamlIlAstTransformer + class AvaloniaXamlIlDesignPropertiesTransformer : IXamlAstTransformer { public bool IsDesignMode { get; set; } @@ -17,14 +17,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers }; private const string AvaloniaNs = "https://github.com/avaloniaui"; - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (node is XamlIlAstObjectNode on) + if (node is XamlAstObjectNode on) { for (var c=0; c() + .FirstOrDefault(); + if(parentScope == null) + throw new XamlX.XamlParseException("No target type scope found for property path", text); + if (parentScope.ScopeType != AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style) + throw new XamlX.XamlParseException("PropertyPath is currently only valid for styles", pv); + + + IEnumerable parsed; + try + { + parsed = PropertyPathGrammar.Parse(text.Text); + } + catch (Exception e) + { + throw new XamlX.XamlParseException("Unable to parse PropertyPath: " + e.Message, text); + } + + var elements = new List(); + IXamlType currentType = parentScope.TargetType.GetClrType(); + + + var expectProperty = true; + var expectCast = true; + var expectTraversal = false; + var types = context.GetAvaloniaTypes(); + + IXamlType GetType(string ns, string name) + { + return TypeReferenceResolver.ResolveType(context, $"{ns}:{name}", false, + text, true).GetClrType(); + } + + void HandleProperty(string name, string typeNamespace, string typeName) + { + if(!expectProperty || currentType == null) + throw new XamlX.XamlParseException("Unexpected property node", text); + + var propertySearchType = + typeName != null ? GetType(typeNamespace, typeName) : currentType; + + IXamlIlPropertyPathElementNode prop = null; + var avaloniaPropertyFieldName = name + "Property"; + var avaloniaPropertyField = propertySearchType.GetAllFields().FirstOrDefault(f => + f.IsStatic && f.IsPublic && f.Name == avaloniaPropertyFieldName); + if (avaloniaPropertyField != null) + { + prop = new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyField, + XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyField, types, text)); + } + else + { + var clrProperty = propertySearchType.GetAllProperties().FirstOrDefault(p => p.Name == name); + prop = new XamlIClrPropertyPathElementNode(clrProperty); + } + + if (prop == null) + throw new XamlX.XamlParseException( + $"Unable to resolve property {name} on type {propertySearchType.GetFqn()}", + text); + + currentType = prop.Type; + elements.Add(prop); + expectProperty = false; + expectTraversal = expectCast = true; + } + + foreach (var ge in parsed) + { + if (ge is PropertyPathGrammar.ChildTraversalSyntax) + { + if (!expectTraversal) + throw new XamlX.XamlParseException("Unexpected child traversal .", text); + elements.Add(new XamlIlChildTraversalPropertyPathElementNode()); + expectTraversal = expectCast = false; + expectProperty = true; + } + else if (ge is PropertyPathGrammar.EnsureTypeSyntax ets) + { + if(!expectCast) + throw new XamlX.XamlParseException("Unexpected cast node", text); + currentType = GetType(ets.TypeNamespace, ets.TypeName); + elements.Add(new XamlIlCastPropertyPathElementNode(currentType, true)); + expectProperty = false; + expectCast = expectTraversal = true; + } + else if (ge is PropertyPathGrammar.CastTypeSyntax cts) + { + if(!expectCast) + throw new XamlX.XamlParseException("Unexpected cast node", text); + //TODO: Check if cast can be done + currentType = GetType(cts.TypeNamespace, cts.TypeName); + elements.Add(new XamlIlCastPropertyPathElementNode(currentType, false)); + expectProperty = false; + expectCast = expectTraversal = true; + } + else if (ge is PropertyPathGrammar.PropertySyntax ps) + { + HandleProperty(ps.Name, null, null); + } + else if (ge is PropertyPathGrammar.TypeQualifiedPropertySyntax tqps) + { + HandleProperty(tqps.Name, tqps.TypeNamespace, tqps.TypeName); + } + else + throw new XamlX.XamlParseException("Unexpected node " + ge, text); + + } + var propertyPathNode = new XamlIlPropertyPathNode(text, elements, types); + if (propertyPathNode.Type == null) + throw new XamlX.XamlParseException("Unexpected end of the property path", text); + pv.Values[0] = propertyPathNode; + } + + return node; + } + + interface IXamlIlPropertyPathElementNode + { + void Emit(XamlEmitContext context, IXamlILEmitter codeGen); + IXamlType Type { get; } + } + + class XamlIlChildTraversalPropertyPathElementNode : IXamlIlPropertyPathElementNode + { + public void Emit(XamlEmitContext context, IXamlILEmitter codeGen) + => codeGen.EmitCall( + context.GetAvaloniaTypes() + .PropertyPathBuilder.FindMethod(m => m.Name == "ChildTraversal")); + + public IXamlType Type => null; + } + + class XamlIlAvaloniaPropertyPropertyPathElementNode : IXamlIlPropertyPathElementNode + { + private readonly IXamlField _field; + + public XamlIlAvaloniaPropertyPropertyPathElementNode(IXamlField field, IXamlType propertyType) + { + _field = field; + Type = propertyType; + } + + public void Emit(XamlEmitContext context, IXamlILEmitter codeGen) + => codeGen + .Ldsfld(_field) + .EmitCall(context.GetAvaloniaTypes() + .PropertyPathBuilder.FindMethod(m => m.Name == "Property")); + + public IXamlType Type { get; } + } + + class XamlIClrPropertyPathElementNode : IXamlIlPropertyPathElementNode + { + private readonly IXamlProperty _property; + + public XamlIClrPropertyPathElementNode(IXamlProperty property) + { + _property = property; + } + + public void Emit(XamlEmitContext context, IXamlILEmitter codeGen) + { + context.Configuration.GetExtra() + .Emit(context, codeGen, _property); + + codeGen.EmitCall(context.GetAvaloniaTypes() + .PropertyPathBuilder.FindMethod(m => m.Name == "Property")); + } + + public IXamlType Type => _property.Getter?.ReturnType ?? _property.Setter?.Parameters[0]; + } + + class XamlIlCastPropertyPathElementNode : IXamlIlPropertyPathElementNode + { + private readonly IXamlType _type; + private readonly bool _ensureType; + + public XamlIlCastPropertyPathElementNode(IXamlType type, bool ensureType) + { + _type = type; + _ensureType = ensureType; + } + + public void Emit(XamlEmitContext context, IXamlILEmitter codeGen) + { + codeGen + .Ldtype(_type) + .EmitCall(context.GetAvaloniaTypes() + .PropertyPathBuilder.FindMethod(m => m.Name == (_ensureType ? "EnsureType" : "Cast"))); + } + + public IXamlType Type => _type; + } + + class XamlIlPropertyPathNode : XamlAstNode, IXamlIlPropertyPathNode, IXamlAstEmitableNode + { + private readonly List _elements; + private readonly AvaloniaXamlIlWellKnownTypes _types; + + public XamlIlPropertyPathNode(IXamlLineInfo lineInfo, + List elements, + AvaloniaXamlIlWellKnownTypes types) : base(lineInfo) + { + _elements = elements; + _types = types; + Type = new XamlAstClrTypeReference(this, types.PropertyPath, false); + } + + public IXamlAstTypeReference Type { get; } + public IXamlType PropertyType => _elements.LastOrDefault()?.Type; + public XamlILNodeEmitResult Emit(XamlEmitContext context, IXamlILEmitter codeGen) + { + codeGen + .Newobj(_types.PropertyPathBuilder.FindConstructor()); + foreach(var e in _elements) + e.Emit(context, codeGen); + codeGen.EmitCall(_types.PropertyPathBuilder.FindMethod(m => m.Name == "Build")); + return XamlILNodeEmitResult.Type(0, _types.PropertyPath); + } + } + } + + interface IXamlIlPropertyPathNode : IXamlAstValueNode + { + IXamlType PropertyType { get; } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlRootObjectScopeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlRootObjectScopeTransformer.cs new file mode 100644 index 0000000000..0513c78a7a --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlRootObjectScopeTransformer.cs @@ -0,0 +1,72 @@ +using System; +using System.Linq; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.TypeSystem; +using XamlX.IL; +using XamlX.Emit; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlRootObjectScope : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (!context.ParentNodes().Any() + && node is XamlValueWithManipulationNode mnode) + { + mnode.Manipulation = new XamlManipulationGroupNode(mnode, + new[] + { + mnode.Manipulation, + new HandleRootObjectScopeNode(mnode, context.GetAvaloniaTypes()) + }); + } + return node; + } + class HandleRootObjectScopeNode : XamlAstNode, IXamlAstManipulationNode + { + private readonly AvaloniaXamlIlWellKnownTypes _types; + + public HandleRootObjectScopeNode(IXamlLineInfo lineInfo, + AvaloniaXamlIlWellKnownTypes types) : base(lineInfo) + { + _types = types; + } + } + internal class Emitter : IXamlILAstNodeEmitter + { + public XamlILNodeEmitResult Emit(IXamlAstNode node, XamlEmitContext context, IXamlILEmitter codeGen) + { + if (!(node is HandleRootObjectScopeNode)) + { + return null; + } + var types = context.GetAvaloniaTypes(); + + var next = codeGen.DefineLabel(); + var scopeField = context.RuntimeContext.ContextType.Fields.First(f => + f.Name == AvaloniaXamlIlLanguage.ContextNameScopeFieldName); + using (var local = codeGen.LocalsPool.GetLocal(types.StyledElement)) + { + codeGen + .Isinst(types.StyledElement) + .Dup() + .Stloc(local.Local) + .Brfalse(next) + .Ldloc(local.Local) + .Ldloc(context.ContextLocal) + .Ldfld(scopeField) + .EmitCall(types.NameScopeSetNameScope, true) + .MarkLabel(next) + .Ldloc(context.ContextLocal) + .Ldfld(scopeField) + .EmitCall(types.INameScopeComplete, true); + } + + return XamlILNodeEmitResult.Void(1); + } + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs index d5114244cf..b81d25d613 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs @@ -3,41 +3,45 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using Avalonia.Markup.Parsers; -using XamlIl; -using XamlIl.Ast; -using XamlIl.Transform; -using XamlIl.Transform.Transformers; -using XamlIl.TypeSystem; +using XamlX; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { - class AvaloniaXamlIlSelectorTransformer : IXamlIlAstTransformer + using XamlParseException = XamlX.XamlParseException; + using XamlLoadException = XamlX.XamlLoadException; + class AvaloniaXamlIlSelectorTransformer : IXamlAstTransformer { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (!(node is XamlIlAstObjectNode on && on.Type.GetClrType().FullName == "Avalonia.Styling.Style")) + if (!(node is XamlAstObjectNode on && on.Type.GetClrType().FullName == "Avalonia.Styling.Style")) return node; - var pn = on.Children.OfType() + var pn = on.Children.OfType() .FirstOrDefault(p => p.Property.GetClrProperty().Name == "Selector"); if (pn == null) return node; if (pn.Values.Count != 1) - throw new XamlIlParseException("Selector property should should have exactly one value", node); + throw new XamlParseException("Selector property should should have exactly one value", node); if (pn.Values[0] is XamlIlSelectorNode) //Deja vu. I've just been in this place before return node; - if (!(pn.Values[0] is XamlIlAstTextNode tn)) - throw new XamlIlParseException("Selector property should be a text node", node); + if (!(pn.Values[0] is XamlAstTextNode tn)) + throw new XamlParseException("Selector property should be a text node", node); var selectorType = pn.Property.GetClrProperty().Getter.ReturnType; var initialNode = new XamlIlSelectorInitialNode(node, selectorType); XamlIlSelectorNode Create(IEnumerable syntax, - Func typeResolver) + Func typeResolver) { XamlIlSelectorNode result = initialNode; XamlIlOrSelectorNode results = null; @@ -63,18 +67,18 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers var type = result?.TargetType; if (type == null) - throw new XamlIlParseException("Property selectors must be applied to a type.", node); + throw new XamlParseException("Property selectors must be applied to a type.", node); var targetProperty = type.GetAllProperties().FirstOrDefault(p => p.Name == property.Property); if (targetProperty == null) - throw new XamlIlParseException($"Cannot find '{property.Property}' on '{type}", node); + throw new XamlParseException($"Cannot find '{property.Property}' on '{type}", node); - if (!XamlIlTransformHelpers.TryGetCorrectlyTypedValue(context, - new XamlIlAstTextNode(node, property.Value, context.Configuration.WellKnownTypes.String), + if (!XamlTransformHelpers.TryGetCorrectlyTypedValue(context, + new XamlAstTextNode(node, property.Value, context.Configuration.WellKnownTypes.String), targetProperty.PropertyType, out var typedValue)) - throw new XamlIlParseException( + throw new XamlParseException( $"Cannot convert '{property.Value}' to '{targetProperty.PropertyType.GetFqn()}", node); @@ -100,7 +104,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers result = initialNode; break; default: - throw new XamlIlParseException($"Unsupported selector grammar '{i.GetType()}'.", node); + throw new XamlParseException($"Unsupported selector grammar '{i.GetType()}'.", node); } } @@ -119,15 +123,15 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } catch (Exception e) { - throw new XamlIlParseException("Unable to parse selector: " + e.Message, node); + throw new XamlParseException("Unable to parse selector: " + e.Message, node); } var selector = Create(parsed, (p, n) - => XamlIlTypeReferenceResolver.ResolveType(context, $"{p}:{n}", true, node, true)); + => TypeReferenceResolver.ResolveType(context, $"{p}:{n}", true, node, true)); pn.Values[0] = selector; return new AvaloniaXamlIlTargetTypeMetadataNode(on, - new XamlIlAstClrTypeReference(selector, selector.TargetType, false), + new XamlAstClrTypeReference(selector, selector.TargetType, false), AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style); } @@ -135,32 +139,32 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers - abstract class XamlIlSelectorNode : XamlIlAstNode, IXamlIlAstValueNode, IXamlIlAstEmitableNode + abstract class XamlIlSelectorNode : XamlAstNode, IXamlAstValueNode, IXamlAstEmitableNode { protected XamlIlSelectorNode Previous { get; } - public abstract IXamlIlType TargetType { get; } + public abstract IXamlType TargetType { get; } public XamlIlSelectorNode(XamlIlSelectorNode previous, - IXamlIlLineInfo info = null, - IXamlIlType selectorType = null) : base(info ?? previous) + IXamlLineInfo info = null, + IXamlType selectorType = null) : base(info ?? previous) { Previous = previous; - Type = selectorType == null ? previous.Type : new XamlIlAstClrTypeReference(this, selectorType, false); + Type = selectorType == null ? previous.Type : new XamlAstClrTypeReference(this, selectorType, false); } - public IXamlIlAstTypeReference Type { get; } + public IXamlAstTypeReference Type { get; } - public virtual XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + public virtual XamlILNodeEmitResult Emit(XamlEmitContext context, IXamlILEmitter codeGen) { if (Previous != null) context.Emit(Previous, codeGen, Type.GetClrType()); DoEmit(context, codeGen); - return XamlIlNodeEmitResult.Type(0, Type.GetClrType()); + return XamlILNodeEmitResult.Type(0, Type.GetClrType()); } - protected abstract void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen); + protected abstract void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen); - protected void EmitCall(XamlIlEmitContext context, IXamlIlEmitter codeGen, Func method) + protected void EmitCall(XamlEmitContext context, IXamlILEmitter codeGen, Func method) { var selectors = context.Configuration.TypeSystem.GetType("Avalonia.Styling.Selectors"); var found = selectors.FindMethod(m => m.IsStatic && m.Parameters.Count > 0 && method(m)); @@ -170,27 +174,27 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers class XamlIlSelectorInitialNode : XamlIlSelectorNode { - public XamlIlSelectorInitialNode(IXamlIlLineInfo info, - IXamlIlType selectorType) : base(null, info, selectorType) + public XamlIlSelectorInitialNode(IXamlLineInfo info, + IXamlType selectorType) : base(null, info, selectorType) { } - public override IXamlIlType TargetType => null; - protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen) => codeGen.Ldnull(); + public override IXamlType TargetType => null; + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) => codeGen.Ldnull(); } class XamlIlTypeSelector : XamlIlSelectorNode { public bool Concrete { get; } - public XamlIlTypeSelector(XamlIlSelectorNode previous, IXamlIlType type, bool concrete) : base(previous) + public XamlIlTypeSelector(XamlIlSelectorNode previous, IXamlType type, bool concrete) : base(previous) { TargetType = type; Concrete = concrete; } - public override IXamlIlType TargetType { get; } - protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + public override IXamlType TargetType { get; } + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) { var name = Concrete ? "OfType" : "Is"; codeGen.Ldtype(TargetType); @@ -217,8 +221,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } - public override IXamlIlType TargetType => Previous?.TargetType; - protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + public override IXamlType TargetType => Previous?.TargetType; + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) { codeGen.Ldstr(String); var name = _type.ToString(); @@ -242,8 +246,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers _type = type; } - public override IXamlIlType TargetType => null; - protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + public override IXamlType TargetType => null; + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) { var name = _type.ToString(); EmitCall(context, codeGen, @@ -260,8 +264,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers Argument = argument; } - public override IXamlIlType TargetType => Previous?.TargetType; - protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + public override IXamlType TargetType => Previous?.TargetType; + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) { context.Emit(Argument, codeGen, Type.GetClrType()); EmitCall(context, codeGen, @@ -272,22 +276,22 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers class XamlIlPropertyEqualsSelector : XamlIlSelectorNode { public XamlIlPropertyEqualsSelector(XamlIlSelectorNode previous, - IXamlIlProperty property, - IXamlIlAstValueNode value) + IXamlProperty property, + IXamlAstValueNode value) : base(previous) { Property = property; Value = value; } - public IXamlIlProperty Property { get; set; } - public IXamlIlAstValueNode Value { get; set; } + public IXamlProperty Property { get; set; } + public IXamlAstValueNode Value { get; set; } - public override IXamlIlType TargetType => Previous?.TargetType; - protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + public override IXamlType TargetType => Previous?.TargetType; + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) { if (!XamlIlAvaloniaPropertyHelper.Emit(context, codeGen, Property)) - throw new XamlIlLoadException( + throw new XamlLoadException( $"{Property.Name} of {(Property.Setter ?? Property.Getter).DeclaringType.GetFqn()} doesn't seem to be an AvaloniaProperty", this); context.Emit(Value, codeGen, context.Configuration.WellKnownTypes.Object); @@ -302,7 +306,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers class XamlIlOrSelectorNode : XamlIlSelectorNode { List _selectors = new List(); - public XamlIlOrSelectorNode(IXamlIlLineInfo info, IXamlIlType selectorType) : base(null, info, selectorType) + public XamlIlOrSelectorNode(IXamlLineInfo info, IXamlType selectorType) : base(null, info, selectorType) { } @@ -311,11 +315,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers _selectors.Add(node); } - public override IXamlIlType TargetType + public override IXamlType TargetType { get { - IXamlIlType result = null; + IXamlType result = null; foreach (var selector in _selectors) { @@ -340,10 +344,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } } - protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) { if (_selectors.Count == 0) - throw new XamlIlLoadException("Invalid selector count", this); + throw new XamlLoadException("Invalid selector count", this); if (_selectors.Count == 1) { _selectors[0].Emit(context, codeGen); diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs index 629e2562d3..e816265422 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs @@ -1,64 +1,79 @@ using System; using System.Collections.Generic; using System.Linq; -using XamlIl; -using XamlIl.Ast; -using XamlIl.Transform; -using XamlIl.Transform.Transformers; -using XamlIl.TypeSystem; +using Avalonia.Data.Core; +using XamlX; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { - class AvaloniaXamlIlSetterTransformer : IXamlIlAstTransformer + using XamlParseException = XamlX.XamlParseException; + using XamlLoadException = XamlX.XamlLoadException; + class AvaloniaXamlIlSetterTransformer : IXamlAstTransformer { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (!(node is XamlIlAstObjectNode on + if (!(node is XamlAstObjectNode on && on.Type.GetClrType().FullName == "Avalonia.Styling.Setter")) return node; - var parent = context.ParentNodes().OfType() + var parent = context.ParentNodes().OfType() .FirstOrDefault(p => p.Type.GetClrType().FullName == "Avalonia.Styling.Style"); if (parent == null) - throw new XamlIlParseException( + throw new XamlParseException( "Avalonia.Styling.Setter is only valid inside Avalonia.Styling.Style", node); - var selectorProperty = parent.Children.OfType() + var selectorProperty = parent.Children.OfType() .FirstOrDefault(p => p.Property.GetClrProperty().Name == "Selector"); if (selectorProperty == null) - throw new XamlIlParseException( + throw new XamlParseException( "Can not find parent Style Selector", node); var selector = selectorProperty.Values.FirstOrDefault() as XamlIlSelectorNode; if (selector?.TargetType == null) - throw new XamlIlParseException( + throw new XamlParseException( "Can not resolve parent Style Selector type", node); - - var property = @on.Children.OfType() + IXamlType propType = null; + var property = @on.Children.OfType() .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Property"); - if (property == null) - throw new XamlIlParseException("Setter without a property is not valid", node); + if (property != null) + { - var propertyName = property.Values.OfType().FirstOrDefault()?.Text; - if (propertyName == null) - throw new XamlIlParseException("Setter.Property must be a string", node); + var propertyName = property.Values.OfType().FirstOrDefault()?.Text; + if (propertyName == null) + throw new XamlParseException("Setter.Property must be a string", node); - var avaloniaPropertyNode = XamlIlAvaloniaPropertyHelper.CreateNode(context, propertyName, - new XamlIlAstClrTypeReference(selector, selector.TargetType, false), property.Values[0]); - property.Values = new List + var avaloniaPropertyNode = XamlIlAvaloniaPropertyHelper.CreateNode(context, propertyName, + new XamlAstClrTypeReference(selector, selector.TargetType, false), property.Values[0]); + property.Values = new List {avaloniaPropertyNode}; + propType = avaloniaPropertyNode.AvaloniaPropertyType; + } + else { - avaloniaPropertyNode - }; + var propertyPath = on.Children.OfType() + .FirstOrDefault(x => x.Property.GetClrProperty().Name == "PropertyPath"); + if (propertyPath == null) + throw new XamlX.XamlParseException("Setter without a property or property path is not valid", node); + if (propertyPath.Values[0] is IXamlIlPropertyPathNode ppn + && ppn.PropertyType != null) + propType = ppn.PropertyType; + else + throw new XamlX.XamlParseException("Unable to get the property path property type", node); + } var valueProperty = on.Children - .OfType().FirstOrDefault(p => p.Property.GetClrProperty().Name == "Value"); - if (valueProperty?.Values?.Count == 1 && valueProperty.Values[0] is XamlIlAstTextNode) + .OfType().FirstOrDefault(p => p.Property.GetClrProperty().Name == "Value"); + if (valueProperty?.Values?.Count == 1 && valueProperty.Values[0] is XamlAstTextNode) { - var propType = avaloniaPropertyNode.AvaloniaPropertyType; - if (!XamlIlTransformHelpers.TryGetCorrectlyTypedValue(context, valueProperty.Values[0], + if (!XamlTransformHelpers.TryGetCorrectlyTypedValue(context, valueProperty.Values[0], propType, out var converted)) - throw new XamlIlParseException( + throw new XamlParseException( $"Unable to convert property value to {propType.GetFqn()}", valueProperty.Values[0]); @@ -69,9 +84,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers return node; } - class SetterValueProperty : XamlIlAstClrProperty + class SetterValueProperty : XamlAstClrProperty { - public SetterValueProperty(IXamlIlLineInfo line, IXamlIlType setterType, IXamlIlType targetType, + public SetterValueProperty(IXamlLineInfo line, IXamlType setterType, IXamlType targetType, AvaloniaXamlIlWellKnownTypes types) : base(line, "Value", setterType, null) { @@ -82,21 +97,21 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers Setters.Add(new XamlIlDirectCallPropertySetter(method, targetType)); } - class XamlIlDirectCallPropertySetter : IXamlIlPropertySetter + class XamlIlDirectCallPropertySetter : IXamlPropertySetter, IXamlEmitablePropertySetter { - private readonly IXamlIlMethod _method; - private readonly IXamlIlType _type; - public IXamlIlType TargetType { get; } + private readonly IXamlMethod _method; + private readonly IXamlType _type; + public IXamlType TargetType { get; } public PropertySetterBinderParameters BinderParameters { get; } = new PropertySetterBinderParameters(); - public IReadOnlyList Parameters { get; } - public void Emit(IXamlIlEmitter codegen) + public IReadOnlyList Parameters { get; } + public void Emit(IXamlILEmitter codegen) { if (_type.IsValueType) codegen.Box(_type); codegen.EmitCall(_method, true); } - public XamlIlDirectCallPropertySetter(IXamlIlMethod method, IXamlIlType type) + public XamlIlDirectCallPropertySetter(IXamlMethod method, IXamlType type) { _method = method; _type = type; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs index 548f0161d6..8e194e9385 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs @@ -1,20 +1,22 @@ using System.Collections.Generic; using System.Linq; -using XamlIl; -using XamlIl.Ast; -using XamlIl.Transform; -using XamlIl.TypeSystem; +using XamlX; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { - class AvaloniaXamlIlTransformInstanceAttachedProperties : IXamlIlAstTransformer + class AvaloniaXamlIlTransformInstanceAttachedProperties : IXamlAstTransformer { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (node is XamlIlAstNamePropertyReference prop - && prop.TargetType is XamlIlAstClrTypeReference targetRef - && prop.DeclaringType is XamlIlAstClrTypeReference declaringRef) + if (node is XamlAstNamePropertyReference prop + && prop.TargetType is XamlAstClrTypeReference targetRef + && prop.DeclaringType is XamlAstClrTypeReference declaringRef) { // Target and declared type aren't assignable but both inherit from AvaloniaObject var avaloniaObject = context.Configuration.TypeSystem.FindType("Avalonia.AvaloniaObject"); @@ -70,21 +72,21 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers return node; } - class AvaloniaAttachedInstanceProperty : XamlIlAstClrProperty, IXamlIlAvaloniaProperty + class AvaloniaAttachedInstanceProperty : XamlAstClrProperty, IXamlIlAvaloniaProperty { - private readonly XamlIlTransformerConfiguration _config; - private readonly IXamlIlType _declaringType; - private readonly IXamlIlType _avaloniaPropertyType; - private readonly IXamlIlType _avaloniaObject; - private readonly IXamlIlField _field; - - public AvaloniaAttachedInstanceProperty(XamlIlAstNamePropertyReference prop, - XamlIlTransformerConfiguration config, - IXamlIlType declaringType, - IXamlIlType type, - IXamlIlType avaloniaPropertyType, - IXamlIlType avaloniaObject, - IXamlIlField field) : base(prop, prop.Name, + private readonly TransformerConfiguration _config; + private readonly IXamlType _declaringType; + private readonly IXamlType _avaloniaPropertyType; + private readonly IXamlType _avaloniaObject; + private readonly IXamlField _field; + + public AvaloniaAttachedInstanceProperty(XamlAstNamePropertyReference prop, + TransformerConfiguration config, + IXamlType declaringType, + IXamlType type, + IXamlType avaloniaPropertyType, + IXamlType avaloniaObject, + IXamlField field) : base(prop, prop.Name, declaringType, null) @@ -104,11 +106,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers Getter = new GetterMethod(this); } - public IXamlIlType PropertyType { get; } + public IXamlType PropertyType { get; } - public IXamlIlField AvaloniaProperty => _field; + public IXamlField AvaloniaProperty => _field; - class SetterMethod : IXamlIlPropertySetter + class SetterMethod : IXamlPropertySetter, IXamlEmitablePropertySetter { private readonly AvaloniaAttachedInstanceProperty _parent; @@ -118,10 +120,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers Parameters = new[] {_parent._avaloniaObject, _parent.PropertyType}; } - public IXamlIlType TargetType => _parent.DeclaringType; + public IXamlType TargetType => _parent.DeclaringType; public PropertySetterBinderParameters BinderParameters { get; } = new PropertySetterBinderParameters(); - public IReadOnlyList Parameters { get; } - public void Emit(IXamlIlEmitter emitter) + public IReadOnlyList Parameters { get; } + public void Emit(IXamlILEmitter emitter) { var so = _parent._config.WellKnownTypes.Object; var method = _parent._avaloniaObject @@ -133,7 +135,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers && m.Parameters[2].IsEnum ); if (method == null) - throw new XamlIlTypeSystemException( + throw new XamlTypeSystemException( "Unable to find SetValue(AvaloniaProperty, object, BindingPriority) on AvaloniaObject"); using (var loc = emitter.LocalsPool.GetLocal(_parent.PropertyType)) emitter @@ -150,7 +152,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } } - class GetterMethod : IXamlIlCustomEmitMethod + class GetterMethod : IXamlCustomEmitMethod { public GetterMethod(AvaloniaAttachedInstanceProperty parent) { @@ -163,16 +165,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public bool IsPublic => true; public bool IsStatic => true; public string Name { get; protected set; } - public IXamlIlType DeclaringType { get; } - public IXamlIlMethod MakeGenericMethod(IReadOnlyList typeArguments) + public IXamlType DeclaringType { get; } + public IXamlMethod MakeGenericMethod(IReadOnlyList typeArguments) => throw new System.NotSupportedException(); - public bool Equals(IXamlIlMethod other) => + public bool Equals(IXamlMethod other) => other is GetterMethod m && m.Name == Name && m.DeclaringType.Equals(DeclaringType); - public IXamlIlType ReturnType => Parent.PropertyType; - public IReadOnlyList Parameters { get; } - public void EmitCall(IXamlIlEmitter emitter) + public IXamlType ReturnType => Parent.PropertyType; + public IReadOnlyList Parameters { get; } + public void EmitCall(IXamlILEmitter emitter) { var method = Parent._avaloniaObject .FindMethod(m => m.IsPublic && !m.IsStatic && m.Name == "GetValue" @@ -180,7 +182,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers m.Parameters.Count == 1 && m.Parameters[0].Equals(Parent._avaloniaPropertyType)); if (method == null) - throw new XamlIlTypeSystemException( + throw new XamlTypeSystemException( "Unable to find T GetValue(AvaloniaProperty) on AvaloniaObject"); emitter .Ldsfld(Parent._field) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs new file mode 100644 index 0000000000..154c6a235c --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Text; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlTransformSyntheticCompiledBindingMembers : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is XamlAstNamePropertyReference prop + && prop.TargetType is XamlAstClrTypeReference targetRef + && targetRef.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)) + { + if (prop.Name == "ElementName") + { + return new AvaloniaSyntheticCompiledBindingProperty(node, + SyntheticCompiledBindingPropertyName.ElementName); + } + else if (prop.Name == "RelativeSource") + { + return new AvaloniaSyntheticCompiledBindingProperty(node, + SyntheticCompiledBindingPropertyName.RelativeSource); + } + else if (prop.Name == "Source") + { + return new AvaloniaSyntheticCompiledBindingProperty(node, + SyntheticCompiledBindingPropertyName.Source); + } + } + + return node; + } + } + + enum SyntheticCompiledBindingPropertyName + { + ElementName, + RelativeSource, + Source + } + + class AvaloniaSyntheticCompiledBindingProperty : XamlAstNode, IXamlAstPropertyReference + { + public SyntheticCompiledBindingPropertyName Name { get; } + + public AvaloniaSyntheticCompiledBindingProperty( + IXamlLineInfo lineInfo, + SyntheticCompiledBindingPropertyName name) + : base(lineInfo) + { + Name = name; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransitionsTypeMetadataTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransitionsTypeMetadataTransformer.cs index c4136f4824..4be2fc6f60 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransitionsTypeMetadataTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransitionsTypeMetadataTransformer.cs @@ -1,17 +1,17 @@ -using XamlIl.Ast; -using XamlIl.Transform; +using XamlX.Ast; +using XamlX.Transform; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { - class AvaloniaXamlIlTransitionsTypeMetadataTransformer : IXamlIlAstTransformer + class AvaloniaXamlIlTransitionsTypeMetadataTransformer : IXamlAstTransformer { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (node is XamlIlAstObjectNode on) + if (node is XamlAstObjectNode on) { foreach (var ch in on.Children) { - if (ch is XamlIlAstXamlPropertyValueNode pn + if (ch is XamlAstXamlPropertyValueNode pn && pn.Property.GetClrProperty().Getter?.ReturnType.Equals(context.GetAvaloniaTypes().Transitions) == true) { for (var c = 0; c < pn.Values.Count; c++) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 1efae902c6..bf1fb4a6fc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -1,80 +1,119 @@ -using XamlIl.Transform; -using XamlIl.TypeSystem; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { class AvaloniaXamlIlWellKnownTypes { - public IXamlIlType AvaloniaObject { get; } - public IXamlIlType IAvaloniaObject { get; } - public IXamlIlType BindingPriority { get; } - public IXamlIlType AvaloniaObjectExtensions { get; } - public IXamlIlType AvaloniaProperty { get; } - public IXamlIlType AvaloniaPropertyT { get; } - public IXamlIlType IBinding { get; } - public IXamlIlMethod AvaloniaObjectBindMethod { get; } - public IXamlIlMethod AvaloniaObjectSetValueMethod { get; } - public IXamlIlType IDisposable { get; } - public XamlIlTypeWellKnownTypes XamlIlTypes { get; } - public XamlIlLanguageTypeMappings XamlIlMappings { get; } - public IXamlIlType Transitions { get; } - public IXamlIlType AssignBindingAttribute { get; } - public IXamlIlType UnsetValueType { get; } - public IXamlIlType StyledElement { get; } - public IXamlIlType NameScope { get; } - public IXamlIlMethod NameScopeSetNameScope { get; } - public IXamlIlType INameScope { get; } - public IXamlIlMethod INameScopeRegister { get; } - public IXamlIlMethod INameScopeComplete { get; } - - public AvaloniaXamlIlWellKnownTypes(XamlIlAstTransformationContext ctx) + public IXamlType AvaloniaObject { get; } + public IXamlType IAvaloniaObject { get; } + public IXamlType BindingPriority { get; } + public IXamlType AvaloniaObjectExtensions { get; } + public IXamlType AvaloniaProperty { get; } + public IXamlType AvaloniaPropertyT { get; } + public IXamlType IBinding { get; } + public IXamlMethod AvaloniaObjectBindMethod { get; } + public IXamlMethod AvaloniaObjectSetValueMethod { get; } + public IXamlType IDisposable { get; } + public XamlTypeWellKnownTypes XamlIlTypes { get; } + public XamlLanguageTypeMappings XamlIlMappings { get; } + public IXamlType Transitions { get; } + public IXamlType AssignBindingAttribute { get; } + public IXamlType UnsetValueType { get; } + public IXamlType StyledElement { get; } + public IXamlType NameScope { get; } + public IXamlMethod NameScopeSetNameScope { get; } + public IXamlType INameScope { get; } + public IXamlMethod INameScopeRegister { get; } + public IXamlMethod INameScopeComplete { get; } + public IXamlType IPropertyInfo { get; } + public IXamlType ClrPropertyInfo { get; } + public IXamlType PropertyPath { get; } + public IXamlType PropertyPathBuilder { get; } + public IXamlType IPropertyAccessor { get; } + public IXamlType PropertyInfoAccessorFactory { get; } + public IXamlType CompiledBindingPathBuilder { get; } + public IXamlType CompiledBindingPath { get; } + public IXamlType CompiledBindingExtension { get; } + public IXamlType DataTemplate { get; } + public IXamlType IItemsPresenterHost { get; } + public IXamlType ReflectionBindingExtension { get; } + + public IXamlType RelativeSource { get; } + + public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg) { - XamlIlTypes = ctx.Configuration.WellKnownTypes; - XamlIlMappings = ctx.Configuration.TypeMappings; - AvaloniaObject = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaObject"); - IAvaloniaObject = ctx.Configuration.TypeSystem.GetType("Avalonia.IAvaloniaObject"); - AvaloniaObjectExtensions = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaObjectExtensions"); - AvaloniaProperty = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaProperty"); - AvaloniaPropertyT = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaProperty`1"); - BindingPriority = ctx.Configuration.TypeSystem.GetType("Avalonia.Data.BindingPriority"); - IBinding = ctx.Configuration.TypeSystem.GetType("Avalonia.Data.IBinding"); - IDisposable = ctx.Configuration.TypeSystem.GetType("System.IDisposable"); - Transitions = ctx.Configuration.TypeSystem.GetType("Avalonia.Animation.Transitions"); - AssignBindingAttribute = ctx.Configuration.TypeSystem.GetType("Avalonia.Data.AssignBindingAttribute"); + XamlIlTypes = cfg.WellKnownTypes; + AvaloniaObject = cfg.TypeSystem.GetType("Avalonia.AvaloniaObject"); + IAvaloniaObject = cfg.TypeSystem.GetType("Avalonia.IAvaloniaObject"); + AvaloniaObjectExtensions = cfg.TypeSystem.GetType("Avalonia.AvaloniaObjectExtensions"); + AvaloniaProperty = cfg.TypeSystem.GetType("Avalonia.AvaloniaProperty"); + AvaloniaPropertyT = cfg.TypeSystem.GetType("Avalonia.AvaloniaProperty`1"); + BindingPriority = cfg.TypeSystem.GetType("Avalonia.Data.BindingPriority"); + IBinding = cfg.TypeSystem.GetType("Avalonia.Data.IBinding"); + IDisposable = cfg.TypeSystem.GetType("System.IDisposable"); + Transitions = cfg.TypeSystem.GetType("Avalonia.Animation.Transitions"); + AssignBindingAttribute = cfg.TypeSystem.GetType("Avalonia.Data.AssignBindingAttribute"); AvaloniaObjectBindMethod = AvaloniaObjectExtensions.FindMethod("Bind", IDisposable, false, IAvaloniaObject, AvaloniaProperty, - IBinding, ctx.Configuration.WellKnownTypes.Object); - UnsetValueType = ctx.Configuration.TypeSystem.GetType("Avalonia.UnsetValueType"); - StyledElement = ctx.Configuration.TypeSystem.GetType("Avalonia.StyledElement"); - INameScope = ctx.Configuration.TypeSystem.GetType("Avalonia.Controls.INameScope"); + IBinding, cfg.WellKnownTypes.Object); + UnsetValueType = cfg.TypeSystem.GetType("Avalonia.UnsetValueType"); + StyledElement = cfg.TypeSystem.GetType("Avalonia.StyledElement"); + INameScope = cfg.TypeSystem.GetType("Avalonia.Controls.INameScope"); INameScopeRegister = INameScope.GetMethod( new FindMethodMethodSignature("Register", XamlIlTypes.Void, XamlIlTypes.String, XamlIlTypes.Object) { - IsStatic = false, DeclaringOnly = true, IsExactMatch = true + IsStatic = false, + DeclaringOnly = true, + IsExactMatch = true }); INameScopeComplete = INameScope.GetMethod( new FindMethodMethodSignature("Complete", XamlIlTypes.Void) { - IsStatic = false, DeclaringOnly = true, IsExactMatch = true + IsStatic = false, + DeclaringOnly = true, + IsExactMatch = true }); - NameScope = ctx.Configuration.TypeSystem.GetType("Avalonia.Controls.NameScope"); + NameScope = cfg.TypeSystem.GetType("Avalonia.Controls.NameScope"); NameScopeSetNameScope = NameScope.GetMethod(new FindMethodMethodSignature("SetNameScope", - XamlIlTypes.Void, StyledElement, INameScope) {IsStatic = true}); - + XamlIlTypes.Void, StyledElement, INameScope) + { IsStatic = true }); AvaloniaObjectSetValueMethod = AvaloniaObject.FindMethod("SetValue", XamlIlTypes.Void, false, AvaloniaProperty, XamlIlTypes.Object, BindingPriority); - + IPropertyInfo = cfg.TypeSystem.GetType("Avalonia.Data.Core.IPropertyInfo"); + ClrPropertyInfo = cfg.TypeSystem.GetType("Avalonia.Data.Core.ClrPropertyInfo"); + PropertyPath = cfg.TypeSystem.GetType("Avalonia.Data.Core.PropertyPath"); + PropertyPathBuilder = cfg.TypeSystem.GetType("Avalonia.Data.Core.PropertyPathBuilder"); + IPropertyAccessor = cfg.TypeSystem.GetType("Avalonia.Data.Core.Plugins.IPropertyAccessor"); + PropertyInfoAccessorFactory = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.PropertyInfoAccessorFactory"); + CompiledBindingPathBuilder = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPathBuilder"); + CompiledBindingPath = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPath"); + CompiledBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension"); + DataTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.DataTemplate"); + IItemsPresenterHost = cfg.TypeSystem.GetType("Avalonia.Controls.Presenters.IItemsPresenterHost"); + ReflectionBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension"); + RelativeSource = cfg.TypeSystem.GetType("Avalonia.Data.RelativeSource"); } } static class AvaloniaXamlIlWellKnownTypesExtensions { - public static AvaloniaXamlIlWellKnownTypes GetAvaloniaTypes(this XamlIlAstTransformationContext ctx) + public static AvaloniaXamlIlWellKnownTypes GetAvaloniaTypes(this AstTransformationContext ctx) + { + if (ctx.TryGetItem(out var rv)) + return rv; + ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx.Configuration)); + return rv; + } + + public static AvaloniaXamlIlWellKnownTypes GetAvaloniaTypes(this XamlEmitContext ctx) { if (ctx.TryGetItem(out var rv)) return rv; - ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx)); + ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx.Configuration)); return rv; } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs index a79a4977ef..1f2508715e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs @@ -1,17 +1,17 @@ using System.Linq; -using XamlIl; -using XamlIl.Ast; -using XamlIl.Transform; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { - class IgnoredDirectivesTransformer : IXamlIlAstTransformer + class IgnoredDirectivesTransformer : IXamlAstTransformer { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (node is XamlIlAstObjectNode no) + if (node is XamlAstObjectNode no) { - foreach (var d in no.Children.OfType().ToList()) + foreach (var d in no.Children.OfType().ToList()) { if (d.Namespace == XamlNamespaces.Xaml2006) { diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/XNameTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/XNameTransformer.cs index dd64ab542a..f4eb7cf5f9 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/XNameTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/XNameTransformer.cs @@ -1,30 +1,29 @@ -using XamlIl; -using XamlIl.Ast; -using XamlIl.Transform; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { - class XNameTransformer : IXamlIlAstTransformer + class XNameTransformer : IXamlAstTransformer { - /// /// Converts x:Name directives to regular Name assignments /// /// - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (node is XamlIlAstObjectNode on) + if (node is XamlAstObjectNode on) { for (var c =0; c< on.Children.Count;c++) { var ch = on.Children[c]; - if (ch is XamlIlAstXmlDirective d + if (ch is XamlAstXmlDirective d && d.Namespace == XamlNamespaces.Xaml2006 && d.Name == "Name") - on.Children[c] = new XamlIlAstXamlPropertyValueNode(d, - new XamlIlAstNamePropertyReference(d, on.Type, "Name", on.Type), + on.Children[c] = new XamlAstXamlPropertyValueNode(d, + new XamlAstNamePropertyReference(d, on.Type, "Name", on.Type), d.Values); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs index 6fc83cb5a5..7f1b8caf0d 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs @@ -5,17 +5,36 @@ using System.Xml; using Avalonia.Markup.Xaml.Parsers; using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; using Avalonia.Utilities; -using XamlIl; -using XamlIl.Ast; -using XamlIl.Transform; -using XamlIl.Transform.Transformers; -using XamlIl.TypeSystem; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; +using XamlX.Emit; +using XamlX.IL; + +using XamlIlEmitContext = XamlX.Emit.XamlEmitContext; +using IXamlIlAstEmitableNode = XamlX.Emit.IXamlAstEmitableNode; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { class XamlIlAvaloniaPropertyHelper { - public static bool Emit(XamlIlEmitContext context, IXamlIlEmitter emitter, XamlIlAstClrProperty property) + public static bool EmitProvideValueTarget(XamlIlEmitContext context, IXamlILEmitter emitter, + XamlAstClrProperty property) + { + if (Emit(context, emitter, property)) + return true; + var foundClr = property.DeclaringType.Properties.FirstOrDefault(p => p.Name == property.Name); + if (foundClr == null) + return false; + context + .Configuration.GetExtra() + .Emit(context, emitter, foundClr); + return true; + } + + public static bool Emit(XamlIlEmitContext context, IXamlILEmitter emitter, XamlAstClrProperty property) { if (property is IXamlIlAvaloniaProperty ap) { @@ -32,7 +51,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return true; } - public static bool Emit(XamlIlEmitContext context, IXamlIlEmitter emitter, IXamlIlProperty property) + public static bool Emit(XamlIlEmitContext context, IXamlILEmitter emitter, IXamlProperty property) { var type = (property.Getter ?? property.Setter).DeclaringType; var name = property.Name + "Property"; @@ -44,16 +63,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return true; } - public static IXamlIlAvaloniaPropertyNode CreateNode(XamlIlAstTransformationContext context, - string propertyName, IXamlIlAstTypeReference selectorTypeReference, IXamlIlLineInfo lineInfo) + public static IXamlIlAvaloniaPropertyNode CreateNode(AstTransformationContext context, + string propertyName, IXamlAstTypeReference selectorTypeReference, IXamlLineInfo lineInfo) { - XamlIlAstNamePropertyReference forgedReference; + XamlAstNamePropertyReference forgedReference; var parser = new PropertyParser(); var parsedPropertyName = parser.Parse(new CharacterReader(propertyName.AsSpan())); if(parsedPropertyName.owner == null) - forgedReference = new XamlIlAstNamePropertyReference(lineInfo, selectorTypeReference, + forgedReference = new XamlAstNamePropertyReference(lineInfo, selectorTypeReference, propertyName, selectorTypeReference); else { @@ -62,101 +81,107 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions xmlOwner += ":"; xmlOwner += parsedPropertyName.owner; - var tref = XamlIlTypeReferenceResolver.ResolveType(context, xmlOwner, false, lineInfo, true); + var tref = TypeReferenceResolver.ResolveType(context, xmlOwner, false, lineInfo, true); var propertyFieldName = parsedPropertyName.name + "Property"; var found = tref.Type.GetAllFields() .FirstOrDefault(f => f.IsStatic && f.IsPublic && f.Name == propertyFieldName); if (found == null) - throw new XamlIlParseException( + throw new XamlX.XamlParseException( $"Unable to find {propertyFieldName} field on type {tref.Type.GetFullName()}", lineInfo); return new XamlIlAvaloniaPropertyFieldNode(context.GetAvaloniaTypes(), lineInfo, found); } var clrProperty = - ((XamlIlAstClrProperty)new XamlIlPropertyReferenceResolver().Transform(context, + ((XamlAstClrProperty)new PropertyReferenceResolver().Transform(context, forgedReference)); return new XamlIlAvaloniaPropertyNode(lineInfo, context.Configuration.TypeSystem.GetType("Avalonia.AvaloniaProperty"), clrProperty); } + + public static IXamlType GetAvaloniaPropertyType(IXamlField field, + AvaloniaXamlIlWellKnownTypes types, IXamlLineInfo lineInfo) + { + var avaloniaPropertyType = field.FieldType; + while (avaloniaPropertyType != null) + { + if (avaloniaPropertyType.GenericTypeDefinition?.Equals(types.AvaloniaPropertyT) == true) + { + return avaloniaPropertyType.GenericArguments[0]; + } + + avaloniaPropertyType = avaloniaPropertyType.BaseType; + } + + throw new XamlX.XamlParseException( + $"{field.Name}'s type {field.FieldType} doesn't inherit from AvaloniaProperty, make sure to use typed properties", + lineInfo); + + } } - interface IXamlIlAvaloniaPropertyNode : IXamlIlAstValueNode + interface IXamlIlAvaloniaPropertyNode : IXamlAstValueNode { - IXamlIlType AvaloniaPropertyType { get; } + IXamlType AvaloniaPropertyType { get; } } - class XamlIlAvaloniaPropertyNode : XamlIlAstNode, IXamlIlAstValueNode, IXamlIlAstEmitableNode, IXamlIlAvaloniaPropertyNode + class XamlIlAvaloniaPropertyNode : XamlAstNode, IXamlAstValueNode, IXamlIlAstEmitableNode, IXamlIlAvaloniaPropertyNode { - public XamlIlAvaloniaPropertyNode(IXamlIlLineInfo lineInfo, IXamlIlType type, XamlIlAstClrProperty property) : base(lineInfo) + public XamlIlAvaloniaPropertyNode(IXamlLineInfo lineInfo, IXamlType type, XamlAstClrProperty property) : base(lineInfo) { - Type = new XamlIlAstClrTypeReference(this, type, false); + Type = new XamlAstClrTypeReference(this, type, false); Property = property; AvaloniaPropertyType = Property.Getter?.ReturnType ?? Property.Setters.First().Parameters[0]; } - public XamlIlAstClrProperty Property { get; } + public XamlAstClrProperty Property { get; } - public IXamlIlAstTypeReference Type { get; } - public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + public IXamlAstTypeReference Type { get; } + public XamlILNodeEmitResult Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) { if (!XamlIlAvaloniaPropertyHelper.Emit(context, codeGen, Property)) - throw new XamlIlLoadException(Property.Name + " is not an AvaloniaProperty", this); - return XamlIlNodeEmitResult.Type(0, Type.GetClrType()); + throw new XamlX.XamlLoadException(Property.Name + " is not an AvaloniaProperty", this); + return XamlILNodeEmitResult.Type(0, Type.GetClrType()); } - public IXamlIlType AvaloniaPropertyType { get; } + public IXamlType AvaloniaPropertyType { get; } } - class XamlIlAvaloniaPropertyFieldNode : XamlIlAstNode, IXamlIlAstValueNode, IXamlIlAstEmitableNode, IXamlIlAvaloniaPropertyNode + class XamlIlAvaloniaPropertyFieldNode : XamlAstNode, IXamlAstValueNode, IXamlIlAstEmitableNode, IXamlIlAvaloniaPropertyNode { - private readonly IXamlIlField _field; + private readonly IXamlField _field; public XamlIlAvaloniaPropertyFieldNode(AvaloniaXamlIlWellKnownTypes types, - IXamlIlLineInfo lineInfo, IXamlIlField field) : base(lineInfo) + IXamlLineInfo lineInfo, IXamlField field) : base(lineInfo) { _field = field; - var avaloniaPropertyType = field.FieldType; - while (avaloniaPropertyType != null) - { - if (avaloniaPropertyType.GenericTypeDefinition?.Equals(types.AvaloniaPropertyT) == true) - { - AvaloniaPropertyType = avaloniaPropertyType.GenericArguments[0]; - return; - } - - avaloniaPropertyType = avaloniaPropertyType.BaseType; - } - - throw new XamlIlParseException( - $"{field.Name}'s type {field.FieldType} doesn't inherit from AvaloniaProperty, make sure to use typed properties", - lineInfo); - + AvaloniaPropertyType = XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(field, + types, lineInfo); } - public IXamlIlAstTypeReference Type => new XamlIlAstClrTypeReference(this, _field.FieldType, false); - public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + public IXamlAstTypeReference Type => new XamlAstClrTypeReference(this, _field.FieldType, false); + public XamlILNodeEmitResult Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) { codeGen.Ldsfld(_field); - return XamlIlNodeEmitResult.Type(0, _field.FieldType); + return XamlILNodeEmitResult.Type(0, _field.FieldType); } - public IXamlIlType AvaloniaPropertyType { get; } + public IXamlType AvaloniaPropertyType { get; } } interface IXamlIlAvaloniaProperty { - IXamlIlField AvaloniaProperty { get; } + IXamlField AvaloniaProperty { get; } } - class XamlIlAvaloniaProperty : XamlIlAstClrProperty, IXamlIlAvaloniaProperty + class XamlIlAvaloniaProperty : XamlAstClrProperty, IXamlIlAvaloniaProperty { - public IXamlIlField AvaloniaProperty { get; } - public XamlIlAvaloniaProperty(XamlIlAstClrProperty original, IXamlIlField field, + public IXamlField AvaloniaProperty { get; } + public XamlIlAvaloniaProperty(XamlAstClrProperty original, IXamlField field, AvaloniaXamlIlWellKnownTypes types) :base(original, original.Name, original.DeclaringType, original.Getter, original.Setters) { @@ -168,41 +193,41 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions Setters.Insert(0, new UnsetValueSetter(types, original.DeclaringType, field)); } - abstract class AvaloniaPropertyCustomSetter : IXamlIlPropertySetter + abstract class AvaloniaPropertyCustomSetter : IXamlPropertySetter, IXamlEmitablePropertySetter { protected AvaloniaXamlIlWellKnownTypes Types; - protected IXamlIlField AvaloniaProperty; + protected IXamlField AvaloniaProperty; public AvaloniaPropertyCustomSetter(AvaloniaXamlIlWellKnownTypes types, - IXamlIlType declaringType, - IXamlIlField avaloniaProperty) + IXamlType declaringType, + IXamlField avaloniaProperty) { Types = types; AvaloniaProperty = avaloniaProperty; TargetType = declaringType; } - public IXamlIlType TargetType { get; } + public IXamlType TargetType { get; } public PropertySetterBinderParameters BinderParameters { get; } = new PropertySetterBinderParameters { AllowXNull = false }; - public IReadOnlyList Parameters { get; set; } - public abstract void Emit(IXamlIlEmitter codegen); + public IReadOnlyList Parameters { get; set; } + public abstract void Emit(IXamlILEmitter codegen); } class BindingSetter : AvaloniaPropertyCustomSetter { public BindingSetter(AvaloniaXamlIlWellKnownTypes types, - IXamlIlType declaringType, - IXamlIlField avaloniaProperty) : base(types, declaringType, avaloniaProperty) + IXamlType declaringType, + IXamlField avaloniaProperty) : base(types, declaringType, avaloniaProperty) { Parameters = new[] {types.IBinding}; } - public override void Emit(IXamlIlEmitter emitter) + public override void Emit(IXamlILEmitter emitter) { using (var bloc = emitter.LocalsPool.GetLocal(Types.IBinding)) emitter @@ -217,13 +242,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions class UnsetValueSetter : AvaloniaPropertyCustomSetter { - public UnsetValueSetter(AvaloniaXamlIlWellKnownTypes types, IXamlIlType declaringType, IXamlIlField avaloniaProperty) + public UnsetValueSetter(AvaloniaXamlIlWellKnownTypes types, IXamlType declaringType, IXamlField avaloniaProperty) : base(types, declaringType, avaloniaProperty) { Parameters = new[] {types.UnsetValueType}; } - public override void Emit(IXamlIlEmitter codegen) + public override void Emit(IXamlILEmitter codegen) { var unsetValue = Types.AvaloniaProperty.Fields.First(f => f.Name == "UnsetValue"); codegen diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs new file mode 100644 index 0000000000..1f527e9569 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -0,0 +1,660 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; +using System.Reflection.Emit; +using Avalonia.Markup.Parsers; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; +using XamlX; +using XamlX.Emit; +using XamlX.IL; +using Avalonia.Utilities; + +using XamlIlEmitContext = XamlX.Emit.XamlEmitContext; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions +{ + static class XamlIlBindingPathHelper + { + public static IXamlType UpdateCompiledBindingExtension(AstTransformationContext context, XamlAstConstructableObjectNode binding, IXamlType startType) + { + IXamlType bindingResultType = null; + if (binding.Arguments.Count > 0 && binding.Arguments[0] is ParsedBindingPathNode bindingPath) + { + var transformed = TransformBindingPath( + context, + bindingPath, + startType, + bindingPath.Path); + + bindingResultType = transformed.BindingResultType; + binding.Arguments[0] = transformed; + } + else + { + var bindingPathAssignment = binding.Children.OfType() + .FirstOrDefault(v => v.Property.Name == "Path"); + + if (bindingPathAssignment is null) + { + return startType; + } + + if (bindingPathAssignment.Values[0] is ParsedBindingPathNode bindingPathNode) + { + var transformed = TransformBindingPath( + context, + bindingPathNode, + startType, + bindingPathNode.Path); + + bindingResultType = transformed.BindingResultType; + bindingPathAssignment.Values[0] = transformed; + } + else + { + throw new InvalidOperationException(); + } + } + + return bindingResultType; + } + + private static IXamlIlBindingPathNode TransformBindingPath(AstTransformationContext context, IXamlLineInfo lineInfo, IXamlType startType, IEnumerable bindingExpression) + { + List transformNodes = new List(); + List nodes = new List(); + foreach (var astNode in bindingExpression) + { + var targetType = nodes.Count == 0 ? startType : nodes[nodes.Count - 1].Type; + switch (astNode) + { + case BindingExpressionGrammar.EmptyExpressionNode _: + break; + case BindingExpressionGrammar.NotNode _: + transformNodes.Add(new XamlIlNotPathElementNode(context.Configuration.WellKnownTypes.Boolean)); + break; + case BindingExpressionGrammar.StreamNode _: + IXamlType observableType; + if (targetType.GenericTypeDefinition?.Equals(context.Configuration.TypeSystem.FindType("System.IObservable`1")) == true) + { + observableType = targetType; + } + else + { + observableType = targetType.GetAllInterfaces().FirstOrDefault(i => i.GenericTypeDefinition?.Equals(context.Configuration.TypeSystem.FindType("System.IObservable`1")) ?? false); + } + + if (observableType != null) + { + nodes.Add(new XamlIlStreamObservablePathElementNode(observableType.GenericArguments[0])); + break; + } + bool foundTask = false; + for (var currentType = targetType; currentType != null; currentType = currentType.BaseType) + { + if (currentType.GenericTypeDefinition.Equals(context.Configuration.TypeSystem.GetType("System.Threading.Tasks.Task`1"))) + { + foundTask = true; + nodes.Add(new XamlIlStreamTaskPathElementNode(currentType.GenericArguments[0])); + break; + } + } + if (foundTask) + { + break; + } + throw new XamlX.XamlParseException($"Compiled bindings do not support stream bindings for objects of type {targetType.FullName}.", lineInfo); + case BindingExpressionGrammar.PropertyNameNode propName: + var avaloniaPropertyFieldNameMaybe = propName.PropertyName + "Property"; + var avaloniaPropertyFieldMaybe = targetType.GetAllFields().FirstOrDefault(f => + f.IsStatic && f.IsPublic && f.Name == avaloniaPropertyFieldNameMaybe); + + if (avaloniaPropertyFieldMaybe != null) + { + nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyFieldMaybe, + XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyFieldMaybe, context.GetAvaloniaTypes(), lineInfo))); + } + else + { + var clrProperty = targetType.GetAllProperties().FirstOrDefault(p => p.Name == propName.PropertyName); + + if (clrProperty is null) + { + throw new XamlX.XamlParseException($"Unable to resolve property of name '{propName.PropertyName}' on type '{targetType}'.", lineInfo); + } + nodes.Add(new XamlIlClrPropertyPathElementNode(clrProperty)); + } + break; + case BindingExpressionGrammar.IndexerNode indexer: + { + if (targetType.IsArray) + { + nodes.Add(new XamlIlArrayIndexerPathElementNode(targetType, indexer.Arguments, lineInfo)); + break; + } + + IXamlProperty property = null; + for (var currentType = targetType; currentType != null; currentType = currentType.BaseType) + { + var defaultMemberAttribute = currentType.CustomAttributes.FirstOrDefault(x => x.Type.Namespace == "System.Reflection" && x.Type.Name == "DefaultMemberAttribute"); + if (defaultMemberAttribute != null) + { + property = targetType.GetAllProperties().FirstOrDefault(x => x.Name == (string)defaultMemberAttribute.Parameters[0]); + break; + } + }; + if (property is null) + { + throw new XamlX.XamlParseException($"The type '${targetType}' does not have an indexer.", lineInfo); + } + + IEnumerable parameters = property.IndexerParameters; + + List values = new List(); + int currentParamIndex = 0; + foreach (var param in parameters) + { + var textNode = new XamlAstTextNode(lineInfo, indexer.Arguments[currentParamIndex], type: context.Configuration.WellKnownTypes.String); + if (!XamlTransformHelpers.TryGetCorrectlyTypedValue(context, textNode, + param, out var converted)) + throw new XamlX.XamlParseException( + $"Unable to convert indexer parameter value of '{indexer.Arguments[currentParamIndex]}' to {param.GetFqn()}", + textNode); + + values.Add(converted); + currentParamIndex++; + } + + bool isNotifyingCollection = targetType.GetAllInterfaces().Any(i => i.FullName == "System.Collections.Specialized.INotifyCollectionChanged"); + + nodes.Add(new XamlIlClrIndexerPathElementNode(property, values, string.Join(",", indexer.Arguments), isNotifyingCollection)); + break; + } + case BindingExpressionGrammar.AttachedPropertyNameNode attachedProp: + var avaloniaPropertyFieldName = attachedProp.PropertyName + "Property"; + var avaloniaPropertyField = GetType(attachedProp.Namespace, attachedProp.TypeName).GetAllFields().FirstOrDefault(f => + f.IsStatic && f.IsPublic && f.Name == avaloniaPropertyFieldName); + nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyField, + XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyField, context.GetAvaloniaTypes(), lineInfo))); + break; + case BindingExpressionGrammar.SelfNode _: + nodes.Add(new SelfPathElementNode(targetType)); + break; + case VisualAncestorBindingExpressionNode visualAncestor: + nodes.Add(new FindVisualAncestorPathElementNode(visualAncestor.Type, visualAncestor.Level)); + break; + case TemplatedParentBindingExpressionNode templatedParent: + var templatedParentField = context.GetAvaloniaTypes().StyledElement.GetAllFields() + .FirstOrDefault(f => f.IsStatic && f.IsPublic && f.Name == "TemplatedParentProperty"); + nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode( + templatedParentField, + templatedParent.Type)); + break; + case BindingExpressionGrammar.AncestorNode ancestor: + if (ancestor.Namespace is null && ancestor.TypeName is null) + { + var styledElementType = context.GetAvaloniaTypes().StyledElement; + var ancestorType = context + .ParentNodes() + .OfType() + .Where(x => styledElementType.IsAssignableFrom(x.Type.GetClrType())) + .ElementAtOrDefault(ancestor.Level) + ?.Type.GetClrType(); + + if (ancestorType is null) + { + throw new XamlX.XamlParseException("Unable to resolve implicit ancestor type based on XAML tree.", lineInfo); + } + + nodes.Add(new FindAncestorPathElementNode(ancestorType, ancestor.Level)); + } + else + { + nodes.Add(new FindAncestorPathElementNode(GetType(ancestor.Namespace, ancestor.TypeName), ancestor.Level)); + } + break; + case BindingExpressionGrammar.NameNode elementName: + IXamlType elementType = null; + foreach (var deferredContent in context.ParentNodes().OfType()) + { + elementType = ScopeRegistrationFinder.GetTargetType(deferredContent, elementName.Name); + if (!(elementType is null)) + { + break; + } + } + if (elementType is null) + { + elementType = ScopeRegistrationFinder.GetTargetType(context.ParentNodes().Last(), elementName.Name); + } + + if (elementType is null) + { + throw new XamlX.XamlParseException($"Unable to find element '{elementName.Name}' in the current namescope. Unable to use a compiled binding with a name binding if the name cannot be found at compile time.", lineInfo); + } + nodes.Add(new ElementNamePathElementNode(elementName.Name, elementType)); + break; + case RawSourceBindingExpressionNode rawSource: + nodes.Add(new RawSourcePathElementNode(rawSource.RawSource)); + break; + } + } + + return new XamlIlBindingPathNode(lineInfo, context.GetAvaloniaTypes().CompiledBindingPath, transformNodes, nodes); + + IXamlType GetType(string ns, string name) + { + return TypeReferenceResolver.ResolveType(context, $"{ns}:{name}", false, + lineInfo, true).GetClrType(); + } + } + + class ScopeRegistrationFinder : IXamlAstVisitor + { + private Stack _stack = new Stack(); + private Stack _childScopesStack = new Stack(); + + private ScopeRegistrationFinder(string name) + { + Name = name; + } + + string Name { get; } + + IXamlType TargetType { get; set; } + + public static IXamlType GetTargetType(IXamlAstNode namescopeRoot, string name) + { + var finder = new ScopeRegistrationFinder(name); + namescopeRoot.Visit(finder); + return finder.TargetType; + } + + void IXamlAstVisitor.Pop() + { + var node = _stack.Pop(); + if (_childScopesStack.Count > 0 && node == _childScopesStack.Peek()) + { + _childScopesStack.Pop(); + } + } + + void IXamlAstVisitor.Push(IXamlAstNode node) + { + _stack.Push(node); + if (node is NestedScopeMetadataNode) + { + _childScopesStack.Push(node); + } + } + + IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) + { + if (_childScopesStack.Count == 0 && node is AvaloniaNameScopeRegistrationXamlIlNode registration) + { + if (registration.Name is XamlAstTextNode text && text.Text == Name) + { + TargetType = registration.TargetType; + } + } + return node; + } + } + + interface IXamlIlBindingPathElementNode + { + IXamlType Type { get; } + + void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen); + } + + class XamlIlNotPathElementNode : IXamlIlBindingPathElementNode + { + public XamlIlNotPathElementNode(IXamlType boolType) + { + Type = boolType; + } + + public IXamlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "Not")); + } + } + + class XamlIlStreamObservablePathElementNode : IXamlIlBindingPathElementNode + { + public XamlIlStreamObservablePathElementNode(IXamlType type) + { + Type = type; + } + + public IXamlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "StreamObservable").MakeGenericMethod(new[] { Type })); + } + } + + class XamlIlStreamTaskPathElementNode : IXamlIlBindingPathElementNode + { + public XamlIlStreamTaskPathElementNode(IXamlType type) + { + Type = type; + } + + public IXamlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "StreamTask").MakeGenericMethod(new[] { Type })); + } + } + + class SelfPathElementNode : IXamlIlBindingPathElementNode + { + public SelfPathElementNode(IXamlType type) + { + Type = type; + } + + public IXamlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "Self")); + } + } + + class FindAncestorPathElementNode : IXamlIlBindingPathElementNode + { + private readonly int _level; + + public FindAncestorPathElementNode(IXamlType ancestorType, int level) + { + Type = ancestorType; + _level = level; + } + + public IXamlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.Ldtype(Type) + .Ldc_I4(_level) + .EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "FindAncestor")); + } + } + + class FindVisualAncestorPathElementNode : IXamlIlBindingPathElementNode + { + private readonly int _level; + + public FindVisualAncestorPathElementNode(IXamlType ancestorType, int level) + { + Type = ancestorType; + _level = level; + } + + public IXamlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.Ldtype(Type) + .Ldc_I4(_level) + .EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "VisualAncestor")); + } + } + + class ElementNamePathElementNode : IXamlIlBindingPathElementNode + { + private readonly string _name; + + public ElementNamePathElementNode(string name, IXamlType elementType) + { + _name = name; + Type = elementType; + } + + public IXamlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + var scopeField = context.RuntimeContext.ContextType.Fields.First(f => + f.Name == AvaloniaXamlIlLanguage.ContextNameScopeFieldName); + + codeGen + .Ldloc(context.ContextLocal) + .Ldfld(scopeField) + .Ldstr(_name) + .EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "ElementName")); + } + } + + class XamlIlAvaloniaPropertyPropertyPathElementNode : IXamlIlBindingPathElementNode + { + private readonly IXamlField _field; + + public XamlIlAvaloniaPropertyPropertyPathElementNode(IXamlField field, IXamlType propertyType) + { + _field = field; + Type = propertyType; + } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.Ldsfld(_field); + context.Configuration.GetExtra() + .EmitLoadAvaloniaPropertyAccessorFactory(context, codeGen); + codeGen.EmitCall(context.GetAvaloniaTypes() + .CompiledBindingPathBuilder.FindMethod(m => m.Name == "Property")); + } + + public IXamlType Type { get; } + } + + class XamlIlClrPropertyPathElementNode : IXamlIlBindingPathElementNode + { + private readonly IXamlProperty _property; + + public XamlIlClrPropertyPathElementNode(IXamlProperty property) + { + _property = property; + } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + context.Configuration.GetExtra() + .Emit(context, codeGen, _property); + + context.Configuration.GetExtra() + .EmitLoadInpcPropertyAccessorFactory(context, codeGen); + + codeGen + .EmitCall(context.GetAvaloniaTypes() + .CompiledBindingPathBuilder.FindMethod(m => m.Name == "Property")); + } + + public IXamlType Type => _property.Getter?.ReturnType ?? _property.Setter?.Parameters[0]; + } + + class XamlIlClrIndexerPathElementNode : IXamlIlBindingPathElementNode + { + private readonly IXamlProperty _property; + private readonly List _values; + private readonly string _indexerKey; + private readonly bool _isNotifyingCollection; + + public XamlIlClrIndexerPathElementNode(IXamlProperty property, List values, string indexerKey, bool isNotifyingCollection) + { + _property = property; + _values = values; + _indexerKey = indexerKey; + _isNotifyingCollection = isNotifyingCollection; + } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + var intType = context.Configuration.TypeSystem.GetType("System.Int32"); + context.Configuration.GetExtra() + .Emit(context, codeGen, _property, _values, _indexerKey); + + if (_isNotifyingCollection + && + _values.Count == 1 + && _values[0].Type.GetClrType().Equals(intType)) + { + context.Configuration.GetExtra() + .EmitLoadIndexerAccessorFactory(context, codeGen, _values[0]); + } + else + { + context.Configuration.GetExtra() + .EmitLoadInpcPropertyAccessorFactory(context, codeGen); + } + + codeGen.EmitCall(context.GetAvaloniaTypes() + .CompiledBindingPathBuilder.FindMethod(m => m.Name == "Property")); + } + + public IXamlType Type => _property.Getter?.ReturnType ?? _property.Setter?.Parameters[0]; + } + + class XamlIlArrayIndexerPathElementNode : IXamlIlBindingPathElementNode + { + private readonly IXamlType _arrayType; + private readonly List _values; + + public XamlIlArrayIndexerPathElementNode(IXamlType arrayType, IList values, IXamlLineInfo lineInfo) + { + _arrayType = arrayType; + _values = new List(values.Count); + foreach (var item in values) + { + if (!int.TryParse(item, out var index)) + { + throw new XamlX.XamlParseException($"Unable to convert '{item}' to an integer.", lineInfo.Line, lineInfo.Position); + } + _values.Add(index); + } + } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + var intType = context.Configuration.TypeSystem.GetType("System.Int32"); + var indices = codeGen.DefineLocal(intType.MakeArrayType(1)); + codeGen.Ldc_I4(_values.Count) + .Newarr(intType) + .Stloc(indices); + for (int i = 0; i < _values.Count; i++) + { + codeGen.Ldloc(indices) + .Ldc_I4(i) + .Ldc_I4(_values[i]) + .Emit(OpCodes.Stelem_I4); + } + + codeGen.Ldloc(indices) + .Ldtype(Type) + .EmitCall(context.GetAvaloniaTypes() + .CompiledBindingPathBuilder.FindMethod(m => m.Name == "ArrayElement")); + } + + public IXamlType Type => _arrayType.ArrayElementType; + } + + class RawSourcePathElementNode : XamlAstNode, IXamlIlBindingPathElementNode + { + private readonly IXamlAstValueNode _rawSource; + + public RawSourcePathElementNode(IXamlAstValueNode rawSource) + :base(rawSource) + { + _rawSource = rawSource; + + } + + public IXamlType Type => _rawSource.Type.GetClrType(); + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + context.Emit(_rawSource, codeGen, Type); + codeGen + .EmitCall(context.GetAvaloniaTypes() + .CompiledBindingPathBuilder.FindMethod(m => m.Name == "SetRawSource")); + } + } + + class XamlIlBindingPathNode : XamlAstNode, IXamlIlBindingPathNode, IXamlAstEmitableNode + { + private readonly List _transformElements; + private readonly List _elements; + + public XamlIlBindingPathNode(IXamlLineInfo lineInfo, + IXamlType bindingPathType, + List transformElements, + List elements) : base(lineInfo) + { + Type = new XamlAstClrTypeReference(lineInfo, bindingPathType, false); + _transformElements = transformElements; + _elements = elements; + } + + public IXamlType BindingResultType + => _transformElements.Count > 0 + ? _transformElements[0].Type + : _elements[_elements.Count - 1].Type; + + public IXamlAstTypeReference Type { get; } + + public XamlILNodeEmitResult Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + var types = context.GetAvaloniaTypes(); + codeGen.Newobj(types.CompiledBindingPathBuilder.FindConstructor()); + + foreach (var transform in _transformElements) + { + transform.Emit(context, codeGen); + } + + foreach (var element in _elements) + { + element.Emit(context, codeGen); + } + + codeGen.EmitCall(types.CompiledBindingPathBuilder.FindMethod(m => m.Name == "Build")); + return XamlILNodeEmitResult.Type(0, types.CompiledBindingPath); + } + + public override void VisitChildren(IXamlAstVisitor visitor) + { + for (int i = 0; i < _transformElements.Count; i++) + { + if (_transformElements[i] is IXamlAstNode ast) + { + _transformElements[i] = (IXamlIlBindingPathElementNode)ast.Visit(visitor); + } + } + for (int i = 0; i < _elements.Count; i++) + { + if (_elements[i] is IXamlAstNode ast) + { + _elements[i] = (IXamlIlBindingPathElementNode)ast.Visit(visitor); + } + } + } + } + } + + interface IXamlIlBindingPathNode : IXamlAstValueNode + { + IXamlType BindingResultType { get; } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs new file mode 100644 index 0000000000..871a2a2045 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.TypeSystem; +using XamlX.IL; +using XamlX.Emit; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions +{ + class XamlIlClrPropertyInfoEmitter + { + private readonly IXamlTypeBuilder _builder; + + private Dictionary> _fields + = new Dictionary>(); + + public XamlIlClrPropertyInfoEmitter(IXamlTypeBuilder builder) + { + _builder = builder; + } + + static string GetKey(IXamlProperty property, string indexerArgumentsKey) + { + var baseKey = property.Getter.DeclaringType.GetFullName() + "." + property.Name; + + if (indexerArgumentsKey is null) + { + return baseKey; + } + + return baseKey + $"[{indexerArgumentsKey}]"; + } + + public IXamlType Emit(XamlEmitContext context, IXamlILEmitter codeGen, IXamlProperty property, IEnumerable indexerArguments = null, string indexerArgumentsKey = null) + { + indexerArguments = indexerArguments ?? Enumerable.Empty(); + var types = context.GetAvaloniaTypes(); + IXamlMethod Get() + { + var key = GetKey(property, indexerArgumentsKey); + if (!_fields.TryGetValue(key, out var lst)) + _fields[key] = lst = new List<(IXamlProperty prop, IXamlMethod get)>(); + + foreach (var cached in lst) + { + if ( + ((cached.prop.Getter == null && property.Getter == null) || + cached.prop.Getter?.Equals(property.Getter) == true) && + ((cached.prop.Setter == null && property.Setter == null) || + cached.prop.Setter?.Equals(property.Setter) == true) + ) + return cached.get; + } + + var name = lst.Count == 0 ? key : key + "_" + Guid.NewGuid().ToString("N"); + + var field = _builder.DefineField(types.IPropertyInfo, name + "!Field", false, true); + + void Load(IXamlMethod m, IXamlILEmitter cg) + { + cg + .Ldarg_0(); + if (m.DeclaringType.IsValueType) + cg.Unbox(m.DeclaringType); + else + cg.Castclass(m.DeclaringType); + + foreach (var indexerArg in indexerArguments) + { + context.Emit(indexerArg, cg, indexerArg.Type.GetClrType()); + } + } + + var getter = property.Getter == null ? + null : + _builder.DefineMethod(types.XamlIlTypes.Object, + new[] {types.XamlIlTypes.Object}, name + "!Getter", false, true, false); + if (getter != null) + { + Load(property.Getter, getter.Generator); + + getter.Generator.EmitCall(property.Getter); + if (property.Getter.ReturnType.IsValueType) + getter.Generator.Box(property.Getter.ReturnType); + getter.Generator.Ret(); + } + + var setter = property.Setter == null ? + null : + _builder.DefineMethod(types.XamlIlTypes.Void, + new[] {types.XamlIlTypes.Object, types.XamlIlTypes.Object}, + name + "!Setter", false, true, false); + if (setter != null) + { + Load(property.Setter, setter.Generator); + + setter.Generator.Ldarg(1); + if (property.Setter.Parameters[0].IsValueType) + setter.Generator.Unbox_Any(property.Setter.Parameters[0]); + else + setter.Generator.Castclass(property.Setter.Parameters[0]); + setter.Generator + .EmitCall(property.Setter, true) + .Ret(); + } + + var get = _builder.DefineMethod(types.IPropertyInfo, Array.Empty(), + name + "!Property", true, true, false); + + + var ctor = types.ClrPropertyInfo.Constructors.First(c => + c.Parameters.Count == 4 && c.IsStatic == false); + + var cacheMiss = get.Generator.DefineLabel(); + get.Generator + .Ldsfld(field) + .Brfalse(cacheMiss) + .Ldsfld(field) + .Ret() + .MarkLabel(cacheMiss) + .Ldstr(property.Name); + + void EmitFunc(IXamlILEmitter emitter, IXamlMethod method, IXamlType del) + { + if (method == null) + emitter.Ldnull(); + else + { + emitter + .Ldnull() + .Ldftn(method) + .Newobj(del.Constructors.First(c => + c.Parameters.Count == 2 && + c.Parameters[0].Equals(context.Configuration.WellKnownTypes.Object))); + } + } + + EmitFunc(get.Generator, getter, ctor.Parameters[1]); + EmitFunc(get.Generator, setter, ctor.Parameters[2]); + get.Generator + .Ldtype(property.PropertyType) + .Newobj(ctor) + .Stsfld(field) + .Ldsfld(field) + .Ret(); + + lst.Add((property, get)); + return get; + } + + codeGen.EmitCall(Get()); + return types.IPropertyInfo; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlPropertyInfoAccessorFactoryEmitter.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlPropertyInfoAccessorFactoryEmitter.cs new file mode 100644 index 0000000000..d24a43a84f --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlPropertyInfoAccessorFactoryEmitter.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.TypeSystem; +using XamlX.Emit; +using XamlX.IL; + +using XamlIlEmitContext = XamlX.Emit.XamlEmitContext; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions +{ + class XamlIlPropertyInfoAccessorFactoryEmitter + { + private const string IndexerClosureFactoryMethodName = "CreateAccessor"; + private readonly IXamlTypeBuilder _indexerClosureTypeBuilder; + private IXamlType _indexerClosureType; + public XamlIlPropertyInfoAccessorFactoryEmitter(IXamlTypeBuilder indexerClosureType) + { + _indexerClosureTypeBuilder = indexerClosureType; + } + + public IXamlType EmitLoadInpcPropertyAccessorFactory(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.Ldnull(); + EmitLoadPropertyAccessorFactory(context, codeGen, context.GetAvaloniaTypes().PropertyInfoAccessorFactory, "CreateInpcPropertyAccessor"); + return EmitCreateAccessorFactoryDelegate(context, codeGen); + } + + public IXamlType EmitLoadAvaloniaPropertyAccessorFactory(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.Ldnull(); + EmitLoadPropertyAccessorFactory(context, codeGen, context.GetAvaloniaTypes().PropertyInfoAccessorFactory, "CreateAvaloniaPropertyAccessor"); + return EmitCreateAccessorFactoryDelegate(context, codeGen); + } + + private void EmitLoadPropertyAccessorFactory(XamlIlEmitContext context, IXamlILEmitter codeGen, IXamlType type, string accessorFactoryName, bool isStatic = true) + { + var types = context.GetAvaloniaTypes(); + var weakReferenceType = context.Configuration.TypeSystem.GetType("System.WeakReference`1").MakeGenericType(context.Configuration.WellKnownTypes.Object); + FindMethodMethodSignature accessorFactorySignature = new FindMethodMethodSignature(accessorFactoryName, types.IPropertyAccessor, weakReferenceType, types.IPropertyInfo) + { + IsStatic = isStatic + }; + codeGen.Ldftn(type.GetMethod(accessorFactorySignature)); + } + + public IXamlType EmitLoadIndexerAccessorFactory(XamlIlEmitContext context, IXamlILEmitter codeGen, IXamlAstValueNode value) + { + var intType = context.Configuration.TypeSystem.GetType("System.Int32"); + if (_indexerClosureType is null) + { + _indexerClosureType = InitializeClosureType(context); + } + + context.Emit(value, codeGen, intType); + codeGen.Newobj(_indexerClosureType.FindConstructor(new List { intType })); + EmitLoadPropertyAccessorFactory(context, codeGen, _indexerClosureType, IndexerClosureFactoryMethodName, isStatic: false); + return EmitCreateAccessorFactoryDelegate(context, codeGen); + } + + private IXamlType InitializeClosureType(XamlIlEmitContext context) + { + var types = context.GetAvaloniaTypes(); + var intType = context.Configuration.TypeSystem.GetType("System.Int32"); + var weakReferenceType = context.Configuration.TypeSystem.GetType("System.WeakReference`1").MakeGenericType(context.Configuration.WellKnownTypes.Object); + var indexAccessorFactoryMethod = context.GetAvaloniaTypes().PropertyInfoAccessorFactory.GetMethod( + new FindMethodMethodSignature( + "CreateIndexerPropertyAccessor", + types.IPropertyAccessor, + weakReferenceType, + types.IPropertyInfo, + intType) + { + IsStatic = true + }); + var indexField = _indexerClosureTypeBuilder.DefineField(intType, "_index", false, false); + var ctor = _indexerClosureTypeBuilder.DefineConstructor(false, intType); + ctor.Generator + .Ldarg_0() + .Ldarg(1) + .Stfld(indexField) + .Ret(); + _indexerClosureTypeBuilder.DefineMethod( + types.IPropertyAccessor, + new[] { weakReferenceType, types.IPropertyInfo }, + IndexerClosureFactoryMethodName, + isPublic: true, + isStatic: false, + isInterfaceImpl: false) + .Generator + .Ldarg(1) + .Ldarg(2) + .LdThisFld(indexField) + .EmitCall(indexAccessorFactoryMethod) + .Ret(); + + return _indexerClosureTypeBuilder.CreateType(); + } + + private IXamlType EmitCreateAccessorFactoryDelegate(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + var types = context.GetAvaloniaTypes(); + var weakReferenceType = context.Configuration.TypeSystem.GetType("System.WeakReference`1").MakeGenericType(context.Configuration.WellKnownTypes.Object); + var funcType = context.Configuration.TypeSystem.GetType("System.Func`3").MakeGenericType( + weakReferenceType, + types.IPropertyInfo, + types.IPropertyAccessor); + codeGen.Newobj(funcType.Constructors.First(c => + c.Parameters.Count == 2 && + c.Parameters[0].Equals(context.Configuration.WellKnownTypes.Object))); + return funcType; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index 4c4b6cf8ff..0028377ce7 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit 4c4b6cf8ff0894c925d87b27d4fc7a064440c218 +Subproject commit 0028377ce7c7dc21f9fe71b45f62a95991b1ab58 diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 673eb51901..aca39dd8b0 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -3,6 +3,12 @@ netstandard2.0 Avalonia + + + + + + diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index f1a62f9bdc..bf43730481 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -15,15 +15,14 @@ namespace Avalonia.Data /// /// A XAML binding. /// - public class Binding : IBinding + public class Binding : BindingBase { /// /// Initializes a new instance of the class. /// public Binding() + :base() { - FallbackValue = AvaloniaProperty.UnsetValue; - TargetNullValue = AvaloniaProperty.UnsetValue; } /// @@ -32,52 +31,16 @@ namespace Avalonia.Data /// The binding path. /// The binding mode. public Binding(string path, BindingMode mode = BindingMode.Default) - : this() + : base(mode) { Path = path; - Mode = mode; } - /// - /// Gets or sets the to use. - /// - public IValueConverter Converter { get; set; } - - /// - /// Gets or sets a parameter to pass to . - /// - public object ConverterParameter { get; set; } - /// /// Gets or sets the name of the element to use as the binding source. /// public string ElementName { get; set; } - /// - /// Gets or sets the value to use when the binding is unable to produce a value. - /// - public object FallbackValue { get; set; } - - /// - /// Gets or sets the value to use when the binding result is null. - /// - public object TargetNullValue { get; set; } - - /// - /// Gets or sets the binding mode. - /// - public BindingMode Mode { get; set; } - - /// - /// Gets or sets the binding path. - /// - public string Path { get; set; } = ""; - - /// - /// Gets or sets the binding priority. - /// - public BindingPriority Priority { get; set; } - /// /// Gets or sets the relative source for the binding. /// @@ -89,68 +52,58 @@ namespace Avalonia.Data public object Source { get; set; } /// - /// Gets or sets the string format. + /// Gets or sets the binding path. /// - public string StringFormat { get; set; } - - public WeakReference DefaultAnchor { get; set; } - - public WeakReference NameScope { get; set; } + public string Path { get; set; } = ""; /// /// Gets or sets a function used to resolve types from names in the binding path. /// public Func TypeResolver { get; set; } - /// - public InstancedBinding Initiate( - IAvaloniaObject target, - AvaloniaProperty targetProperty, - object anchor = null, - bool enableDataValidation = false) + protected override ExpressionObserver CreateExpressionObserver(IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation) { Contract.Requires(target != null); anchor = anchor ?? DefaultAnchor?.Target; enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue; - - ExpressionObserver observer; INameScope nameScope = null; NameScope?.TryGetTarget(out nameScope); + var (node, mode) = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver, nameScope); if (ElementName != null) { - observer = CreateElementObserver( + return CreateElementObserver( (target as IStyledElement) ?? (anchor as IStyledElement), ElementName, node); } else if (Source != null) { - observer = CreateSourceObserver(Source, node); + return CreateSourceObserver(Source, node); } else if (RelativeSource == null) { if (mode == SourceMode.Data) { - observer = CreateDataContextObserver( + return CreateDataContextObserver( target, node, targetProperty == StyledElement.DataContextProperty, - anchor); + anchor); } else { - observer = new ExpressionObserver( + return CreateSourceObserver( (target as IStyledElement) ?? (anchor as IStyledElement), node); } } else if (RelativeSource.Mode == RelativeSourceMode.DataContext) { - observer = CreateDataContextObserver( + return CreateDataContextObserver( target, node, targetProperty == StyledElement.DataContextProperty, @@ -158,13 +111,13 @@ namespace Avalonia.Data } else if (RelativeSource.Mode == RelativeSourceMode.Self) { - observer = CreateSourceObserver( + return CreateSourceObserver( (target as IStyledElement) ?? (anchor as IStyledElement), node); } else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) { - observer = CreateTemplatedParentObserver( + return CreateTemplatedParentObserver( (target as IStyledElement) ?? (anchor as IStyledElement), node); } @@ -175,7 +128,7 @@ namespace Avalonia.Data throw new InvalidOperationException("AncestorType must be set for RelativeSourceMode.FindAncestor when searching the visual tree."); } - observer = CreateFindAncestorObserver( + return CreateFindAncestorObserver( (target as IStyledElement) ?? (anchor as IStyledElement), RelativeSource, node); @@ -184,197 +137,6 @@ namespace Avalonia.Data { throw new NotSupportedException(); } - - var fallback = FallbackValue; - - // If we're binding to DataContext and our fallback is UnsetValue then override - // the fallback value to null, as broken bindings to DataContext must reset the - // DataContext in order to not propagate incorrect DataContexts to child controls. - // See Avalonia.Markup.UnitTests.Data.DataContext_Binding_Should_Produce_Correct_Results. - if (targetProperty == StyledElement.DataContextProperty && fallback == AvaloniaProperty.UnsetValue) - { - fallback = null; - } - - var converter = Converter; - var targetType = targetProperty?.PropertyType ?? typeof(object); - - // We only respect `StringFormat` if the type of the property we're assigning to will - // accept a string. Note that this is slightly different to WPF in that WPF only applies - // `StringFormat` for target type `string` (not `object`). - if (!string.IsNullOrWhiteSpace(StringFormat) && - (targetType == typeof(string) || targetType == typeof(object))) - { - converter = new StringFormatValueConverter(StringFormat, converter); - } - - var subject = new BindingExpression( - observer, - targetType, - fallback, - TargetNullValue, - converter ?? DefaultValueConverter.Instance, - ConverterParameter, - Priority); - - return new InstancedBinding(subject, Mode, Priority); - } - - private ExpressionObserver CreateDataContextObserver( - IAvaloniaObject target, - ExpressionNode node, - bool targetIsDataContext, - object anchor) - { - Contract.Requires(target != null); - - if (!(target is IDataContextProvider)) - { - target = anchor as IDataContextProvider; - - if (target == null) - { - throw new InvalidOperationException("Cannot find a DataContext to bind to."); - } - } - - if (!targetIsDataContext) - { - var result = new ExpressionObserver( - () => target.GetValue(StyledElement.DataContextProperty), - node, - new UpdateSignal(target, StyledElement.DataContextProperty), - null); - - return result; - } - else - { - return new ExpressionObserver( - GetParentDataContext(target), - node, - null); - } - } - - private ExpressionObserver CreateElementObserver( - IStyledElement target, - string elementName, - ExpressionNode node) - { - Contract.Requires(target != null); - - NameScope.TryGetTarget(out var scope); - if (scope == null) - throw new InvalidOperationException("Name scope is null or was already collected"); - var result = new ExpressionObserver( - NameScopeLocator.Track(scope, elementName), - node, - null); - return result; - } - - private ExpressionObserver CreateFindAncestorObserver( - IStyledElement target, - RelativeSource relativeSource, - ExpressionNode node) - { - Contract.Requires(target != null); - - IObservable controlLocator; - - switch (relativeSource.Tree) - { - case TreeType.Logical: - controlLocator = ControlLocator.Track( - (ILogical)target, - relativeSource.AncestorLevel - 1, - relativeSource.AncestorType); - break; - case TreeType.Visual: - controlLocator = VisualLocator.Track( - (IVisual)target, - relativeSource.AncestorLevel - 1, - relativeSource.AncestorType); - break; - default: - throw new InvalidOperationException("Invalid tree to traverse."); - } - - return new ExpressionObserver( - controlLocator, - node, - null); - } - - private ExpressionObserver CreateSourceObserver( - object source, - ExpressionNode node) - { - Contract.Requires(source != null); - - return new ExpressionObserver(source, node); - } - - private ExpressionObserver CreateTemplatedParentObserver( - IAvaloniaObject target, - ExpressionNode node) - { - Contract.Requires(target != null); - - var result = new ExpressionObserver( - () => target.GetValue(StyledElement.TemplatedParentProperty), - node, - new UpdateSignal(target, StyledElement.TemplatedParentProperty), - null); - - return result; - } - - private IObservable GetParentDataContext(IAvaloniaObject target) - { - // The DataContext is based on the visual parent and not the logical parent: this may - // seem counter intuitive considering the fact that property inheritance works on the logical - // tree, but consider a ContentControl with a ContentPresenter. The ContentControl's - // Content property is bound to a value which becomes the ContentPresenter's - // DataContext - it is from this that the child hosted by the ContentPresenter needs to - // inherit its DataContext. - return target.GetObservable(Visual.VisualParentProperty) - .Select(x => - { - return (x as IAvaloniaObject)?.GetObservable(StyledElement.DataContextProperty) ?? - Observable.Return((object)null); - }).Switch(); - } - - private class UpdateSignal : SingleSubscriberObservableBase - { - private readonly IAvaloniaObject _target; - private readonly AvaloniaProperty _property; - - public UpdateSignal(IAvaloniaObject target, AvaloniaProperty property) - { - _target = target; - _property = property; - } - - protected override void Subscribed() - { - _target.PropertyChanged += PropertyChanged; - } - - protected override void Unsubscribed() - { - _target.PropertyChanged -= PropertyChanged; - } - - private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - if (e.Property == _property) - { - PublishNext(Unit.Default); - } - } } } } diff --git a/src/Markup/Avalonia.Markup/Data/BindingBase.cs b/src/Markup/Avalonia.Markup/Data/BindingBase.cs new file mode 100644 index 0000000000..7c4e7b5efe --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/BindingBase.cs @@ -0,0 +1,289 @@ + +using System; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using Avalonia.Controls; +using Avalonia.Data.Converters; +using Avalonia.Data.Core; +using Avalonia.LogicalTree; +using Avalonia.Markup.Parsers; +using Avalonia.Reactive; +using Avalonia.VisualTree; + + +namespace Avalonia.Data +{ + public abstract class BindingBase : IBinding + { + /// + /// Initializes a new instance of the class. + /// + public BindingBase() + { + FallbackValue = AvaloniaProperty.UnsetValue; + TargetNullValue = AvaloniaProperty.UnsetValue; + } + + /// + /// Initializes a new instance of the class. + /// + /// The binding mode. + public BindingBase(BindingMode mode = BindingMode.Default) + :this() + { + Mode = mode; + } + + /// + /// Gets or sets the to use. + /// + public IValueConverter Converter { get; set; } + + /// + /// Gets or sets a parameter to pass to . + /// + public object ConverterParameter { get; set; } + + /// + /// Gets or sets the value to use when the binding is unable to produce a value. + /// + public object FallbackValue { get; set; } + + /// + /// Gets or sets the value to use when the binding result is null. + /// + public object TargetNullValue { get; set; } + + /// + /// Gets or sets the binding mode. + /// + public BindingMode Mode { get; set; } + + /// + /// Gets or sets the binding priority. + /// + public BindingPriority Priority { get; set; } + + /// + /// Gets or sets the string format. + /// + public string StringFormat { get; set; } + + public WeakReference DefaultAnchor { get; set; } + + public WeakReference NameScope { get; set; } + + protected abstract ExpressionObserver CreateExpressionObserver( + IAvaloniaObject target, + AvaloniaProperty targetProperty, + object anchor, + bool enableDataValidation); + + /// + public InstancedBinding Initiate( + IAvaloniaObject target, + AvaloniaProperty targetProperty, + object anchor = null, + bool enableDataValidation = false) + { + Contract.Requires(target != null); + anchor = anchor ?? DefaultAnchor?.Target; + + enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue; + + var observer = CreateExpressionObserver(target, targetProperty, anchor, enableDataValidation); + + var fallback = FallbackValue; + + // If we're binding to DataContext and our fallback is UnsetValue then override + // the fallback value to null, as broken bindings to DataContext must reset the + // DataContext in order to not propagate incorrect DataContexts to child controls. + // See Avalonia.Markup.UnitTests.Data.DataContext_Binding_Should_Produce_Correct_Results. + if (targetProperty == StyledElement.DataContextProperty && fallback == AvaloniaProperty.UnsetValue) + { + fallback = null; + } + + var converter = Converter; + var targetType = targetProperty?.PropertyType ?? typeof(object); + + // We only respect `StringFormat` if the type of the property we're assigning to will + // accept a string. Note that this is slightly different to WPF in that WPF only applies + // `StringFormat` for target type `string` (not `object`). + if (!string.IsNullOrWhiteSpace(StringFormat) && + (targetType == typeof(string) || targetType == typeof(object))) + { + converter = new StringFormatValueConverter(StringFormat, converter); + } + + var subject = new BindingExpression( + observer, + targetType, + fallback, + TargetNullValue, + converter ?? DefaultValueConverter.Instance, + ConverterParameter, + Priority); + + return new InstancedBinding(subject, Mode, Priority); + } + + protected ExpressionObserver CreateDataContextObserver( + IAvaloniaObject target, + ExpressionNode node, + bool targetIsDataContext, + object anchor) + { + Contract.Requires(target != null); + + if (!(target is IStyledElement)) + { + target = anchor as IStyledElement; + + if (target == null) + { + throw new InvalidOperationException("Cannot find a DataContext to bind to."); + } + } + + if (!targetIsDataContext) + { + var result = new ExpressionObserver( + () => target.GetValue(StyledElement.DataContextProperty), + node, + new UpdateSignal(target, StyledElement.DataContextProperty), + null); + + return result; + } + else + { + return new ExpressionObserver( + GetParentDataContext(target), + node, + null); + } + } + + protected ExpressionObserver CreateElementObserver( + IStyledElement target, + string elementName, + ExpressionNode node) + { + Contract.Requires(target != null); + + NameScope.TryGetTarget(out var scope); + if (scope == null) + throw new InvalidOperationException("Name scope is null or was already collected"); + var result = new ExpressionObserver( + NameScopeLocator.Track(scope, elementName), + node, + null); + return result; + } + + protected ExpressionObserver CreateFindAncestorObserver( + IStyledElement target, + RelativeSource relativeSource, + ExpressionNode node) + { + Contract.Requires(target != null); + + IObservable controlLocator; + + switch (relativeSource.Tree) + { + case TreeType.Logical: + controlLocator = ControlLocator.Track( + (ILogical)target, + relativeSource.AncestorLevel - 1, + relativeSource.AncestorType); + break; + case TreeType.Visual: + controlLocator = VisualLocator.Track( + (IVisual)target, + relativeSource.AncestorLevel - 1, + relativeSource.AncestorType); + break; + default: + throw new InvalidOperationException("Invalid tree to traverse."); + } + + return new ExpressionObserver( + controlLocator, + node, + null); + } + + protected ExpressionObserver CreateSourceObserver( + object source, + ExpressionNode node) + { + Contract.Requires(source != null); + + return new ExpressionObserver(source, node); + } + + protected ExpressionObserver CreateTemplatedParentObserver( + IAvaloniaObject target, + ExpressionNode node) + { + Contract.Requires(target != null); + + var result = new ExpressionObserver( + () => target.GetValue(StyledElement.TemplatedParentProperty), + node, + new UpdateSignal(target, StyledElement.TemplatedParentProperty), + null); + + return result; + } + + protected IObservable GetParentDataContext(IAvaloniaObject target) + { + // The DataContext is based on the visual parent and not the logical parent: this may + // seem counter intuitive considering the fact that property inheritance works on the logical + // tree, but consider a ContentControl with a ContentPresenter. The ContentControl's + // Content property is bound to a value which becomes the ContentPresenter's + // DataContext - it is from this that the child hosted by the ContentPresenter needs to + // inherit its DataContext. + return target.GetObservable(Visual.VisualParentProperty) + .Select(x => + { + return (x as IAvaloniaObject)?.GetObservable(StyledElement.DataContextProperty) ?? + Observable.Return((object)null); + }).Switch(); + } + + private class UpdateSignal : SingleSubscriberObservableBase + { + private readonly IAvaloniaObject _target; + private readonly AvaloniaProperty _property; + + public UpdateSignal(IAvaloniaObject target, AvaloniaProperty property) + { + _target = target; + _property = property; + } + + protected override void Subscribed() + { + _target.PropertyChanged += PropertyChanged; + } + + protected override void Unsubscribed() + { + _target.PropertyChanged -= PropertyChanged; + } + + private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + PublishNext(Unit.Default); + } + } + } + } +} diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 9ab5882361..7aa1eed890 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -107,6 +107,14 @@ namespace Avalonia.Data private object ConvertValue(IList values, Type targetType, IMultiValueConverter converter) { + for (var i = 0; i < values.Count; ++i) + { + if (values[i] is BindingNotification notification) + { + values[i] = notification.Value; + } + } + var culture = CultureInfo.CurrentCulture; var converted = converter.Convert(values, targetType, ConverterParameter, culture); diff --git a/src/Markup/Avalonia.Markup/Markup/Data/DelayedBinding.cs b/src/Markup/Avalonia.Markup/Markup/Data/DelayedBinding.cs index ebb544bcca..0b0ed7b06a 100644 --- a/src/Markup/Avalonia.Markup/Markup/Data/DelayedBinding.cs +++ b/src/Markup/Avalonia.Markup/Markup/Data/DelayedBinding.cs @@ -147,8 +147,7 @@ namespace Avalonia.Markup.Data } catch (Exception e) { - Logger.TryGet(LogEventLevel.Error)?.Log( - LogArea.Property, + Logger.TryGet(LogEventLevel.Error, LogArea.Property)?.Log( control, "Error setting {Property} on {Target}: {Exception}", Property.Name, diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs index c3651594e9..e858bacb74 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs @@ -4,7 +4,10 @@ using Avalonia.Utilities; namespace Avalonia.Markup.Parsers { - internal static class ArgumentListParser +#if !BUILDTASK + public +#endif + static class ArgumentListParser { public static IList ParseArguments(this ref CharacterReader r, char open, char close, char delimiter = ',') { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs new file mode 100644 index 0000000000..8de64e56ff --- /dev/null +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs @@ -0,0 +1,387 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Data.Core; +using Avalonia.Utilities; +using System; +using System.Collections.Generic; + +namespace Avalonia.Markup.Parsers +{ + internal enum SourceMode + { + Data, + Control + } + + internal static class BindingExpressionGrammar + { + public static (IList Nodes, SourceMode Mode) Parse(ref CharacterReader r) + { + var nodes = new List(); + var state = State.Start; + var mode = SourceMode.Data; + + while (!r.End && state != State.End) + { + switch (state) + { + case State.Start: + state = ParseStart(ref r, nodes); + break; + + case State.AfterMember: + state = ParseAfterMember(ref r, nodes); + break; + + case State.BeforeMember: + state = ParseBeforeMember(ref r, nodes); + break; + + case State.AttachedProperty: + state = ParseAttachedProperty(ref r, nodes); + break; + + case State.Indexer: + state = ParseIndexer(ref r, nodes); + break; + + case State.ElementName: + state = ParseElementName(ref r, nodes); + mode = SourceMode.Control; + break; + + case State.RelativeSource: + state = ParseRelativeSource(ref r, nodes); + mode = SourceMode.Control; + break; + } + } + + if (state == State.BeforeMember) + { + throw new ExpressionParseException(r.Position, "Unexpected end of expression."); + } + + return (nodes, mode); + } + + private static State ParseStart(ref CharacterReader r, IList nodes) + { + if (ParseNot(ref r)) + { + nodes.Add(new NotNode()); + return State.Start; + } + + else if (ParseSharp(ref r)) + { + return State.ElementName; + } + else if (ParseDollarSign(ref r)) + { + return State.RelativeSource; + } + else if (ParseOpenBrace(ref r)) + { + return State.AttachedProperty; + } + else if (PeekOpenBracket(ref r)) + { + return State.Indexer; + } + else if (ParseDot(ref r)) + { + nodes.Add(new EmptyExpressionNode()); + return State.End; + } + else + { + var identifier = r.ParseIdentifier(); + + if (!identifier.IsEmpty) + { + nodes.Add(new PropertyNameNode { PropertyName = identifier.ToString() }); + return State.AfterMember; + } + } + + return State.End; + } + + private static State ParseAfterMember(ref CharacterReader r, IList nodes) + { + if (ParseMemberAccessor(ref r)) + { + return State.BeforeMember; + } + else if (ParseStreamOperator(ref r)) + { + nodes.Add(new StreamNode()); + return State.AfterMember; + } + else if (PeekOpenBracket(ref r)) + { + return State.Indexer; + } + + return State.End; + } + + private static State ParseBeforeMember(ref CharacterReader r, IList nodes) + { + if (ParseOpenBrace(ref r)) + { + return State.AttachedProperty; + } + else + { + var identifier = r.ParseIdentifier(); + + if (!identifier.IsEmpty) + { + nodes.Add(new PropertyNameNode { PropertyName = identifier.ToString() }); + return State.AfterMember; + } + + return State.End; + } + } + + private static State ParseAttachedProperty(ref CharacterReader r, List nodes) + { + var (ns, owner) = ParseTypeName(ref r); + + if (r.End || !r.TakeIf('.')) + { + throw new ExpressionParseException(r.Position, "Invalid attached property name."); + } + + var name = r.ParseIdentifier(); + + if (r.End || !r.TakeIf(')')) + { + throw new ExpressionParseException(r.Position, "Expected ')'."); + } + + nodes.Add(new AttachedPropertyNameNode + { + Namespace = ns.ToString(), + TypeName = owner.ToString(), + PropertyName = name.ToString() + }); + return State.AfterMember; + } + + private static State ParseIndexer(ref CharacterReader r, List nodes) + { + var args = r.ParseArguments('[', ']'); + + if (args.Count == 0) + { + throw new ExpressionParseException(r.Position, "Indexer may not be empty."); + } + + nodes.Add(new IndexerNode { Arguments = args }); + return State.AfterMember; + } + + private static State ParseElementName(ref CharacterReader r, List nodes) + { + var name = r.ParseIdentifier(); + + if (name.IsEmpty) + { + throw new ExpressionParseException(r.Position, "Element name expected after '#'."); + } + + nodes.Add(new NameNode { Name = name.ToString() }); + return State.AfterMember; + } + + private static State ParseRelativeSource(ref CharacterReader r, List nodes) + { + var mode = r.ParseIdentifier(); + + if (mode.SequenceEqual("self".AsSpan())) + { + nodes.Add(new SelfNode()); + } + else if (mode.SequenceEqual("parent".AsSpan())) + { + string ancestorNamespace = null; + string ancestorType = null; + var ancestorLevel = 0; + if (PeekOpenBracket(ref r)) + { + var args = r.ParseArguments('[', ']', ';'); + if (args.Count > 2 || args.Count == 0) + { + throw new ExpressionParseException(r.Position, "Too many arguments in RelativeSource syntax sugar"); + } + else if (args.Count == 1) + { + if (int.TryParse(args[0], out int level)) + { + ancestorType = null; + ancestorLevel = level; + } + else + { + var reader = new CharacterReader(args[0].AsSpan()); + (ancestorNamespace, ancestorType) = ParseTypeName(ref reader); + } + } + else + { + var reader = new CharacterReader(args[0].AsSpan()); + (ancestorNamespace, ancestorType) = ParseTypeName(ref reader); + ancestorLevel = int.Parse(args[1]); + } + } + nodes.Add(new AncestorNode + { + Namespace = ancestorNamespace, + TypeName = ancestorType, + Level = ancestorLevel + }); + } + else + { + throw new ExpressionParseException(r.Position, "Unknown RelativeSource mode."); + } + + return State.AfterMember; + } + + private static TypeName ParseTypeName(ref CharacterReader r) + { + ReadOnlySpan ns, typeName; + ns = ReadOnlySpan.Empty; + var typeNameOrNamespace = r.ParseIdentifier(); + + if (!r.End && r.TakeIf(':')) + { + ns = typeNameOrNamespace; + typeName = r.ParseIdentifier(); + } + else + { + typeName = typeNameOrNamespace; + } + + return new TypeName(ns, typeName); + } + + private static bool ParseNot(ref CharacterReader r) + { + return !r.End && r.TakeIf('!'); + } + + private static bool ParseMemberAccessor(ref CharacterReader r) + { + return !r.End && r.TakeIf('.'); + } + + private static bool ParseOpenBrace(ref CharacterReader r) + { + return !r.End && r.TakeIf('('); + } + + private static bool PeekOpenBracket(ref CharacterReader r) + { + return !r.End && r.Peek == '['; + } + + private static bool ParseStreamOperator(ref CharacterReader r) + { + return !r.End && r.TakeIf('^'); + } + + private static bool ParseDollarSign(ref CharacterReader r) + { + return !r.End && r.TakeIf('$'); + } + + private static bool ParseSharp(ref CharacterReader r) + { + return !r.End && r.TakeIf('#'); + } + + private static bool ParseDot(ref CharacterReader r) + { + return !r.End && r.TakeIf('.'); + } + + private enum State + { + Start, + RelativeSource, + ElementName, + AfterMember, + BeforeMember, + AttachedProperty, + Indexer, + End, + } + + private readonly ref struct TypeName + { + public TypeName(ReadOnlySpan ns, ReadOnlySpan typeName) + { + Namespace = ns; + Type = typeName; + } + + public readonly ReadOnlySpan Namespace; + public readonly ReadOnlySpan Type; + + public void Deconstruct(out string ns, out string typeName) + { + ns = Namespace.ToString(); + typeName = Type.ToString(); + } + } + + public interface INode {} + + public interface ITransformNode {} + + public class EmptyExpressionNode : INode { } + + public class PropertyNameNode : INode + { + public string PropertyName { get; set; } + } + + public class AttachedPropertyNameNode : INode + { + public string Namespace { get; set; } + public string TypeName { get; set; } + public string PropertyName { get; set; } + } + + public class IndexerNode : INode + { + public IList Arguments { get; set; } + } + + public class NotNode : INode, ITransformNode {} + + public class StreamNode : INode {} + + public class SelfNode : INode {} + + public class NameNode : INode + { + public string Name { get; set; } + } + + public class AncestorNode : INode + { + public string Namespace { get; set; } + public string TypeName { get; set; } + public int Level { get; set; } + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index ae9f338806..1048148c1f 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -8,12 +8,6 @@ using Avalonia.Controls; namespace Avalonia.Markup.Parsers { - internal enum SourceMode - { - Data, - Control - } - internal class ExpressionParser { private readonly bool _enableValidation; @@ -29,332 +23,85 @@ namespace Avalonia.Markup.Parsers public (ExpressionNode Node, SourceMode Mode) Parse(ref CharacterReader r) { - var nodes = new List(); - var state = State.Start; - var mode = SourceMode.Data; + ExpressionNode rootNode = null; + ExpressionNode node = null; + var (astNodes, mode) = BindingExpressionGrammar.Parse(ref r); - while (!r.End && state != State.End) + foreach (var astNode in astNodes) { - switch (state) + ExpressionNode nextNode = null; + switch (astNode) { - case State.Start: - state = ParseStart(ref r, nodes); + case BindingExpressionGrammar.EmptyExpressionNode _: + nextNode = new EmptyExpressionNode(); break; - - case State.AfterMember: - state = ParseAfterMember(ref r, nodes); + case BindingExpressionGrammar.NotNode _: + nextNode = new LogicalNotNode(); break; - - case State.BeforeMember: - state = ParseBeforeMember(ref r, nodes); + case BindingExpressionGrammar.StreamNode _: + nextNode = new StreamNode(); break; - - case State.AttachedProperty: - state = ParseAttachedProperty(ref r, nodes); + case BindingExpressionGrammar.PropertyNameNode propName: + nextNode = new PropertyAccessorNode(propName.PropertyName, _enableValidation); break; - - case State.Indexer: - state = ParseIndexer(ref r, nodes); + case BindingExpressionGrammar.IndexerNode indexer: + nextNode = new StringIndexerNode(indexer.Arguments); break; - - case State.ElementName: - state = ParseElementName(ref r, nodes); - mode = SourceMode.Control; + case BindingExpressionGrammar.AttachedPropertyNameNode attachedProp: + nextNode = ParseAttachedProperty(attachedProp); break; - - case State.RelativeSource: - state = ParseRelativeSource(ref r, nodes); - mode = SourceMode.Control; + case BindingExpressionGrammar.SelfNode _: + nextNode = new SelfNode(); + break; + case BindingExpressionGrammar.AncestorNode ancestor: + nextNode = ParseFindAncestor(ancestor); + break; + case BindingExpressionGrammar.NameNode elementName: + nextNode = new ElementNameNode(_nameScope, elementName.Name); break; } - } - - if (state == State.BeforeMember) - { - throw new ExpressionParseException(r.Position, "Unexpected end of expression."); - } - - for (int n = 0; n < nodes.Count - 1; ++n) - { - nodes[n].Next = nodes[n + 1]; - } - - return (nodes.FirstOrDefault(), mode); - } - - private State ParseStart(ref CharacterReader r, IList nodes) - { - if (ParseNot(ref r)) - { - nodes.Add(new LogicalNotNode()); - return State.Start; - } - - else if (ParseSharp(ref r)) - { - return State.ElementName; - } - else if (ParseDollarSign(ref r)) - { - return State.RelativeSource; - } - else if (ParseOpenBrace(ref r)) - { - return State.AttachedProperty; - } - else if (PeekOpenBracket(ref r)) - { - return State.Indexer; - } - else if (ParseDot(ref r)) - { - nodes.Add(new EmptyExpressionNode()); - return State.End; - } - else - { - var identifier = r.ParseIdentifier(); - - if (!identifier.IsEmpty) + if (rootNode is null) { - nodes.Add(new PropertyAccessorNode(identifier.ToString(), _enableValidation)); - return State.AfterMember; + rootNode = node = nextNode; } - } - - return State.End; - } - - private static State ParseAfterMember(ref CharacterReader r, IList nodes) - { - if (ParseMemberAccessor(ref r)) - { - return State.BeforeMember; - } - else if (ParseStreamOperator(ref r)) - { - nodes.Add(new StreamNode()); - return State.AfterMember; - } - else if (PeekOpenBracket(ref r)) - { - return State.Indexer; - } - - return State.End; - } - - private State ParseBeforeMember(ref CharacterReader r, IList nodes) - { - if (ParseOpenBrace(ref r)) - { - return State.AttachedProperty; - } - else - { - var identifier = r.ParseIdentifier(); - - if (!identifier.IsEmpty) + else { - nodes.Add(new PropertyAccessorNode(identifier.ToString(), _enableValidation)); - return State.AfterMember; + node.Next = nextNode; + node = nextNode; } - - return State.End; - } - } - - private State ParseAttachedProperty(ref CharacterReader r, List nodes) - { - var (ns, owner) = ParseTypeName(ref r); - - if (r.End || !r.TakeIf('.')) - { - throw new ExpressionParseException(r.Position, "Invalid attached property name."); - } - - var name = r.ParseIdentifier(); - - if (r.End || !r.TakeIf(')')) - { - throw new ExpressionParseException(r.Position, "Expected ')'."); - } - - if (_typeResolver == null) - { - throw new InvalidOperationException("Cannot parse a binding path with an attached property without a type resolver. Maybe you can use a LINQ Expression binding path instead?"); - } - - var property = AvaloniaPropertyRegistry.Instance.FindRegistered(_typeResolver(ns.ToString(), owner.ToString()), name.ToString()); - - nodes.Add(new AvaloniaPropertyAccessorNode(property, _enableValidation)); - return State.AfterMember; - } - - private State ParseIndexer(ref CharacterReader r, List nodes) - { - var args = r.ParseArguments('[', ']'); - - if (args.Count == 0) - { - throw new ExpressionParseException(r.Position, "Indexer may not be empty."); } - nodes.Add(new StringIndexerNode(args)); - return State.AfterMember; + return (rootNode, mode); } - private State ParseElementName(ref CharacterReader r, List nodes) + private FindAncestorNode ParseFindAncestor(BindingExpressionGrammar.AncestorNode node) { - var name = r.ParseIdentifier(); + Type ancestorType = null; + var ancestorLevel = node.Level; - if (name == null) + if (!(node.Namespace is null) && !(node.TypeName is null)) { - throw new ExpressionParseException(r.Position, "Element name expected after '#'."); - } - - nodes.Add(new ElementNameNode(_nameScope, name.ToString())); - return State.AfterMember; - } - - private State ParseRelativeSource(ref CharacterReader r, List nodes) - { - var mode = r.ParseIdentifier(); - - if (mode.Equals("self".AsSpan(), StringComparison.InvariantCulture)) - { - nodes.Add(new SelfNode()); - } - else if (mode.Equals("parent".AsSpan(), StringComparison.InvariantCulture)) - { - Type ancestorType = null; - var ancestorLevel = 0; - if (PeekOpenBracket(ref r)) + if (_typeResolver == null) { - var args = r.ParseArguments('[', ']', ';'); - if (args.Count > 2 || args.Count == 0) - { - throw new ExpressionParseException(r.Position, "Too many arguments in RelativeSource syntax sugar"); - } - else if (args.Count == 1) - { - if (int.TryParse(args[0], out int level)) - { - ancestorType = null; - ancestorLevel = level; - } - else - { - var reader = new CharacterReader(args[0].AsSpan()); - var typeName = ParseTypeName(ref reader); - ancestorType = _typeResolver(typeName.Namespace.ToString(), typeName.Type.ToString()); - } - } - else - { - var reader = new CharacterReader(args[0].AsSpan()); - var typeName = ParseTypeName(ref reader); - ancestorType = _typeResolver(typeName.Namespace.ToString(), typeName.Type.ToString()); - ancestorLevel = int.Parse(args[1]); - } + throw new InvalidOperationException("Cannot parse a binding path with a typed FindAncestor without a type resolver. Maybe you can use a LINQ Expression binding path instead?"); } - nodes.Add(new FindAncestorNode(ancestorType, ancestorLevel)); - } - else - { - throw new ExpressionParseException(r.Position, "Unknown RelativeSource mode."); - } - return State.AfterMember; - } - - private static TypeName ParseTypeName(ref CharacterReader r) - { - ReadOnlySpan ns, typeName; - ns = ReadOnlySpan.Empty; - var typeNameOrNamespace = r.ParseIdentifier(); - - if (!r.End && r.TakeIf(':')) - { - ns = typeNameOrNamespace; - typeName = r.ParseIdentifier(); + ancestorType = _typeResolver(node.Namespace, node.TypeName); } - else - { - typeName = typeNameOrNamespace; - } - - return new TypeName(ns, typeName); - } - - private static bool ParseNot(ref CharacterReader r) - { - return !r.End && r.TakeIf('!'); - } - - private static bool ParseMemberAccessor(ref CharacterReader r) - { - return !r.End && r.TakeIf('.'); - } - - private static bool ParseOpenBrace(ref CharacterReader r) - { - return !r.End && r.TakeIf('('); - } - - private static bool PeekOpenBracket(ref CharacterReader r) - { - return !r.End && r.Peek == '['; - } - - private static bool ParseStreamOperator(ref CharacterReader r) - { - return !r.End && r.TakeIf('^'); - } - - private static bool ParseDollarSign(ref CharacterReader r) - { - return !r.End && r.TakeIf('$'); - } - - private static bool ParseSharp(ref CharacterReader r) - { - return !r.End && r.TakeIf('#'); - } - private static bool ParseDot(ref CharacterReader r) - { - return !r.End && r.TakeIf('.'); - } - - private enum State - { - Start, - RelativeSource, - ElementName, - AfterMember, - BeforeMember, - AttachedProperty, - Indexer, - End, + return new FindAncestorNode(ancestorType, ancestorLevel); } - private readonly ref struct TypeName + private AvaloniaPropertyAccessorNode ParseAttachedProperty(BindingExpressionGrammar.AttachedPropertyNameNode node) { - public TypeName(ReadOnlySpan ns, ReadOnlySpan typeName) + if (_typeResolver == null) { - Namespace = ns; - Type = typeName; + throw new InvalidOperationException("Cannot parse a binding path with an attached property without a type resolver. Maybe you can use a LINQ Expression binding path instead?"); } - public readonly ReadOnlySpan Namespace; - public readonly ReadOnlySpan Type; + var property = AvaloniaPropertyRegistry.Instance.FindRegistered(_typeResolver(node.Namespace, node.TypeName), node.PropertyName); - public void Deconstruct(out ReadOnlySpan ns, out ReadOnlySpan typeName) - { - ns = Namespace; - typeName = Type; - } + return new AvaloniaPropertyAccessorNode(property, _enableValidation); } } } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs index 7eec80fc00..97198145a8 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs @@ -5,7 +5,7 @@ using Avalonia.LogicalTree; namespace Avalonia.Markup.Parsers.Nodes { - internal class ElementNameNode : ExpressionNode + public class ElementNameNode : ExpressionNode { private readonly WeakReference _nameScope; private readonly string _name; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs index 321a85c1d7..f304d1e9a2 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs @@ -4,7 +4,7 @@ using Avalonia.LogicalTree; namespace Avalonia.Markup.Parsers.Nodes { - internal class FindAncestorNode : ExpressionNode + public class FindAncestorNode : ExpressionNode { private readonly int _level; private readonly Type _ancestorType; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs index 2cb87efa65..1cd233c68a 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs @@ -2,7 +2,7 @@ namespace Avalonia.Markup.Parsers.Nodes { - internal class SelfNode : ExpressionNode + public class SelfNode : ExpressionNode { public override string Description => "$self"; } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs new file mode 100644 index 0000000000..c5953b514c --- /dev/null +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using Avalonia.Data.Core; +using Avalonia.Utilities; + +namespace Avalonia.Markup.Parsers +{ +#if !BUILDTASK + public +#endif + class PropertyPathGrammar + { + private enum State + { + Start, + Next, + AfterProperty, + End + } + + public static IEnumerable Parse(string s) + { + var r = new CharacterReader(s.AsSpan()); + return Parse(ref r); + } + + private static IEnumerable Parse(ref CharacterReader r) + { + var state = State.Start; + var parsed = new List(); + while (state != State.End) + { + ISyntax syntax = null; + if (state == State.Start) + (state, syntax) = ParseStart(ref r); + else if (state == State.Next) + (state, syntax) = ParseNext(ref r); + else if (state == State.AfterProperty) + (state, syntax) = ParseAfterProperty(ref r); + + + if (syntax != null) + { + parsed.Add(syntax); + } + } + + if (state != State.End && r.End) + { + throw new ExpressionParseException(r.Position, "Unexpected end of property path"); + } + + return parsed; + } + + private static (State, ISyntax) ParseNext(ref CharacterReader r) + { + r.SkipWhitespace(); + if (r.End) + return (State.End, null); + + return ParseStart(ref r); + } + + private static (State, ISyntax) ParseStart(ref CharacterReader r) + { + if (TryParseCasts(ref r, out var rv)) + return rv; + r.SkipWhitespace(); + + if (r.TakeIf('(')) + return ParseTypeQualifiedProperty(ref r); + + return ParseProperty(ref r); + } + + private static (State, ISyntax) ParseTypeQualifiedProperty(ref CharacterReader r) + { + r.SkipWhitespace(); + const string error = + "Unable to parse qualified property name, expected `(ns:TypeName.PropertyName)` or `(TypeName.PropertyName)` after `(`"; + + var typeName = ParseXamlIdentifier(ref r); + + + if (!r.TakeIf('.')) + throw new ExpressionParseException(r.Position, error); + + var propertyName = r.ParseIdentifier(); + if (propertyName.IsEmpty) + throw new ExpressionParseException(r.Position, error); + + r.SkipWhitespace(); + if (!r.TakeIf(')')) + throw new ExpressionParseException(r.Position, + "Expected ')' after qualified property name " + + typeName.ns + ':' + typeName.name + + "." + propertyName.ToString()); + + return (State.AfterProperty, + new TypeQualifiedPropertySyntax + { + Name = propertyName.ToString(), + TypeName = typeName.name, + TypeNamespace = typeName.ns + }); + } + + static (string ns, string name) ParseXamlIdentifier(ref CharacterReader r) + { + var ident = r.ParseIdentifier(); + if (ident.IsEmpty) + throw new ExpressionParseException(r.Position, "Expected identifier"); + if (r.TakeIf(':')) + { + var part2 = r.ParseIdentifier(); + if (part2.IsEmpty) + throw new ExpressionParseException(r.Position, + "Expected the rest of the identifier after " + ident.ToString() + ":"); + return (ident.ToString(), part2.ToString()); + } + + return (null, ident.ToString()); + } + + private static (State, ISyntax) ParseProperty(ref CharacterReader r) + { + r.SkipWhitespace(); + var prop = r.ParseIdentifier(); + if (prop.IsEmpty) + throw new ExpressionParseException(r.Position, "Unable to parse property name"); + return (State.AfterProperty, new PropertySyntax {Name = prop.ToString()}); + } + + private static bool TryParseCasts(ref CharacterReader r, out (State, ISyntax) rv) + { + if (r.TakeIfKeyword(":=")) + rv = ParseEnsureType(ref r); + else if (r.TakeIfKeyword(":>") || r.TakeIfKeyword("as ")) + rv = ParseCastType(ref r); + else + { + rv = default; + return false; + } + + return true; + } + + private static (State, ISyntax) ParseAfterProperty(ref CharacterReader r) + { + if (TryParseCasts(ref r, out var rv)) + return rv; + + r.SkipWhitespace(); + if (r.End) + return (State.End, null); + if (r.TakeIf('.')) + return (State.Next, ChildTraversalSyntax.Instance); + + + + throw new ExpressionParseException(r.Position, "Unexpected character " + r.Peek + " after property name"); + } + + private static (State, ISyntax) ParseEnsureType(ref CharacterReader r) + { + r.SkipWhitespace(); + var type = ParseXamlIdentifier(ref r); + return (State.AfterProperty, new EnsureTypeSyntax {TypeName = type.name, TypeNamespace = type.ns}); + } + + private static (State, ISyntax) ParseCastType(ref CharacterReader r) + { + r.SkipWhitespace(); + var type = ParseXamlIdentifier(ref r); + return (State.AfterProperty, new CastTypeSyntax {TypeName = type.name, TypeNamespace = type.ns}); + } + + public interface ISyntax + { + + } + + public class PropertySyntax : ISyntax + { + public string Name { get; set; } + + public override bool Equals(object obj) + => obj is PropertySyntax other + && other.Name == Name; + } + + public class TypeQualifiedPropertySyntax : ISyntax + { + public string Name { get; set; } + public string TypeName { get; set; } + public string TypeNamespace { get; set; } + + public override bool Equals(object obj) + => obj is TypeQualifiedPropertySyntax other + && other.Name == Name + && other.TypeName == TypeName + && other.TypeNamespace == TypeNamespace; + } + + public class ChildTraversalSyntax : ISyntax + { + public static ChildTraversalSyntax Instance { get; } = new ChildTraversalSyntax(); + public override bool Equals(object obj) => obj is ChildTraversalSyntax; + } + + public class EnsureTypeSyntax : ISyntax + { + public string TypeName { get; set; } + public string TypeNamespace { get; set; } + public override bool Equals(object obj) + => obj is EnsureTypeSyntax other + && other.TypeName == TypeName + && other.TypeNamespace == TypeNamespace; + } + + public class CastTypeSyntax : ISyntax + { + public string TypeName { get; set; } + public string TypeNamespace { get; set; } + public override bool Equals(object obj) + => obj is CastTypeSyntax other + && other.TypeName == TypeName + && other.TypeNamespace == TypeNamespace; + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index 5e0f43e066..b25e9490cd 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -154,7 +154,7 @@ namespace Avalonia.Markup.Parsers private static (State, ISyntax) ParseColon(ref CharacterReader r) { - var identifier = r.ParseIdentifier(); + var identifier = r.ParseStyleClass(); if (identifier.IsEmpty) { @@ -214,7 +214,7 @@ namespace Avalonia.Markup.Parsers private static (State, ISyntax) ParseClass(ref CharacterReader r) { - var @class = r.ParseIdentifier(); + var @class = r.ParseStyleClass(); if (@class.IsEmpty) { throw new ExpressionParseException(r.Position, $"Expected a class name after '.'."); diff --git a/src/Skia/Avalonia.Skia/Assets/NoiseAsset_256X256_PNG.png b/src/Skia/Avalonia.Skia/Assets/NoiseAsset_256X256_PNG.png new file mode 100644 index 0000000000..41de173d90 Binary files /dev/null and b/src/Skia/Avalonia.Skia/Assets/NoiseAsset_256X256_PNG.png differ diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj index 68da513528..d5ad70b944 100644 --- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj +++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj @@ -7,6 +7,9 @@ true true + + + diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 26fdb08a4b..a510763f64 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -17,7 +17,7 @@ namespace Avalonia.Skia /// /// Skia based drawing context. /// - internal class DrawingContextImpl : IDrawingContextImpl, ISkiaDrawingContextImpl + internal class DrawingContextImpl : IDrawingContextImpl, ISkiaDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport { private IDisposable[] _disposables; private readonly Vector _dpi; @@ -34,6 +34,7 @@ namespace Avalonia.Skia private readonly SKPaint _strokePaint = new SKPaint(); private readonly SKPaint _fillPaint = new SKPaint(); private readonly SKPaint _boxShadowPaint = new SKPaint(); + private static SKShader s_acrylicNoiseShader; /// /// Context create info. @@ -227,6 +228,41 @@ namespace Avalonia.Skia return bounds; } + /// + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + { + if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0) + return; + + var rc = rect.Rect.ToSKRect(); + var isRounded = rect.IsRounded; + var needRoundRect = rect.IsRounded; + using var skRoundRect = needRoundRect ? new SKRoundRect() : null; + + if (needRoundRect) + skRoundRect.SetRectRadii(rc, + new[] + { + rect.RadiiTopLeft.ToSKPoint(), rect.RadiiTopRight.ToSKPoint(), + rect.RadiiBottomRight.ToSKPoint(), rect.RadiiBottomLeft.ToSKPoint(), + }); + + if (material != null) + { + using (var paint = CreateAcrylicPaint(_fillPaint, material)) + { + if (isRounded) + { + Canvas.DrawRoundRect(skRoundRect, paint.Paint); + } + else + { + Canvas.DrawRect(rc, paint.Paint); + } + + } + } + } /// public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows = default) @@ -389,6 +425,12 @@ namespace Avalonia.Skia Canvas.ClipRect(clip.ToSKRect()); } + public void PushClip(RoundedRect clip) + { + Canvas.Save(); + Canvas.ClipRoundRect(clip.ToSKRoundRect()); + } + /// public void PopClip() { @@ -653,6 +695,86 @@ namespace Avalonia.Skia } } + static SKColorFilter CreateAlphaColorFilter(double opacity) + { + if (opacity > 1) + opacity = 1; + var c = new byte[256]; + var a = new byte[256]; + for (var i = 0; i < 256; i++) + { + c[i] = (byte)i; + a[i] = (byte)(i * opacity); + } + + return SKColorFilter.CreateTable(a, c, c, c); + } + + static byte Blend(byte leftColor, byte leftAlpha, byte rightColor, byte rightAlpha) + { + var ca = leftColor / 255d; + var aa = leftAlpha / 255d; + var cb = rightColor / 255d; + var ab = rightAlpha / 255d; + var r = (ca * aa + cb * ab * (1 - aa)) / (aa + ab * (1 - aa)); + return (byte)(r * 255); + } + + static Color Blend(Color left, Color right) + { + var aa = left.A / 255d; + var ab = right.A / 255d; + return new Color( + (byte)((aa + ab * (1 - aa)) * 255), + Blend(left.R, left.A, right.R, right.A), + Blend(left.G, left.A, right.G, right.A), + Blend(left.B, left.A, right.B, right.A) + ); + } + + internal PaintWrapper CreateAcrylicPaint (SKPaint paint, IExperimentalAcrylicMaterial material, bool disposePaint = false) + { + var paintWrapper = new PaintWrapper(paint, disposePaint); + + paint.IsAntialias = true; + + double opacity = _currentOpacity; + + var tintOpacity = + material.BackgroundSource == AcrylicBackgroundSource.Digger ? + material.TintOpacity : 1; + + const double noiseOpcity = 0.0225; + + var tintColor = material.TintColor; + var tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, tintColor.A); + + if (s_acrylicNoiseShader == null) + { + using (var stream = typeof(DrawingContextImpl).Assembly.GetManifestResourceStream("Avalonia.Skia.Assets.NoiseAsset_256X256_PNG.png")) + using (var bitmap = SKBitmap.Decode(stream)) + { + s_acrylicNoiseShader = SKShader.CreateBitmap(bitmap, SKShaderTileMode.Repeat, SKShaderTileMode.Repeat) + .WithColorFilter(CreateAlphaColorFilter(noiseOpcity)); + } + } + + using (var backdrop = SKShader.CreateColor(new SKColor(material.MaterialColor.R, material.MaterialColor.G, material.MaterialColor.B, material.MaterialColor.A))) + using (var tintShader = SKShader.CreateColor(tint)) + using (var effectiveTint = SKShader.CreateCompose(backdrop, tintShader)) + using (var compose = SKShader.CreateCompose(effectiveTint, s_acrylicNoiseShader)) + { + paint.Shader = compose; + + if (material.BackgroundSource == AcrylicBackgroundSource.Digger) + { + paint.BlendMode = SKBlendMode.Src; + } + + return paintWrapper; + } + } + /// /// Creates paint wrapper for given brush. /// @@ -805,7 +927,7 @@ namespace Avalonia.Skia }; return new SurfaceRenderTarget(createInfo); - } + } /// /// Skia cached paint state. diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 5f876464e2..ade659f5eb 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -569,7 +569,7 @@ namespace Avalonia.Skia float constraint = -1; - if (_wrapping == TextWrapping.Wrap) + if (_wrapping != TextWrapping.NoWrap) { constraint = widthConstraint <= 0 ? MAX_LINE_WIDTH : widthConstraint; if (constraint > MAX_LINE_WIDTH) diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index 6375f74c59..ec7e0a67ed 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs @@ -40,6 +40,21 @@ namespace Avalonia.Skia return new SKRect((float)r.X, (float)r.Y, (float)r.Right, (float)r.Bottom); } + public static SKRoundRect ToSKRoundRect(this RoundedRect r) + { + var rc = r.Rect.ToSKRect(); + var result = new SKRoundRect(); + + result.SetRectRadii(rc, + new[] + { + r.RadiiTopLeft.ToSKPoint(), r.RadiiTopRight.ToSKPoint(), + r.RadiiBottomRight.ToSKPoint(), r.RadiiBottomLeft.ToSKPoint(), + }); + + return result; + } + public static Rect ToAvaloniaRect(this SKRect r) { return new Rect(r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top); diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index 7a0823a223..786af7726c 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -1,9 +1,9 @@ using System; +using System.Globalization; using Avalonia.Media; -using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; -using Avalonia.Utility; +using Avalonia.Utilities; using HarfBuzzSharp; using Buffer = HarfBuzzSharp.Buffer; @@ -11,7 +11,7 @@ namespace Avalonia.Skia { internal class TextShaperImpl : ITextShaperImpl { - public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat) + public GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize, CultureInfo culture) { using (var buffer = new Buffer()) { @@ -61,9 +61,11 @@ namespace Avalonia.Skia buffer.AddUtf16(text.Buffer.Span); } + buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); + buffer.GuessSegmentProperties(); - var glyphTypeface = textFormat.Typeface.GlyphTypeface; + var glyphTypeface = typeface.GlyphTypeface; var font = ((GlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font; @@ -71,7 +73,7 @@ namespace Avalonia.Skia font.GetScale(out var scaleX, out _); - var textScale = textFormat.FontRenderingEmSize / scaleX; + var textScale = fontRenderingEmSize / scaleX; var bufferLength = buffer.Length; @@ -101,7 +103,7 @@ namespace Avalonia.Skia SetOffset(glyphPositions, i, textScale, ref glyphOffsets); } - return new GlyphRun(glyphTypeface, textFormat.FontRenderingEmSize, + return new GlyphRun(glyphTypeface, fontRenderingEmSize, new ReadOnlySlice(glyphIndices), new ReadOnlySlice(glyphAdvances), new ReadOnlySlice(glyphOffsets), diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index bbb45cf64c..e0de40525f 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -236,7 +236,7 @@ namespace Avalonia.Direct2D1.Media Math.Max(rrect.RadiiTopRight.X, Math.Max(rrect.RadiiBottomRight.X, rrect.RadiiBottomLeft.X))); var radiusY = Math.Max(rrect.RadiiTopLeft.Y, Math.Max(rrect.RadiiTopRight.Y, Math.Max(rrect.RadiiBottomRight.Y, rrect.RadiiBottomLeft.Y))); - var isRounded = Math.Abs(radiusX) > double.Epsilon || Math.Abs(radiusY) > double.Epsilon; + var isRounded = !MathUtilities.IsZero(radiusX) || !MathUtilities.IsZero(radiusY); if (brush != null) { @@ -359,6 +359,12 @@ namespace Avalonia.Direct2D1.Media _deviceContext.PushAxisAlignedClip(clip.ToSharpDX(), AntialiasMode.PerPrimitive); } + public void PushClip(RoundedRect clip) + { + //TODO: radius + _deviceContext.PushAxisAlignedClip(clip.Rect.ToDirect2D(), AntialiasMode.PerPrimitive); + } + public void PopClip() { _deviceContext.PopAxisAlignedClip(); diff --git a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs index cca014bda5..e1f7aad1b2 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs @@ -82,8 +82,7 @@ namespace Avalonia.Direct2D1.Media } catch (Exception ex) { - Logger.TryGet(LogEventLevel.Error)?.Log( - LogArea.Visual, + Logger.TryGet(LogEventLevel.Error, LogArea.Visual)?.Log( this, "GeometrySink.Close exception: {Exception}", ex); diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index 2d2865e2b9..254b5684a4 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -1,8 +1,9 @@ -using Avalonia.Media; +using System.Globalization; +using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; -using Avalonia.Utility; +using Avalonia.Utilities; using HarfBuzzSharp; using Buffer = HarfBuzzSharp.Buffer; @@ -10,7 +11,7 @@ namespace Avalonia.Direct2D1.Media { internal class TextShaperImpl : ITextShaperImpl { - public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat) + public GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize, CultureInfo culture) { using (var buffer = new Buffer()) { @@ -62,15 +63,17 @@ namespace Avalonia.Direct2D1.Media buffer.GuessSegmentProperties(); - var glyphTypeface = textFormat.Typeface.GlyphTypeface; + var glyphTypeface = typeface.GlyphTypeface; var font = ((GlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font; + buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); + font.Shape(buffer); font.GetScale(out var scaleX, out _); - var textScale = textFormat.FontRenderingEmSize / scaleX; + var textScale = fontRenderingEmSize / scaleX; var len = buffer.Length; @@ -104,7 +107,7 @@ namespace Avalonia.Direct2D1.Media glyphOffsets[i] = new Vector(offsetX, offsetY); } - return new GlyphRun(glyphTypeface, textFormat.FontRenderingEmSize, + return new GlyphRun(glyphTypeface, fontRenderingEmSize, new ReadOnlySlice(glyphIndices), new ReadOnlySlice(glyphAdvances), new ReadOnlySlice(glyphOffsets), diff --git a/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs b/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs index abace92f08..a8060d3fbf 100644 --- a/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs +++ b/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs @@ -24,9 +24,7 @@ namespace Avalonia.Win32.Embedding if (_root.IsFocused) FocusManager.Instance.Focus(null); _root.GotFocus += RootGotFocus; - // ReSharper disable once PossibleNullReferenceException - // Always non-null at this point - _root.PlatformImpl.LostFocus += PlatformImpl_LostFocus; + FixPosition(); } @@ -36,23 +34,6 @@ namespace Avalonia.Win32.Embedding set { _root.Content = value; } } - void Unfocus() - { - var focused = (IVisual)FocusManager.Instance.Current; - if (focused == null) - return; - while (focused.VisualParent != null) - focused = focused.VisualParent; - - if (focused == _root) - KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None); - } - - private void PlatformImpl_LostFocus() - { - Unfocus(); - } - protected override void Dispose(bool disposing) { if (disposing) diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index b30831667b..f5d83611bb 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -18,11 +18,11 @@ using MouseButton = System.Windows.Input.MouseButton; namespace Avalonia.Win32.Interop.Wpf { - class WpfTopLevelImpl : FrameworkElement, IEmbeddableWindowImpl + class WpfTopLevelImpl : FrameworkElement, ITopLevelImpl { private HwndSource _currentHwndSource; private readonly HwndSourceHook _hook; - private readonly IEmbeddableWindowImpl _ttl; + private readonly ITopLevelImpl _ttl; private IInputRoot _inputRoot; private readonly IEnumerable _surfaces; private readonly IMouseDevice _mouse; @@ -241,7 +241,7 @@ namespace Avalonia.Win32.Interop.Wpf Action ITopLevelImpl.TransparencyLevelChanged { get; set; } Action ITopLevelImpl.Closed { get; set; } - public new event Action LostFocus; + public new Action LostFocus { get; set; } internal Vector GetScaling() { @@ -256,5 +256,7 @@ namespace Avalonia.Win32.Interop.Wpf public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { } public WindowTransparencyLevel TransparencyLevel { get; private set; } + + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); } } diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index 67db56dd1b..49700710e9 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -6,6 +6,7 @@ + - + diff --git a/src/Windows/Avalonia.Win32/ClipboardImpl.cs b/src/Windows/Avalonia.Win32/ClipboardImpl.cs index 1c94d7a446..7d9e0a8bd2 100644 --- a/src/Windows/Avalonia.Win32/ClipboardImpl.cs +++ b/src/Windows/Avalonia.Win32/ClipboardImpl.cs @@ -1,25 +1,30 @@ using System; +using System.Linq; +using System.Reactive.Disposables; using System.Runtime.InteropServices; using System.Threading.Tasks; +using Avalonia.Input; using Avalonia.Input.Platform; +using Avalonia.Threading; using Avalonia.Win32.Interop; namespace Avalonia.Win32 { internal class ClipboardImpl : IClipboard { - private async Task OpenClipboard() + private async Task OpenClipboard() { while (!UnmanagedMethods.OpenClipboard(IntPtr.Zero)) { await Task.Delay(100); } + + return Disposable.Create(() => UnmanagedMethods.CloseClipboard()); } public async Task GetTextAsync() { - await OpenClipboard(); - try + using(await OpenClipboard()) { IntPtr hText = UnmanagedMethods.GetClipboardData(UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT); if (hText == IntPtr.Zero) @@ -37,10 +42,6 @@ namespace Avalonia.Win32 UnmanagedMethods.GlobalUnlock(hText); return rv; } - finally - { - UnmanagedMethods.CloseClipboard(); - } } public async Task SetTextAsync(string text) @@ -50,31 +51,66 @@ namespace Avalonia.Win32 throw new ArgumentNullException(nameof(text)); } - await OpenClipboard(); - - UnmanagedMethods.EmptyClipboard(); - - try + using(await OpenClipboard()) { + UnmanagedMethods.EmptyClipboard(); + var hGlobal = Marshal.StringToHGlobalUni(text); UnmanagedMethods.SetClipboardData(UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT, hGlobal); } - finally - { - UnmanagedMethods.CloseClipboard(); - } } public async Task ClearAsync() { - await OpenClipboard(); - try + using(await OpenClipboard()) { UnmanagedMethods.EmptyClipboard(); } - finally + } + + public async Task SetDataObjectAsync(IDataObject data) + { + Dispatcher.UIThread.VerifyAccess(); + var wrapper = new DataObject(data); + while (true) { - UnmanagedMethods.CloseClipboard(); + if (UnmanagedMethods.OleSetClipboard(wrapper) == 0) + break; + await Task.Delay(100); + } + } + + public async Task GetFormatsAsync() + { + Dispatcher.UIThread.VerifyAccess(); + while (true) + { + if (UnmanagedMethods.OleGetClipboard(out var dataObject) == 0) + { + var wrapper = new OleDataObject(dataObject); + var formats = wrapper.GetDataFormats().ToArray(); + Marshal.ReleaseComObject(dataObject); + return formats; + } + + await Task.Delay(100); + } + } + + public async Task GetDataAsync(string format) + { + Dispatcher.UIThread.VerifyAccess(); + while (true) + { + if (UnmanagedMethods.OleGetClipboard(out var dataObject) == 0) + { + var wrapper = new OleDataObject(dataObject); + var rv = wrapper.Get(format); + Marshal.ReleaseComObject(dataObject); + return rv; + } + + await Task.Delay(100); } } } diff --git a/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs b/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs index 34f2894657..e08f1b28be 100644 --- a/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs +++ b/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs @@ -6,11 +6,8 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32 { - class EmbeddedWindowImpl : WindowImpl, IEmbeddableWindowImpl + class EmbeddedWindowImpl : WindowImpl { - private static IntPtr DefaultParentWindow = CreateParentWindow(); - private static UnmanagedMethods.WndProc _wndProcDelegate; - protected override IntPtr CreateWindowOverride(ushort atom) { var hWnd = UnmanagedMethods.CreateWindowEx( @@ -22,66 +19,12 @@ namespace Avalonia.Win32 0, 640, 480, - DefaultParentWindow, + OffscreenParentWindow.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); return hWnd; } - protected override IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) - { - if (msg == (uint)UnmanagedMethods.WindowsMessage.WM_KILLFOCUS) - LostFocus?.Invoke(); - return base.WndProc(hWnd, msg, wParam, lParam); - } - - public event Action LostFocus; - - private static IntPtr CreateParentWindow() - { - _wndProcDelegate = new UnmanagedMethods.WndProc(ParentWndProc); - - var wndClassEx = new UnmanagedMethods.WNDCLASSEX - { - cbSize = Marshal.SizeOf(), - hInstance = UnmanagedMethods.GetModuleHandle(null), - lpfnWndProc = _wndProcDelegate, - lpszClassName = "AvaloniaEmbeddedWindow-" + Guid.NewGuid(), - }; - - var atom = UnmanagedMethods.RegisterClassEx(ref wndClassEx); - - if (atom == 0) - { - throw new Win32Exception(); - } - - var hwnd = UnmanagedMethods.CreateWindowEx( - 0, - atom, - null, - (int)UnmanagedMethods.WindowStyles.WS_OVERLAPPEDWINDOW, - UnmanagedMethods.CW_USEDEFAULT, - UnmanagedMethods.CW_USEDEFAULT, - UnmanagedMethods.CW_USEDEFAULT, - UnmanagedMethods.CW_USEDEFAULT, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero); - - if (hwnd == IntPtr.Zero) - { - throw new Win32Exception(); - } - - return hwnd; - } - - private static IntPtr ParentWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) - { - return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); - } } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index da52c3116e..1aec4f0016 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -15,7 +15,7 @@ namespace Avalonia.Win32.Interop [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Using Win32 naming for consistency.")] [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements must be documented", Justification = "Look in Win32 docs.")] [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1602:Enumeration items must be documented", Justification = "Look in Win32 docs.")] - internal static class UnmanagedMethods + internal unsafe static class UnmanagedMethods { public const int CW_USEDEFAULT = unchecked((int)0x80000000); @@ -989,6 +989,12 @@ namespace Avalonia.Win32.Interop } } + [DllImport("user32.dll")] + public static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert); + + [DllImport("user32.dll")] + public static extern bool EnableMenuItem(IntPtr hMenu, uint uIDEnableItem, uint uEnable); + [DllImport("user32.dll", SetLastError = true)] public static extern bool GetWindowPlacement(IntPtr hWnd, ref WINDOWPLACEMENT lpwndpl); @@ -1001,6 +1007,10 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool InvalidateRect(IntPtr hWnd, ref RECT lpRect, bool bErase); + + [DllImport("user32.dll")] + public static extern bool InvalidateRect(IntPtr hWnd, RECT* lpRect, bool bErase); + [DllImport("user32.dll")] public static extern bool IsWindowEnabled(IntPtr hWnd); @@ -1051,6 +1061,8 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool SetParent(IntPtr hWnd, IntPtr hWndNewParent); [DllImport("user32.dll")] + public static extern IntPtr GetParent(IntPtr hWnd); + [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, ShowWindowCommand nCmdShow); [DllImport("kernel32.dll", SetLastError = true)] @@ -1169,6 +1181,12 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern IntPtr SetClipboardData(ClipboardFormat uFormat, IntPtr hMem); + [DllImport("ole32.dll", PreserveSig = false)] + public static extern int OleGetClipboard(out IOleDataObject dataObject); + + [DllImport("ole32.dll", PreserveSig = true)] + public static extern int OleSetClipboard(IOleDataObject dataObject); + [DllImport("kernel32.dll", ExactSpelling = true)] public static extern IntPtr GlobalLock(IntPtr handle); @@ -1305,6 +1323,9 @@ namespace Avalonia.Win32.Interop [DllImport("dwmapi.dll")] public static extern int DwmIsCompositionEnabled(out bool enabled); + [DllImport("dwmapi.dll")] + public static extern bool DwmDefWindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref IntPtr plResult); + [DllImport("dwmapi.dll")] public static extern void DwmEnableBlurBehindWindow(IntPtr hwnd, ref DWM_BLURBEHIND blurBehind); diff --git a/src/Windows/Avalonia.Win32/OffscreenParentWindow.cs b/src/Windows/Avalonia.Win32/OffscreenParentWindow.cs new file mode 100644 index 0000000000..9de105a3d5 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OffscreenParentWindow.cs @@ -0,0 +1,58 @@ +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using Avalonia.Platform; +using Avalonia.Win32.Interop; +namespace Avalonia.Win32 +{ + class OffscreenParentWindow + { + public static IntPtr Handle { get; } = CreateParentWindow(); + private static UnmanagedMethods.WndProc s_wndProcDelegate; + private static IntPtr CreateParentWindow() + { + s_wndProcDelegate = new UnmanagedMethods.WndProc(ParentWndProc); + + var wndClassEx = new UnmanagedMethods.WNDCLASSEX + { + cbSize = Marshal.SizeOf(), + hInstance = UnmanagedMethods.GetModuleHandle(null), + lpfnWndProc = s_wndProcDelegate, + lpszClassName = "AvaloniaEmbeddedWindow-" + Guid.NewGuid(), + }; + + var atom = UnmanagedMethods.RegisterClassEx(ref wndClassEx); + + if (atom == 0) + { + throw new Win32Exception(); + } + + var hwnd = UnmanagedMethods.CreateWindowEx( + 0, + atom, + null, + (int)UnmanagedMethods.WindowStyles.WS_OVERLAPPEDWINDOW, + UnmanagedMethods.CW_USEDEFAULT, + UnmanagedMethods.CW_USEDEFAULT, + UnmanagedMethods.CW_USEDEFAULT, + UnmanagedMethods.CW_USEDEFAULT, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero); + + if (hwnd == IntPtr.Zero) + { + throw new Win32Exception(); + } + + return hwnd; + } + + private static IntPtr ParentWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); + } + } +} diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index 504cefb9b3..cd25b32ed9 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -7,9 +7,54 @@ namespace Avalonia.Win32 { class PopupImpl : WindowImpl, IPopupImpl { + private bool _dropShadowHint = true; + private Size? _maxAutoSize; + + + // This is needed because we are calling virtual methods from constructors + // One fabulous design decision leads to another, I guess + [ThreadStatic] + private static IntPtr s_parentHandle; + public override void Show() { UnmanagedMethods.ShowWindow(Handle.Handle, UnmanagedMethods.ShowWindowCommand.ShowNoActivate); + var parent = UnmanagedMethods.GetParent(Handle.Handle); + if (parent != IntPtr.Zero) + { + IntPtr nextParent = parent; + while (nextParent != IntPtr.Zero) + { + parent = nextParent; + nextParent = UnmanagedMethods.GetParent(parent); + } + + UnmanagedMethods.SetFocus(parent); + } + } + + protected override bool ShouldTakeFocusOnClick => false; + + public override Size MaxAutoSizeHint + { + get + { + if (_maxAutoSize is null) + { + var monitor = UnmanagedMethods.MonitorFromWindow( + Hwnd, + UnmanagedMethods.MONITOR.MONITOR_DEFAULTTONEAREST); + + if (monitor != IntPtr.Zero) + { + var info = UnmanagedMethods.MONITORINFO.Create(); + UnmanagedMethods.GetMonitorInfo(monitor, ref info); + _maxAutoSize = info.rcWork.ToPixelRect().ToRect(Scaling).Size; + } + } + + return _maxAutoSize ?? Size.Infinity; + } } protected override IntPtr CreateWindowOverride(ushort atom) @@ -31,16 +76,13 @@ namespace Avalonia.Win32 UnmanagedMethods.CW_USEDEFAULT, UnmanagedMethods.CW_USEDEFAULT, UnmanagedMethods.CW_USEDEFAULT, - IntPtr.Zero, + s_parentHandle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + s_parentHandle = IntPtr.Zero; - var classes = (int)UnmanagedMethods.GetClassLongPtr(result, (int)UnmanagedMethods.ClassLongIndex.GCL_STYLE); - - classes |= (int)UnmanagedMethods.ClassStyles.CS_DROPSHADOW; - - UnmanagedMethods.SetClassLong(result, UnmanagedMethods.ClassLongIndex.GCL_STYLE, new IntPtr(classes)); + EnableBoxShadow(result, _dropShadowHint); return result; } @@ -49,6 +91,9 @@ namespace Avalonia.Win32 { switch ((UnmanagedMethods.WindowsMessage)msg) { + case UnmanagedMethods.WindowsMessage.WM_DISPLAYCHANGE: + _maxAutoSize = null; + goto default; case UnmanagedMethods.WindowsMessage.WM_MOUSEACTIVATE: return (IntPtr)UnmanagedMethods.MouseActivate.MA_NOACTIVATE; default: @@ -56,7 +101,22 @@ namespace Avalonia.Win32 } } - public PopupImpl(IWindowBaseImpl parent) + // This is needed because we are calling virtual methods from constructors + // One fabulous design decision leads to another, I guess + static IWindowBaseImpl SaveParentHandle(IWindowBaseImpl parent) + { + s_parentHandle = parent.Handle.Handle; + return parent; + } + + // This is needed because we are calling virtual methods from constructors + // One fabulous design decision leads to another, I guess + public PopupImpl(IWindowBaseImpl parent) : this(SaveParentHandle(parent), false) + { + + } + + private PopupImpl(IWindowBaseImpl parent, bool dummy) : base() { PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); } @@ -68,6 +128,32 @@ namespace Avalonia.Win32 //TODO: We ignore the scaling override for now } + private void EnableBoxShadow (IntPtr hwnd, bool enabled) + { + var classes = (int)UnmanagedMethods.GetClassLongPtr(hwnd, (int)UnmanagedMethods.ClassLongIndex.GCL_STYLE); + + if (enabled) + { + classes |= (int)UnmanagedMethods.ClassStyles.CS_DROPSHADOW; + } + else + { + classes &= ~(int)UnmanagedMethods.ClassStyles.CS_DROPSHADOW; + } + + UnmanagedMethods.SetClassLong(hwnd, UnmanagedMethods.ClassLongIndex.GCL_STYLE, new IntPtr(classes)); + } + + public void SetWindowManagerAddShadowHint(bool enabled) + { + _dropShadowHint = enabled; + + if (Handle != null) + { + EnableBoxShadow(Handle.Handle, enabled); + } + } + public IPopupPositioner PopupPositioner { get; } } } diff --git a/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs b/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs new file mode 100644 index 0000000000..d7bb2c037e --- /dev/null +++ b/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs @@ -0,0 +1,205 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Controls.Platform; +using Avalonia.Platform; +using Avalonia.VisualTree; +using Avalonia.Win32.Interop; + +namespace Avalonia.Win32 +{ + class Win32NativeControlHost : INativeControlHostImpl + { + public WindowImpl Window { get; } + + public Win32NativeControlHost(WindowImpl window) + { + Window = window; + } + + void AssertCompatible(IPlatformHandle handle) + { + if (!IsCompatibleWith(handle)) + throw new ArgumentException($"Don't know what to do with {handle.HandleDescriptor}"); + } + + public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent) + { + AssertCompatible(parent); + return new DumbWindow(parent.Handle); + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func create) + { + var holder = new DumbWindow(Window.Handle.Handle); + Win32NativeControlAttachment attachment = null; + try + { + var child = create(holder); + // ReSharper disable once UseObjectOrCollectionInitializer + // It has to be assigned to the variable before property setter is called so we dispose it on exception + attachment = new Win32NativeControlAttachment(holder, child); + attachment.AttachedTo = this; + return attachment; + } + catch + { + attachment?.Dispose(); + holder?.Destroy(); + throw; + } + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle) + { + AssertCompatible(handle); + return new Win32NativeControlAttachment(new DumbWindow(Window.Handle.Handle), + handle) { AttachedTo = this }; + } + + public bool IsCompatibleWith(IPlatformHandle handle) => handle.HandleDescriptor == "HWND"; + + class DumbWindow : IDisposable, INativeControlHostDestroyableControlHandle + { + public IntPtr Handle { get;} + public string HandleDescriptor => "HWND"; + public void Destroy() => Dispose(); + + UnmanagedMethods.WndProc _wndProcDelegate; + private readonly string _className; + + public DumbWindow(IntPtr? parent = null) + { + _wndProcDelegate = WndProc; + var wndClassEx = new UnmanagedMethods.WNDCLASSEX + { + cbSize = Marshal.SizeOf(), + hInstance = UnmanagedMethods.GetModuleHandle(null), + lpfnWndProc = _wndProcDelegate, + lpszClassName = _className = "AvaloniaDumbWindow-" + Guid.NewGuid(), + }; + + var atom = UnmanagedMethods.RegisterClassEx(ref wndClassEx); + Handle = UnmanagedMethods.CreateWindowEx( + 0, + atom, + null, + (int)UnmanagedMethods.WindowStyles.WS_CHILD, + 0, + 0, + 640, + 480, + parent ?? OffscreenParentWindow.Handle, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero); + } + + + + protected virtual unsafe IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); + } + + private void ReleaseUnmanagedResources() + { + UnmanagedMethods.DestroyWindow(Handle); + UnmanagedMethods.UnregisterClass(_className, UnmanagedMethods.GetModuleHandle(null)); + } + + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + ~DumbWindow() + { + ReleaseUnmanagedResources(); + } + } + + class Win32NativeControlAttachment : INativeControlHostControlTopLevelAttachment + { + private DumbWindow _holder; + private IPlatformHandle _child; + private Win32NativeControlHost _attachedTo; + + public Win32NativeControlAttachment(DumbWindow holder, IPlatformHandle child) + { + _holder = holder; + _child = child; + UnmanagedMethods.SetParent(child.Handle, _holder.Handle); + UnmanagedMethods.ShowWindow(child.Handle, UnmanagedMethods.ShowWindowCommand.Show); + } + + void CheckDisposed() + { + if (_holder == null) + throw new ObjectDisposedException(nameof(Win32NativeControlAttachment)); + } + + public void Dispose() + { + if (_child != null) + UnmanagedMethods.SetParent(_child.Handle, OffscreenParentWindow.Handle); + _holder?.Dispose(); + _holder = null; + _child = null; + _attachedTo = null; + } + + public INativeControlHostImpl AttachedTo + { + get => _attachedTo; + set + { + CheckDisposed(); + _attachedTo = (Win32NativeControlHost) value; + if (_attachedTo == null) + { + UnmanagedMethods.ShowWindow(_holder.Handle, UnmanagedMethods.ShowWindowCommand.Hide); + UnmanagedMethods.SetParent(_holder.Handle, OffscreenParentWindow.Handle); + } + else + UnmanagedMethods.SetParent(_holder.Handle, _attachedTo.Window.Handle.Handle); + } + } + + public bool IsCompatibleWith(INativeControlHostImpl host) => host is Win32NativeControlHost; + + public void HideWithSize(Size size) + { + UnmanagedMethods.SetWindowPos(_holder.Handle, IntPtr.Zero, + -100, -100, 1, 1, + UnmanagedMethods.SetWindowPosFlags.SWP_HIDEWINDOW | + UnmanagedMethods.SetWindowPosFlags.SWP_NOACTIVATE); + if (_attachedTo == null || _child == null) + return; + size *= _attachedTo.Window.Scaling; + UnmanagedMethods.MoveWindow(_child.Handle, 0, 0, + Math.Max(1, (int)size.Width), Math.Max(1, (int)size.Height), false); + } + + public unsafe void ShowInBounds(Rect bounds) + { + CheckDisposed(); + if (_attachedTo == null) + throw new InvalidOperationException("The control isn't currently attached to a toplevel"); + bounds *= _attachedTo.Window.Scaling; + var pixelRect = new PixelRect((int)bounds.X, (int)bounds.Y, Math.Max(1, (int)bounds.Width), + Math.Max(1, (int)bounds.Height)); + + UnmanagedMethods.MoveWindow(_child.Handle, 0, 0, pixelRect.Width, pixelRect.Height, true); + UnmanagedMethods.SetWindowPos(_holder.Handle, IntPtr.Zero, pixelRect.X, pixelRect.Y, pixelRect.Width, + pixelRect.Height, + UnmanagedMethods.SetWindowPosFlags.SWP_SHOWWINDOW + | UnmanagedMethods.SetWindowPosFlags.SWP_NOZORDER + | UnmanagedMethods.SetWindowPosFlags.SWP_NOACTIVATE); + + UnmanagedMethods.InvalidateRect(_attachedTo.Window.Handle.Handle, null, false); + } + } + + } +} diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index e25c5e4733..b7bb0e19ba 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -36,7 +36,7 @@ namespace Avalonia public class Win32PlatformOptions { public bool UseDeferredRendering { get; set; } = true; - public bool AllowEglInitialization { get; set; } + public bool AllowEglInitialization { get; set; } = true; public bool? EnableMultitouch { get; set; } public bool OverlayPopups { get; set; } } @@ -209,7 +209,7 @@ namespace Avalonia.Win32 return new WindowImpl(); } - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public IWindowImpl CreateEmbeddableWindow() { var embedded = new EmbeddedWindowImpl(); embedded.Show(); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs new file mode 100644 index 0000000000..0ba1d311bc --- /dev/null +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -0,0 +1,539 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Win32.Input; +using static Avalonia.Win32.Interop.UnmanagedMethods; + +namespace Avalonia.Win32 +{ + public partial class WindowImpl + { + [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", + Justification = "Using Win32 naming for consistency.")] + protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + const double wheelDelta = 120.0; + uint timestamp = unchecked((uint)GetMessageTime()); + + RawInputEventArgs e = null; + var shouldTakeFocus = false; + + switch ((WindowsMessage)msg) + { + case WindowsMessage.WM_ACTIVATE: + { + var wa = (WindowActivate)(ToInt32(wParam) & 0xffff); + + switch (wa) + { + case WindowActivate.WA_ACTIVE: + case WindowActivate.WA_CLICKACTIVE: + { + Activated?.Invoke(); + break; + } + + case WindowActivate.WA_INACTIVE: + { + Deactivated?.Invoke(); + break; + } + } + + return IntPtr.Zero; + } + + case WindowsMessage.WM_NCCALCSIZE: + { + if (ToInt32(wParam) == 1 && !HasFullDecorations || _isClientAreaExtended) + { + return IntPtr.Zero; + } + + break; + } + + case WindowsMessage.WM_CLOSE: + { + bool? preventClosing = Closing?.Invoke(); + if (preventClosing == true) + { + return IntPtr.Zero; + } + + break; + } + + case WindowsMessage.WM_DESTROY: + { + //Window doesn't exist anymore + _hwnd = IntPtr.Zero; + //Remove root reference to this class, so unmanaged delegate can be collected + s_instances.Remove(this); + Closed?.Invoke(); + + _mouseDevice.Dispose(); + _touchDevice?.Dispose(); + //Free other resources + Dispose(); + return IntPtr.Zero; + } + + case WindowsMessage.WM_DPICHANGED: + { + var dpi = ToInt32(wParam) & 0xffff; + var newDisplayRect = Marshal.PtrToStructure(lParam); + _scaling = dpi / 96.0; + ScalingChanged?.Invoke(_scaling); + SetWindowPos(hWnd, + IntPtr.Zero, + newDisplayRect.left, + newDisplayRect.top, + newDisplayRect.right - newDisplayRect.left, + newDisplayRect.bottom - newDisplayRect.top, + SetWindowPosFlags.SWP_NOZORDER | + SetWindowPosFlags.SWP_NOACTIVATE); + return IntPtr.Zero; + } + + case WindowsMessage.WM_KEYDOWN: + case WindowsMessage.WM_SYSKEYDOWN: + { + e = new RawKeyEventArgs( + WindowsKeyboardDevice.Instance, + timestamp, + _owner, + RawKeyEventType.KeyDown, + KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), + WindowsKeyboardDevice.Instance.Modifiers); + break; + } + + case WindowsMessage.WM_MENUCHAR: + { + // mute the system beep + return (IntPtr)((int)MenuCharParam.MNC_CLOSE << 16); + } + + case WindowsMessage.WM_KEYUP: + case WindowsMessage.WM_SYSKEYUP: + { + e = new RawKeyEventArgs( + WindowsKeyboardDevice.Instance, + timestamp, + _owner, + RawKeyEventType.KeyUp, + KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), + WindowsKeyboardDevice.Instance.Modifiers); + break; + } + case WindowsMessage.WM_CHAR: + { + // Ignore control chars + if (ToInt32(wParam) >= 32) + { + e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, _owner, + new string((char)ToInt32(wParam), 1)); + } + + break; + } + + case WindowsMessage.WM_LBUTTONDOWN: + case WindowsMessage.WM_RBUTTONDOWN: + case WindowsMessage.WM_MBUTTONDOWN: + case WindowsMessage.WM_XBUTTONDOWN: + { + shouldTakeFocus = ShouldTakeFocusOnClick; + if (ShouldIgnoreTouchEmulatedMessage()) + { + break; + } + + e = new RawPointerEventArgs( + _mouseDevice, + timestamp, + _owner, + (WindowsMessage)msg switch + { + WindowsMessage.WM_LBUTTONDOWN => RawPointerEventType.LeftButtonDown, + WindowsMessage.WM_RBUTTONDOWN => RawPointerEventType.RightButtonDown, + WindowsMessage.WM_MBUTTONDOWN => RawPointerEventType.MiddleButtonDown, + WindowsMessage.WM_XBUTTONDOWN => + HighWord(ToInt32(wParam)) == 1 ? + RawPointerEventType.XButton1Down : + RawPointerEventType.XButton2Down + }, + DipFromLParam(lParam), GetMouseModifiers(wParam)); + break; + } + + case WindowsMessage.WM_LBUTTONUP: + case WindowsMessage.WM_RBUTTONUP: + case WindowsMessage.WM_MBUTTONUP: + case WindowsMessage.WM_XBUTTONUP: + { + shouldTakeFocus = ShouldTakeFocusOnClick; + if (ShouldIgnoreTouchEmulatedMessage()) + { + break; + } + + e = new RawPointerEventArgs( + _mouseDevice, + timestamp, + _owner, + (WindowsMessage)msg switch + { + WindowsMessage.WM_LBUTTONUP => RawPointerEventType.LeftButtonUp, + WindowsMessage.WM_RBUTTONUP => RawPointerEventType.RightButtonUp, + WindowsMessage.WM_MBUTTONUP => RawPointerEventType.MiddleButtonUp, + WindowsMessage.WM_XBUTTONUP => + HighWord(ToInt32(wParam)) == 1 ? + RawPointerEventType.XButton1Up : + RawPointerEventType.XButton2Up, + }, + DipFromLParam(lParam), GetMouseModifiers(wParam)); + break; + } + + case WindowsMessage.WM_MOUSEMOVE: + { + if (ShouldIgnoreTouchEmulatedMessage()) + { + break; + } + + if (!_trackingMouse) + { + var tm = new TRACKMOUSEEVENT + { + cbSize = Marshal.SizeOf(), + dwFlags = 2, + hwndTrack = _hwnd, + dwHoverTime = 0, + }; + + TrackMouseEvent(ref tm); + } + + e = new RawPointerEventArgs( + _mouseDevice, + timestamp, + _owner, + RawPointerEventType.Move, + DipFromLParam(lParam), GetMouseModifiers(wParam)); + + break; + } + + case WindowsMessage.WM_MOUSEWHEEL: + { + e = new RawMouseWheelEventArgs( + _mouseDevice, + timestamp, + _owner, + PointToClient(PointFromLParam(lParam)), + new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta), GetMouseModifiers(wParam)); + break; + } + + case WindowsMessage.WM_MOUSEHWHEEL: + { + e = new RawMouseWheelEventArgs( + _mouseDevice, + timestamp, + _owner, + PointToClient(PointFromLParam(lParam)), + new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0), GetMouseModifiers(wParam)); + break; + } + + case WindowsMessage.WM_MOUSELEAVE: + { + _trackingMouse = false; + e = new RawPointerEventArgs( + _mouseDevice, + timestamp, + _owner, + RawPointerEventType.LeaveWindow, + new Point(-1, -1), WindowsKeyboardDevice.Instance.Modifiers); + break; + } + + case WindowsMessage.WM_NCLBUTTONDOWN: + case WindowsMessage.WM_NCRBUTTONDOWN: + case WindowsMessage.WM_NCMBUTTONDOWN: + case WindowsMessage.WM_NCXBUTTONDOWN: + { + e = new RawPointerEventArgs( + _mouseDevice, + timestamp, + _owner, + (WindowsMessage)msg switch + { + WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType + .NonClientLeftButtonDown, + WindowsMessage.WM_NCRBUTTONDOWN => RawPointerEventType.RightButtonDown, + WindowsMessage.WM_NCMBUTTONDOWN => RawPointerEventType.MiddleButtonDown, + WindowsMessage.WM_NCXBUTTONDOWN => + HighWord(ToInt32(wParam)) == 1 ? + RawPointerEventType.XButton1Down : + RawPointerEventType.XButton2Down, + }, + PointToClient(PointFromLParam(lParam)), GetMouseModifiers(wParam)); + break; + } + case WindowsMessage.WM_TOUCH: + { + var touchInputCount = wParam.ToInt32(); + + var pTouchInputs = stackalloc TOUCHINPUT[touchInputCount]; + var touchInputs = new Span(pTouchInputs, touchInputCount); + + if (GetTouchInputInfo(lParam, (uint)touchInputCount, pTouchInputs, Marshal.SizeOf())) + { + foreach (var touchInput in touchInputs) + { + Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time, + _owner, + touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_UP) ? + RawPointerEventType.TouchEnd : + touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_DOWN) ? + RawPointerEventType.TouchBegin : + RawPointerEventType.TouchUpdate, + PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)), + WindowsKeyboardDevice.Instance.Modifiers, + touchInput.Id)); + } + + CloseTouchInputHandle(lParam); + return IntPtr.Zero; + } + + break; + } + case WindowsMessage.WM_NCPAINT: + { + if (!HasFullDecorations) + { + return IntPtr.Zero; + } + + break; + } + + case WindowsMessage.WM_NCACTIVATE: + { + if (!HasFullDecorations) + { + return new IntPtr(1); + } + + break; + } + + case WindowsMessage.WM_PAINT: + { + using (_rendererLock.Lock()) + { + if (BeginPaint(_hwnd, out PAINTSTRUCT ps) != IntPtr.Zero) + { + var f = Scaling; + var r = ps.rcPaint; + Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f, + (r.bottom - r.top) / f)); + EndPaint(_hwnd, ref ps); + } + } + + return IntPtr.Zero; + } + + case WindowsMessage.WM_SIZE: + { + using (_rendererLock.Lock()) + { + // Do nothing here, just block until the pending frame render is completed on the render thread + } + + var size = (SizeCommand)wParam; + + if (Resized != null && + (size == SizeCommand.Restored || + size == SizeCommand.Maximized)) + { + var clientSize = new Size(ToInt32(lParam) & 0xffff, ToInt32(lParam) >> 16); + Resized(clientSize / Scaling); + } + + var windowState = size == SizeCommand.Maximized ? + WindowState.Maximized : + (size == SizeCommand.Minimized ? WindowState.Minimized : WindowState.Normal); + + if (windowState != _lastWindowState) + { + _lastWindowState = windowState; + + WindowStateChanged?.Invoke(windowState); + + if (_isClientAreaExtended) + { + UpdateExtendMargins(); + + ExtendClientAreaToDecorationsChanged?.Invoke(true); + } + } + + return IntPtr.Zero; + } + + case WindowsMessage.WM_MOVE: + { + PositionChanged?.Invoke(new PixelPoint((short)(ToInt32(lParam) & 0xffff), + (short)(ToInt32(lParam) >> 16))); + return IntPtr.Zero; + } + + case WindowsMessage.WM_GETMINMAXINFO: + { + MINMAXINFO mmi = Marshal.PtrToStructure(lParam); + + if (_minSize.Width > 0) + { + mmi.ptMinTrackSize.X = + (int)((_minSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right); + } + + if (_minSize.Height > 0) + { + mmi.ptMinTrackSize.Y = + (int)((_minSize.Height * Scaling) + BorderThickness.Top + BorderThickness.Bottom); + } + + if (!double.IsInfinity(_maxSize.Width) && _maxSize.Width > 0) + { + mmi.ptMaxTrackSize.X = + (int)((_maxSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right); + } + + if (!double.IsInfinity(_maxSize.Height) && _maxSize.Height > 0) + { + mmi.ptMaxTrackSize.Y = + (int)((_maxSize.Height * Scaling) + BorderThickness.Top + BorderThickness.Bottom); + } + + Marshal.StructureToPtr(mmi, lParam, true); + return IntPtr.Zero; + } + + case WindowsMessage.WM_DISPLAYCHANGE: + { + (Screen as ScreenImpl)?.InvalidateScreensCache(); + return IntPtr.Zero; + } + + case WindowsMessage.WM_KILLFOCUS: + LostFocus?.Invoke(); + break; + } + +#if USE_MANAGED_DRAG + if (_managedDrag.PreprocessInputEvent(ref e)) + return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); +#endif + + if(shouldTakeFocus) + { + SetFocus(_hwnd); + } + + if (e != null && Input != null) + { + Input(e); + + if (e.Handled) + { + return IntPtr.Zero; + } + } + + using (_rendererLock.Lock()) + { + return DefWindowProc(hWnd, msg, wParam, lParam); + } + } + + private static int ToInt32(IntPtr ptr) + { + if (IntPtr.Size == 4) + return ptr.ToInt32(); + + return (int)(ptr.ToInt64() & 0xffffffff); + } + + private static int HighWord(int param) => param >> 16; + + private Point DipFromLParam(IntPtr lParam) + { + return new Point((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)) / Scaling; + } + + private PixelPoint PointFromLParam(IntPtr lParam) + { + return new PixelPoint((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)); + } + + private bool ShouldIgnoreTouchEmulatedMessage() + { + if (!_multitouch) + { + return false; + } + + // MI_WP_SIGNATURE + // https://docs.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages + const long marker = 0xFF515700L; + + var info = GetMessageExtraInfo().ToInt64(); + return (info & marker) == marker; + } + + private static RawInputModifiers GetMouseModifiers(IntPtr wParam) + { + var keys = (ModifierKeys)ToInt32(wParam); + var modifiers = WindowsKeyboardDevice.Instance.Modifiers; + + if (keys.HasFlagCustom(ModifierKeys.MK_LBUTTON)) + { + modifiers |= RawInputModifiers.LeftMouseButton; + } + + if (keys.HasFlagCustom(ModifierKeys.MK_RBUTTON)) + { + modifiers |= RawInputModifiers.RightMouseButton; + } + + if (keys.HasFlagCustom(ModifierKeys.MK_MBUTTON)) + { + modifiers |= RawInputModifiers.MiddleMouseButton; + } + + if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON1)) + { + modifiers |= RawInputModifiers.XButton1MouseButton; + } + + if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON2)) + { + modifiers |= RawInputModifiers.XButton2MouseButton; + } + + return modifiers; + } + } +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs new file mode 100644 index 0000000000..2badf99f7f --- /dev/null +++ b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs @@ -0,0 +1,140 @@ +using System; +using System.Diagnostics; +using Avalonia.Controls; +using Avalonia.Input; +using static Avalonia.Win32.Interop.UnmanagedMethods; + +#nullable enable + +namespace Avalonia.Win32 +{ + public partial class WindowImpl + { + // Hit test the frame for resizing and moving. + HitTestValues HitTestNCA(IntPtr hWnd, IntPtr wParam, IntPtr lParam) + { + // Get the point coordinates for the hit test. + var ptMouse = PointFromLParam(lParam); + + // Get the window rectangle. + GetWindowRect(hWnd, out var rcWindow); + + // Get the frame rectangle, adjusted for the style without a caption. + RECT rcFrame = new RECT(); + AdjustWindowRectEx(ref rcFrame, (uint)(WindowStyles.WS_OVERLAPPEDWINDOW & ~WindowStyles.WS_CAPTION), false, 0); + + RECT border_thickness = new RECT(); + if (GetStyle().HasFlag(WindowStyles.WS_THICKFRAME)) + { + AdjustWindowRectEx(ref border_thickness, (uint)(GetStyle()), false, 0); + border_thickness.left *= -1; + border_thickness.top *= -1; + } + else if (GetStyle().HasFlag(WindowStyles.WS_BORDER)) + { + border_thickness = new RECT { bottom = 1, left = 1, right = 1, top = 1 }; + } + + if (_extendTitleBarHint >= 0) + { + border_thickness.top = (int)(_extendedMargins.Top * Scaling); + } + + // Determine if the hit test is for resizing. Default middle (1,1). + ushort uRow = 1; + ushort uCol = 1; + bool fOnResizeBorder = false; + + // Determine if the point is at the top or bottom of the window. + if (ptMouse.Y >= rcWindow.top && ptMouse.Y < rcWindow.top + border_thickness.top) + { + fOnResizeBorder = (ptMouse.Y < (rcWindow.top - rcFrame.top)); + uRow = 0; + } + else if (ptMouse.Y < rcWindow.bottom && ptMouse.Y >= rcWindow.bottom - border_thickness.bottom) + { + uRow = 2; + } + + // Determine if the point is at the left or right of the window. + if (ptMouse.X >= rcWindow.left && ptMouse.X < rcWindow.left + border_thickness.left) + { + uCol = 0; // left side + } + else if (ptMouse.X < rcWindow.right && ptMouse.X >= rcWindow.right - border_thickness.right) + { + uCol = 2; // right side + } + + // Hit test (HTTOPLEFT, ... HTBOTTOMRIGHT) + HitTestValues[][] hitTests = new[] + { + new []{ HitTestValues.HTTOPLEFT, fOnResizeBorder ? HitTestValues.HTTOP : HitTestValues.HTCAPTION, HitTestValues.HTTOPRIGHT }, + new []{ HitTestValues.HTLEFT, HitTestValues.HTNOWHERE, HitTestValues.HTRIGHT }, + new []{ HitTestValues.HTBOTTOMLEFT, HitTestValues.HTBOTTOM, HitTestValues.HTBOTTOMRIGHT }, + }; + + return hitTests[uRow][uCol]; + } + + protected virtual IntPtr CustomCaptionProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref bool callDwp) + { + IntPtr lRet = IntPtr.Zero; + + callDwp = !DwmDefWindowProc(hWnd, msg, wParam, lParam, ref lRet); + + switch ((WindowsMessage)msg) + { + case WindowsMessage.WM_DWMCOMPOSITIONCHANGED: + // TODO handle composition changed. + break; + + case WindowsMessage.WM_NCHITTEST: + if (lRet == IntPtr.Zero) + { + if(WindowState == WindowState.FullScreen) + { + return (IntPtr)HitTestValues.HTCLIENT; + } + var hittestResult = HitTestNCA(hWnd, wParam, lParam); + + lRet = (IntPtr)hittestResult; + + uint timestamp = unchecked((uint)GetMessageTime()); + + if (hittestResult == HitTestValues.HTCAPTION) + { + var position = PointToClient(PointFromLParam(lParam)); + + if (_owner is Window window) + { + var visual = window.Renderer.HitTestFirst(position, _owner as Window, x => + { + if (x is IInputElement ie && !ie.IsHitTestVisible) + { + return false; + } + + return true; + }); + + if (visual != null) + { + hittestResult = HitTestValues.HTCLIENT; + lRet = (IntPtr)hittestResult; + } + } + } + + if (hittestResult != HitTestValues.HTNOWHERE) + { + callDwp = false; + } + } + break; + } + + return lRet; + } + } +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs index 138553b962..07f2311be8 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Win32.Input; @@ -14,508 +15,26 @@ namespace Avalonia.Win32 { public partial class WindowImpl { - [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", - Justification = "Using Win32 naming for consistency.")] protected virtual unsafe IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { - const double wheelDelta = 120.0; - uint timestamp = unchecked((uint)GetMessageTime()); + IntPtr lRet = IntPtr.Zero; + bool callDwp = true; - RawInputEventArgs e = null; - - switch ((WindowsMessage)msg) + if (_isClientAreaExtended) { - case WindowsMessage.WM_ACTIVATE: - { - var wa = (WindowActivate)(ToInt32(wParam) & 0xffff); - - switch (wa) - { - case WindowActivate.WA_ACTIVE: - case WindowActivate.WA_CLICKACTIVE: - { - Activated?.Invoke(); - break; - } - - case WindowActivate.WA_INACTIVE: - { - Deactivated?.Invoke(); - break; - } - } - - return IntPtr.Zero; - } - - case WindowsMessage.WM_NCCALCSIZE: - { - if (ToInt32(wParam) == 1 && !HasFullDecorations) - { - return IntPtr.Zero; - } - - break; - } - - case WindowsMessage.WM_CLOSE: - { - bool? preventClosing = Closing?.Invoke(); - if (preventClosing == true) - { - return IntPtr.Zero; - } - - break; - } - - case WindowsMessage.WM_DESTROY: - { - //Window doesn't exist anymore - _hwnd = IntPtr.Zero; - //Remove root reference to this class, so unmanaged delegate can be collected - s_instances.Remove(this); - Closed?.Invoke(); - - _mouseDevice.Dispose(); - _touchDevice?.Dispose(); - //Free other resources - Dispose(); - return IntPtr.Zero; - } - - case WindowsMessage.WM_DPICHANGED: - { - var dpi = ToInt32(wParam) & 0xffff; - var newDisplayRect = Marshal.PtrToStructure(lParam); - _scaling = dpi / 96.0; - ScalingChanged?.Invoke(_scaling); - SetWindowPos(hWnd, - IntPtr.Zero, - newDisplayRect.left, - newDisplayRect.top, - newDisplayRect.right - newDisplayRect.left, - newDisplayRect.bottom - newDisplayRect.top, - SetWindowPosFlags.SWP_NOZORDER | - SetWindowPosFlags.SWP_NOACTIVATE); - return IntPtr.Zero; - } - - case WindowsMessage.WM_KEYDOWN: - case WindowsMessage.WM_SYSKEYDOWN: - { - e = new RawKeyEventArgs( - WindowsKeyboardDevice.Instance, - timestamp, - _owner, - RawKeyEventType.KeyDown, - KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), - WindowsKeyboardDevice.Instance.Modifiers); - break; - } - - case WindowsMessage.WM_MENUCHAR: - { - // mute the system beep - return (IntPtr)((int)MenuCharParam.MNC_CLOSE << 16); - } - - case WindowsMessage.WM_KEYUP: - case WindowsMessage.WM_SYSKEYUP: - { - e = new RawKeyEventArgs( - WindowsKeyboardDevice.Instance, - timestamp, - _owner, - RawKeyEventType.KeyUp, - KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), - WindowsKeyboardDevice.Instance.Modifiers); - break; - } - case WindowsMessage.WM_CHAR: - { - // Ignore control chars - if (ToInt32(wParam) >= 32) - { - e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, _owner, - new string((char)ToInt32(wParam), 1)); - } - - break; - } - - case WindowsMessage.WM_LBUTTONDOWN: - case WindowsMessage.WM_RBUTTONDOWN: - case WindowsMessage.WM_MBUTTONDOWN: - case WindowsMessage.WM_XBUTTONDOWN: - { - if (ShouldIgnoreTouchEmulatedMessage()) - { - break; - } - - e = new RawPointerEventArgs( - _mouseDevice, - timestamp, - _owner, - (WindowsMessage)msg switch - { - WindowsMessage.WM_LBUTTONDOWN => RawPointerEventType.LeftButtonDown, - WindowsMessage.WM_RBUTTONDOWN => RawPointerEventType.RightButtonDown, - WindowsMessage.WM_MBUTTONDOWN => RawPointerEventType.MiddleButtonDown, - WindowsMessage.WM_XBUTTONDOWN => - HighWord(ToInt32(wParam)) == 1 ? - RawPointerEventType.XButton1Down : - RawPointerEventType.XButton2Down - }, - DipFromLParam(lParam), GetMouseModifiers(wParam)); - break; - } - - case WindowsMessage.WM_LBUTTONUP: - case WindowsMessage.WM_RBUTTONUP: - case WindowsMessage.WM_MBUTTONUP: - case WindowsMessage.WM_XBUTTONUP: - { - if (ShouldIgnoreTouchEmulatedMessage()) - { - break; - } - - e = new RawPointerEventArgs( - _mouseDevice, - timestamp, - _owner, - (WindowsMessage)msg switch - { - WindowsMessage.WM_LBUTTONUP => RawPointerEventType.LeftButtonUp, - WindowsMessage.WM_RBUTTONUP => RawPointerEventType.RightButtonUp, - WindowsMessage.WM_MBUTTONUP => RawPointerEventType.MiddleButtonUp, - WindowsMessage.WM_XBUTTONUP => - HighWord(ToInt32(wParam)) == 1 ? - RawPointerEventType.XButton1Up : - RawPointerEventType.XButton2Up, - }, - DipFromLParam(lParam), GetMouseModifiers(wParam)); - break; - } - - case WindowsMessage.WM_MOUSEMOVE: - { - if (ShouldIgnoreTouchEmulatedMessage()) - { - break; - } - - if (!_trackingMouse) - { - var tm = new TRACKMOUSEEVENT - { - cbSize = Marshal.SizeOf(), - dwFlags = 2, - hwndTrack = _hwnd, - dwHoverTime = 0, - }; - - TrackMouseEvent(ref tm); - } - - e = new RawPointerEventArgs( - _mouseDevice, - timestamp, - _owner, - RawPointerEventType.Move, - DipFromLParam(lParam), GetMouseModifiers(wParam)); - - break; - } - - case WindowsMessage.WM_MOUSEWHEEL: - { - e = new RawMouseWheelEventArgs( - _mouseDevice, - timestamp, - _owner, - PointToClient(PointFromLParam(lParam)), - new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta), GetMouseModifiers(wParam)); - break; - } - - case WindowsMessage.WM_MOUSEHWHEEL: - { - e = new RawMouseWheelEventArgs( - _mouseDevice, - timestamp, - _owner, - PointToClient(PointFromLParam(lParam)), - new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0), GetMouseModifiers(wParam)); - break; - } - - case WindowsMessage.WM_MOUSELEAVE: - { - _trackingMouse = false; - e = new RawPointerEventArgs( - _mouseDevice, - timestamp, - _owner, - RawPointerEventType.LeaveWindow, - new Point(-1, -1), WindowsKeyboardDevice.Instance.Modifiers); - break; - } - - case WindowsMessage.WM_NCLBUTTONDOWN: - case WindowsMessage.WM_NCRBUTTONDOWN: - case WindowsMessage.WM_NCMBUTTONDOWN: - case WindowsMessage.WM_NCXBUTTONDOWN: - { - e = new RawPointerEventArgs( - _mouseDevice, - timestamp, - _owner, - (WindowsMessage)msg switch - { - WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType - .NonClientLeftButtonDown, - WindowsMessage.WM_NCRBUTTONDOWN => RawPointerEventType.RightButtonDown, - WindowsMessage.WM_NCMBUTTONDOWN => RawPointerEventType.MiddleButtonDown, - WindowsMessage.WM_NCXBUTTONDOWN => - HighWord(ToInt32(wParam)) == 1 ? - RawPointerEventType.XButton1Down : - RawPointerEventType.XButton2Down, - }, - PointToClient(PointFromLParam(lParam)), GetMouseModifiers(wParam)); - break; - } - case WindowsMessage.WM_TOUCH: - { - var touchInputCount = wParam.ToInt32(); - - var pTouchInputs = stackalloc TOUCHINPUT[touchInputCount]; - var touchInputs = new Span(pTouchInputs, touchInputCount); - - if (GetTouchInputInfo(lParam, (uint)touchInputCount, pTouchInputs, Marshal.SizeOf())) - { - foreach (var touchInput in touchInputs) - { - Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time, - _owner, - touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_UP) ? - RawPointerEventType.TouchEnd : - touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_DOWN) ? - RawPointerEventType.TouchBegin : - RawPointerEventType.TouchUpdate, - PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)), - WindowsKeyboardDevice.Instance.Modifiers, - touchInput.Id)); - } - - CloseTouchInputHandle(lParam); - return IntPtr.Zero; - } - - break; - } - case WindowsMessage.WM_NCPAINT: - { - if (!HasFullDecorations) - { - return IntPtr.Zero; - } - - break; - } - - case WindowsMessage.WM_NCACTIVATE: - { - if (!HasFullDecorations) - { - return new IntPtr(1); - } - - break; - } - - case WindowsMessage.WM_PAINT: - { - using (_rendererLock.Lock()) - { - if (BeginPaint(_hwnd, out PAINTSTRUCT ps) != IntPtr.Zero) - { - var f = Scaling; - var r = ps.rcPaint; - Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f, - (r.bottom - r.top) / f)); - EndPaint(_hwnd, ref ps); - } - } - - return IntPtr.Zero; - } - - case WindowsMessage.WM_SIZE: - { - using (_rendererLock.Lock()) - { - // Do nothing here, just block until the pending frame render is completed on the render thread - } - - var size = (SizeCommand)wParam; - - if (Resized != null && - (size == SizeCommand.Restored || - size == SizeCommand.Maximized)) - { - var clientSize = new Size(ToInt32(lParam) & 0xffff, ToInt32(lParam) >> 16); - Resized(clientSize / Scaling); - } - - var windowState = size == SizeCommand.Maximized ? - WindowState.Maximized : - (size == SizeCommand.Minimized ? WindowState.Minimized : WindowState.Normal); - - if (windowState != _lastWindowState) - { - _lastWindowState = windowState; - WindowStateChanged?.Invoke(windowState); - } - - return IntPtr.Zero; - } - - case WindowsMessage.WM_MOVE: - { - PositionChanged?.Invoke(new PixelPoint((short)(ToInt32(lParam) & 0xffff), - (short)(ToInt32(lParam) >> 16))); - return IntPtr.Zero; - } - - case WindowsMessage.WM_GETMINMAXINFO: - { - MINMAXINFO mmi = Marshal.PtrToStructure(lParam); - - if (_minSize.Width > 0) - { - mmi.ptMinTrackSize.X = - (int)((_minSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right); - } - - if (_minSize.Height > 0) - { - mmi.ptMinTrackSize.Y = - (int)((_minSize.Height * Scaling) + BorderThickness.Top + BorderThickness.Bottom); - } - - if (!double.IsInfinity(_maxSize.Width) && _maxSize.Width > 0) - { - mmi.ptMaxTrackSize.X = - (int)((_maxSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right); - } - - if (!double.IsInfinity(_maxSize.Height) && _maxSize.Height > 0) - { - mmi.ptMaxTrackSize.Y = - (int)((_maxSize.Height * Scaling) + BorderThickness.Top + BorderThickness.Bottom); - } - - Marshal.StructureToPtr(mmi, lParam, true); - return IntPtr.Zero; - } - - case WindowsMessage.WM_DISPLAYCHANGE: - { - (Screen as ScreenImpl)?.InvalidateScreensCache(); - return IntPtr.Zero; - } + lRet = CustomCaptionProc(hWnd, msg, wParam, lParam, ref callDwp); } -#if USE_MANAGED_DRAG - if (_managedDrag.PreprocessInputEvent(ref e)) - return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); -#endif - - if (e != null && Input != null) + if (callDwp) { - Input(e); - - if (e.Handled) - { - return IntPtr.Zero; - } + lRet = AppWndProc(hWnd, msg, wParam, lParam); } - using (_rendererLock.Lock()) - { - return DefWindowProc(hWnd, msg, wParam, lParam); - } + return lRet; } + + public INativeControlHostImpl NativeControlHost => _nativeControlHost; - private static int ToInt32(IntPtr ptr) - { - if (IntPtr.Size == 4) - return ptr.ToInt32(); - - return (int)(ptr.ToInt64() & 0xffffffff); - } - - private static int HighWord(int param) => param >> 16; - - private Point DipFromLParam(IntPtr lParam) - { - return new Point((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)) / Scaling; - } - - private PixelPoint PointFromLParam(IntPtr lParam) - { - return new PixelPoint((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)); - } - - private bool ShouldIgnoreTouchEmulatedMessage() - { - if (!_multitouch) - { - return false; - } - - // MI_WP_SIGNATURE - // https://docs.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages - const long marker = 0xFF515700L; - - var info = GetMessageExtraInfo().ToInt64(); - return (info & marker) == marker; - } - - private static RawInputModifiers GetMouseModifiers(IntPtr wParam) - { - var keys = (ModifierKeys)ToInt32(wParam); - var modifiers = WindowsKeyboardDevice.Instance.Modifiers; - - if (keys.HasFlagCustom(ModifierKeys.MK_LBUTTON)) - { - modifiers |= RawInputModifiers.LeftMouseButton; - } - - if (keys.HasFlagCustom(ModifierKeys.MK_RBUTTON)) - { - modifiers |= RawInputModifiers.RightMouseButton; - } - - if (keys.HasFlagCustom(ModifierKeys.MK_MBUTTON)) - { - modifiers |= RawInputModifiers.MiddleMouseButton; - } - - if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON1)) - { - modifiers |= RawInputModifiers.XButton1MouseButton; - } - - if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON2)) - { - modifiers |= RawInputModifiers.XButton2MouseButton; - } - - return modifiers; - } + protected virtual bool ShouldTakeFocusOnClick => true; } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 0cf5a73b9f..6e9cb81b11 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Runtime.InteropServices; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Media; @@ -18,7 +19,8 @@ namespace Avalonia.Win32 /// /// Window implementation for Win32 platform. /// - public partial class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo + public partial class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo, + ITopLevelImplWithNativeControlHost { private static readonly List s_instances = new List(); @@ -40,6 +42,10 @@ namespace Avalonia.Win32 private SavedWindowInfo _savedWindowInfo; private bool _isFullScreenActive; + private bool _isClientAreaExtended; + private Thickness _extendedMargins; + private Thickness _offScreenMargin; + private double _extendTitleBarHint = -1; #if USE_MANAGED_DRAG private readonly ManagedWindowResizeDragHelper _managedDrag; @@ -52,6 +58,7 @@ namespace Avalonia.Win32 private readonly FramebufferManager _framebuffer; private readonly IGlPlatformSurface _gl; + private Win32NativeControlHost _nativeControlHost; private WndProc _wndProcDelegate; private string _className; private IntPtr _hwnd; @@ -66,7 +73,9 @@ namespace Avalonia.Win32 private OleDropTarget _dropTarget; private Size _minSize; private Size _maxSize; - private WindowImpl _parent; + private POINT _maxTrackSize; + private WindowImpl _parent; + private ExtendClientAreaChromeHints _extendChromeHints = ExtendClientAreaChromeHints.Default; public WindowImpl() { @@ -100,6 +109,7 @@ namespace Avalonia.Win32 Screen = new ScreenImpl(); + _nativeControlHost = new Win32NativeControlHost(this); s_instances.Add(this); } @@ -122,6 +132,8 @@ namespace Avalonia.Win32 public Action PositionChanged { get; set; } public Action WindowStateChanged { get; set; } + + public Action LostFocus { get; set; } public Action TransparencyLevelChanged { get; set; } @@ -168,16 +180,7 @@ namespace Avalonia.Win32 public IPlatformHandle Handle { get; private set; } - public Size MaxClientSize - { - get - { - return (new Size( - GetSystemMetrics(SystemMetric.SM_CXMAXTRACK), - GetSystemMetrics(SystemMetric.SM_CYMAXTRACK)) - - BorderThickness) / Scaling; - } - } + public virtual Size MaxAutoSizeHint => new Size(_maxTrackSize.X / Scaling, _maxTrackSize.Y / Scaling); public IMouseDevice MouseDevice => _mouseDevice; @@ -185,6 +188,11 @@ namespace Avalonia.Win32 { get { + if(_isFullScreenActive) + { + return WindowState.FullScreen; + } + var placement = default(WINDOWPLACEMENT); GetWindowPlacement(_hwnd, ref placement); @@ -211,6 +219,8 @@ namespace Avalonia.Win32 public WindowTransparencyLevel TransparencyLevel { get; private set; } + protected IntPtr Hwnd => _hwnd; + public void SetTransparencyLevelHint (WindowTransparencyLevel transparencyLevel) { TransparencyLevel = EnableBlur(transparencyLevel); @@ -267,7 +277,7 @@ namespace Avalonia.Win32 } accent.AccentFlags = 2; - accent.GradientColor = 0x00FFFFFF; + accent.GradientColor = 0x01000000; var accentPtr = Marshal.AllocHGlobal(accentStructSize); Marshal.StructureToPtr(accent, accentPtr, false); @@ -529,7 +539,7 @@ namespace Avalonia.Win32 0, atom, null, - (int)WindowStyles.WS_OVERLAPPEDWINDOW, + (int)WindowStyles.WS_OVERLAPPEDWINDOW | (int) WindowStyles.WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, @@ -577,7 +587,7 @@ namespace Avalonia.Win32 Handle = new PlatformHandle(_hwnd, PlatformConstants.WindowHandleType); - _multitouch = Win32Platform.Options.EnableMultitouch ?? false; + _multitouch = Win32Platform.Options.EnableMultitouch ?? true; if (_multitouch) { @@ -668,6 +678,98 @@ namespace Avalonia.Win32 } TaskBarList.MarkFullscreen(_hwnd, fullscreen); + + ExtendClientArea(); + } + + private MARGINS UpdateExtendMargins() + { + RECT borderThickness = new RECT(); + RECT borderCaptionThickness = new RECT(); + + AdjustWindowRectEx(ref borderCaptionThickness, (uint)(GetStyle()), false, 0); + AdjustWindowRectEx(ref borderThickness, (uint)(GetStyle() & ~WindowStyles.WS_CAPTION), false, 0); + borderThickness.left *= -1; + borderThickness.top *= -1; + borderCaptionThickness.left *= -1; + borderCaptionThickness.top *= -1; + + bool wantsTitleBar = _extendChromeHints.HasFlag(ExtendClientAreaChromeHints.SystemChrome) || _extendTitleBarHint == -1; + + if (!wantsTitleBar) + { + borderCaptionThickness.top = 1; + } + + MARGINS margins = new MARGINS(); + margins.cxLeftWidth = 1; + margins.cxRightWidth = 1; + margins.cyBottomHeight = 1; + + if (_extendTitleBarHint != -1) + { + borderCaptionThickness.top = (int)(_extendTitleBarHint * Scaling); + } + + margins.cyTopHeight = _extendChromeHints.HasFlag(ExtendClientAreaChromeHints.SystemChrome) && !_extendChromeHints.HasFlag(ExtendClientAreaChromeHints.PreferSystemChrome) ? borderCaptionThickness.top : 1; + + if (WindowState == WindowState.Maximized) + { + _extendedMargins = new Thickness(0, (borderCaptionThickness.top - borderThickness.top) / Scaling, 0, 0); + _offScreenMargin = new Thickness(borderThickness.left / Scaling, borderThickness.top / Scaling, borderThickness.right / Scaling, borderThickness.bottom / Scaling); + } + else + { + _extendedMargins = new Thickness(0, (borderCaptionThickness.top) / Scaling, 0, 0); + _offScreenMargin = new Thickness(); + } + + return margins; + } + + private void ExtendClientArea() + { + if (DwmIsCompositionEnabled(out bool compositionEnabled) < 0 || !compositionEnabled) + { + _isClientAreaExtended = false; + return; + } + + GetWindowRect(_hwnd, out var rcClient); + + // Inform the application of the frame change. + SetWindowPos(_hwnd, + IntPtr.Zero, + rcClient.left, rcClient.top, + rcClient.Width, rcClient.Height, + SetWindowPosFlags.SWP_FRAMECHANGED); + + if (_isClientAreaExtended && WindowState != WindowState.FullScreen) + { + var margins = UpdateExtendMargins(); + + DwmExtendFrameIntoClientArea(_hwnd, ref margins); + } + else + { + var margins = new MARGINS(); + DwmExtendFrameIntoClientArea(_hwnd, ref margins); + + _offScreenMargin = new Thickness(); + _extendedMargins = new Thickness(); + } + + if(!_isClientAreaExtended || (_extendChromeHints.HasFlag(ExtendClientAreaChromeHints.SystemChrome) && + !_extendChromeHints.HasFlag(ExtendClientAreaChromeHints.PreferSystemChrome))) + { + EnableCloseButton(_hwnd); + } + else + { + DisableCloseButton(_hwnd); + } + + ExtendClientAreaToDecorationsChanged?.Invoke(_isClientAreaExtended); } private void ShowWindow(WindowState state) @@ -818,9 +920,10 @@ namespace Avalonia.Win32 // Otherwise it will still show in the taskbar. } + WindowStyles style; if ((oldProperties.IsResizable != newProperties.IsResizable) || forceChanges) { - var style = GetStyle(); + style = GetStyle(); if (newProperties.IsResizable) { @@ -841,7 +944,7 @@ namespace Avalonia.Win32 if ((oldProperties.Decorations != newProperties.Decorations) || forceChanges) { - var style = GetStyle(); + style = GetStyle(); const WindowStyles fullDecorationFlags = WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU; @@ -886,7 +989,26 @@ namespace Avalonia.Win32 SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); } - } + } + } + + private const int MF_BYCOMMAND = 0x0; + private const int MF_BYPOSITION = 0x400; + private const int MF_REMOVE = 0x1000; + private const int MF_ENABLED = 0x0; + private const int MF_GRAYED = 0x1; + private const int MF_DISABLED = 0x2; + private const int SC_CLOSE = 0xF060; + + void DisableCloseButton(IntPtr hwnd) + { + EnableMenuItem(GetSystemMenu(hwnd, false), SC_CLOSE, + MF_BYCOMMAND | MF_DISABLED | MF_GRAYED); + } + void EnableCloseButton(IntPtr hwnd) + { + EnableMenuItem(GetSystemMenu(hwnd, false), SC_CLOSE, + MF_BYCOMMAND | MF_ENABLED); } #if USE_MANAGED_DRAG @@ -912,6 +1034,46 @@ namespace Avalonia.Win32 IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle; + public void SetExtendClientAreaToDecorationsHint(bool hint) + { + _isClientAreaExtended = hint; + + ExtendClientArea(); + } + + public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) + { + _extendChromeHints = hints; + + ExtendClientArea(); + } + + /// + public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) + { + _extendTitleBarHint = titleBarHeight; + + ExtendClientArea(); + } + + /// + public bool IsClientAreaExtendedToDecorations => _isClientAreaExtended; + + /// + public Action ExtendClientAreaToDecorationsChanged { get; set; } + + /// + public bool NeedsManagedDecorations => _isClientAreaExtended && _extendChromeHints.HasFlag(ExtendClientAreaChromeHints.PreferSystemChrome); + + /// + public Thickness ExtendedMargins => _extendedMargins; + + /// + public Thickness OffScreenMargin => _offScreenMargin; + + /// + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0); + private struct SavedWindowInfo { public WindowStyles Style { get; set; } diff --git a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs index db4c916052..ba1bfda949 100644 --- a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs +++ b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs @@ -5,12 +5,13 @@ using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; using Avalonia.Controls.Platform; +using Avalonia.Logging; namespace Avalonia.Win32 { internal class WindowsMountedVolumeInfoListener : IDisposable { - private readonly CompositeDisposable _disposables; + private readonly CompositeDisposable _disposables; private bool _beenDisposed = false; private ObservableCollection mountedDrives; @@ -32,10 +33,22 @@ namespace Avalonia.Win32 var allDrives = DriveInfo.GetDrives(); var mountVolInfos = allDrives - .Where(p => p.IsReady) + .Where(p => + { + try + { + var ret = p.IsReady; + return ret; + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(this, $"Error in Windows drive enumeration: {e.Message}"); + } + return false; + }) .Select(p => new MountedVolumeInfo() { - VolumeLabel = string.IsNullOrEmpty(p.VolumeLabel.Trim()) ? p.RootDirectory.FullName + VolumeLabel = string.IsNullOrEmpty(p.VolumeLabel.Trim()) ? p.RootDirectory.FullName : $"{p.VolumeLabel} ({p.Name})", VolumePath = p.RootDirectory.FullName, VolumeSizeBytes = (ulong)p.TotalSize diff --git a/src/iOS/Avalonia.iOS/Clipboard.cs b/src/iOS/Avalonia.iOS/Clipboard.cs index c66b379453..2deb49473f 100644 --- a/src/iOS/Avalonia.iOS/Clipboard.cs +++ b/src/iOS/Avalonia.iOS/Clipboard.cs @@ -22,5 +22,11 @@ namespace Avalonia.iOS UIPasteboard.General.String = ""; return Task.FromResult(0); } + + public Task SetDataObjectAsync(IDataObject data) => throw new PlatformNotSupportedException(); + + public Task GetFormatsAsync() => throw new PlatformNotSupportedException(); + + public Task GetDataAsync(string format) => throw new PlatformNotSupportedException(); } } diff --git a/src/iOS/Avalonia.iOS/EmbeddableImpl.cs b/src/iOS/Avalonia.iOS/EmbeddableImpl.cs index 838bf49846..d299ff99c1 100644 --- a/src/iOS/Avalonia.iOS/EmbeddableImpl.cs +++ b/src/iOS/Avalonia.iOS/EmbeddableImpl.cs @@ -4,7 +4,7 @@ using Avalonia.Platform; namespace Avalonia.iOS { - class EmbeddableImpl : TopLevelImpl, IEmbeddableWindowImpl + class EmbeddableImpl : TopLevelImpl { public void SetTitle(string title) { @@ -23,11 +23,5 @@ namespace Avalonia.iOS public void SetSystemDecorations(SystemDecorations enabled) { } - - public event Action LostFocus - { - add {} - remove {} - } } } diff --git a/src/iOS/Avalonia.iOS/TopLevelImpl.cs b/src/iOS/Avalonia.iOS/TopLevelImpl.cs index c5b6642982..83a68990d7 100644 --- a/src/iOS/Avalonia.iOS/TopLevelImpl.cs +++ b/src/iOS/Avalonia.iOS/TopLevelImpl.cs @@ -139,5 +139,7 @@ namespace Avalonia.iOS public ILockedFramebuffer Lock() => new EmulatedFramebuffer(this); public IPopupImpl CreatePopup() => null; + + public Action LostFocus { get; set; } } } diff --git a/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs b/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs index 9a40b7162e..aaca5f53d6 100644 --- a/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs +++ b/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs @@ -10,7 +10,7 @@ namespace Avalonia.iOS throw new NotSupportedException(); } - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public WindowImpl CreateEmbeddableWindow() { throw new NotSupportedException(); } diff --git a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs index b5c61883e7..784f40fe1f 100644 --- a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs +++ b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs @@ -2,6 +2,7 @@ using Avalonia.Controls; using Avalonia.Data; using Avalonia.Layout; +using Avalonia.Media; using Avalonia.Styling; using Avalonia.UnitTests; using Moq; @@ -265,6 +266,70 @@ namespace Avalonia.Animation.UnitTests } } + [Fact] + public void Replacing_Transitions_During_Animation_Does_Not_Throw_KeyNotFound() + { + // Issue #4059 + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + Border target; + var clock = new TestClock(); + var root = new TestRoot + { + Clock = clock, + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(Border.TransitionsProperty, + new Transitions + { + new DoubleTransition + { + Property = Border.OpacityProperty, + Duration = TimeSpan.FromSeconds(1), + }, + }), + }, + }, + new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter(Border.TransitionsProperty, + new Transitions + { + new DoubleTransition + { + Property = Border.OpacityProperty, + Duration = TimeSpan.FromSeconds(1), + }, + }), + new Setter(Border.OpacityProperty, 0.0), + }, + }, + }, + Child = target = new Border + { + Background = Brushes.Red, + } + }; + + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + target.Classes.Add("foo"); + clock.Step(TimeSpan.FromSeconds(0)); + clock.Step(TimeSpan.FromSeconds(0.5)); + + Assert.Equal(0.5, target.Opacity); + + target.Classes.Remove("foo"); + } + } + private static Mock CreateTarget() { return CreateTransition(Visual.OpacityProperty); diff --git a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs index df7c0693e1..fa2ed61e65 100644 --- a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs +++ b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Animation.Easings; using Avalonia.Controls.Shapes; using Avalonia.Media; using Avalonia.Styling; @@ -25,6 +26,16 @@ namespace Avalonia.Animation.UnitTests Assert.Equal(4, keySpline.ControlPointY2); } + [Theory] + [InlineData("1,2F,3,4")] + [InlineData("Foo,Bar,Fee,Buzz")] + public void Can_Handle_Invalid_String_KeySpline_Via_TypeConverter(string input) + { + var conv = new KeySplineTypeConverter(); + + Assert.ThrowsAny(() => (KeySpline)conv.ConvertFrom(input)); + } + [Theory] [InlineData(0.00)] [InlineData(0.50)] @@ -46,6 +57,22 @@ namespace Avalonia.Animation.UnitTests Assert.Throws(() => keySpline.ControlPointX2 = input); } + [Fact] + public void SplineEasing_Can_Be_Mutated() + { + var easing = new SplineEasing(); + + Assert.Equal(0, easing.Ease(0)); + Assert.Equal(1, easing.Ease(1)); + + easing.X1 = 0.25; + easing.Y1 = 0.5; + easing.X2 = 0.75; + easing.Y2 = 1.0; + + Assert.NotEqual(0.5, easing.Ease(0.5)); + } + /* To get the test values for the KeySpline test, you can: 1) Grab the WPF sample for KeySpline animations from https://github.com/microsoft/WPF-Samples/tree/master/Animation/KeySplineAnimations @@ -141,5 +168,73 @@ namespace Avalonia.Animation.UnitTests expected = 1.8016358493761722; Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); } + + [Fact] + public void Check_KeySpline_Parsing_Is_Correct() + { + var keyframe1 = new KeyFrame() + { + Setters = + { + new Setter(RotateTransform.AngleProperty, -2.5d), + }, + KeyTime = TimeSpan.FromSeconds(0) + }; + + var keyframe2 = new KeyFrame() + { + Setters = + { + new Setter(RotateTransform.AngleProperty, 2.5d), + }, + KeyTime = TimeSpan.FromSeconds(5), + }; + + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(5), + Children = + { + keyframe1, + keyframe2 + }, + IterationCount = new IterationCount(5), + PlaybackDirection = PlaybackDirection.Alternate, + Easing = Easing.Parse("0.1123555056179775,0.657303370786517,0.8370786516853934,0.499999999999999999") + }; + + var rotateTransform = new RotateTransform(-2.5); + var rect = new Rectangle() + { + RenderTransform = rotateTransform + }; + + var clock = new TestClock(); + var animationRun = animation.RunAsync(rect, clock); + + // position is what you'd expect at end and beginning + clock.Step(TimeSpan.Zero); + Assert.Equal(rotateTransform.Angle, -2.5); + clock.Step(TimeSpan.FromSeconds(5)); + Assert.Equal(rotateTransform.Angle, 2.5); + + // test some points in between end and beginning + var tolerance = 0.01; + clock.Step(TimeSpan.Parse("00:00:10.0153932")); + var expected = -2.4122350198982545; + Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); + + clock.Step(TimeSpan.Parse("00:00:11.2655407")); + expected = -0.37153223002125113; + Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); + + clock.Step(TimeSpan.Parse("00:00:12.6158773")); + expected = 0.3967885416786294; + Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); + + clock.Step(TimeSpan.Parse("00:00:14.6495256")); + expected = 1.8016358493761722; + Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); + } } } diff --git a/tests/Avalonia.Animation.UnitTests/TestClock.cs b/tests/Avalonia.Animation.UnitTests/TestClock.cs index a1c4ff9277..4812880c03 100644 --- a/tests/Avalonia.Animation.UnitTests/TestClock.cs +++ b/tests/Avalonia.Animation.UnitTests/TestClock.cs @@ -5,10 +5,12 @@ namespace Avalonia.Animation.UnitTests { internal class TestClock : IClock, IDisposable { + private TimeSpan _curTime; + private IObserver _observer; public PlayState PlayState { get; set; } = PlayState.Run; - + public void Dispose() { _observer?.OnCompleted(); @@ -19,6 +21,12 @@ namespace Avalonia.Animation.UnitTests _observer?.OnNext(time); } + public void Pulse(TimeSpan time) + { + _curTime += time; + _observer?.OnNext(_curTime); + } + public IDisposable Subscribe(IObserver observer) { _observer = observer; diff --git a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs index 70ffd781a1..640013dedd 100644 --- a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs +++ b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs @@ -10,13 +10,11 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Check_Transitions_Interpolation_Negative_Bounds_Clamp() { - var clock = new MockGlobalClock(); + var clock = new TestClock(); - using (UnitTestApplication.Start(new TestServices(globalClock: clock))) + var border = new Border { - var border = new Border - { - Transitions = new Transitions + Transitions = new Transitions { new DoubleTransition { @@ -24,27 +22,25 @@ namespace Avalonia.Animation.UnitTests Property = Border.OpacityProperty, } } - }; + }; - border.Opacity = 0; + border.Opacity = 0; - clock.Pulse(TimeSpan.FromSeconds(0)); - clock.Pulse(TimeSpan.FromSeconds(-0.5)); + clock.Pulse(TimeSpan.FromSeconds(0)); + clock.Pulse(TimeSpan.FromSeconds(-0.5)); + + Assert.Equal(0, border.Opacity); - Assert.Equal(0, border.Opacity); - } } [Fact] public void Check_Transitions_Interpolation_Positive_Bounds_Clamp() { - var clock = new MockGlobalClock(); + var clock = new TestClock(); - using (UnitTestApplication.Start(new TestServices(globalClock: clock))) + var border = new Border { - var border = new Border - { - Transitions = new Transitions + Transitions = new Transitions { new DoubleTransition { @@ -52,34 +48,62 @@ namespace Avalonia.Animation.UnitTests Property = Border.OpacityProperty, } } - }; + }; - border.Opacity = 0; + border.Opacity = 0; - clock.Pulse(TimeSpan.FromSeconds(0)); - clock.Pulse(TimeSpan.FromMilliseconds(1001)); + clock.Pulse(TimeSpan.FromSeconds(0)); + clock.Pulse(TimeSpan.FromMilliseconds(1001)); + + Assert.Equal(0, border.Opacity); - Assert.Equal(0, border.Opacity); - } } [Fact] public void TransitionInstance_With_Zero_Duration_Is_Completed_On_First_Tick() { - var clock = new MockGlobalClock(); + var clock = new TestClock(); - using (UnitTestApplication.Start(new TestServices(globalClock: clock))) + int i = 0; + var inst = new TransitionInstance(clock, TimeSpan.Zero, TimeSpan.Zero).Subscribe(nextValue => { - int i = 0; - var inst = new TransitionInstance(clock, TimeSpan.Zero).Subscribe(nextValue => + switch (i++) { - switch (i++) - { - case 0: Assert.Equal(0, nextValue); break; - case 1: Assert.Equal(1d, nextValue); break; - } - }); + case 0: Assert.Equal(0, nextValue); break; + case 1: Assert.Equal(1d, nextValue); break; + } + }); + + clock.Pulse(TimeSpan.FromMilliseconds(10)); + } + + [Fact] + public void TransitionInstance_Properly_Calculates_Delay_And_Duration_Values() + { + var clock = new TestClock(); + + int i = -1; + var inst = new TransitionInstance(clock, TimeSpan.FromMilliseconds(30), TimeSpan.FromMilliseconds(70)).Subscribe(nextValue => + { + switch (i++) + { + case 0: Assert.Equal(0, nextValue); break; + case 1: Assert.Equal(0, nextValue); break; + case 2: Assert.Equal(0, nextValue); break; + case 3: Assert.Equal(0, nextValue); break; + case 4: Assert.Equal(Math.Round(10d / 70d, 4), Math.Round(nextValue, 4)); break; + case 5: Assert.Equal(Math.Round(20d / 70d, 4), Math.Round(nextValue, 4)); break; + case 6: Assert.Equal(Math.Round(30d / 70d, 4), Math.Round(nextValue, 4)); break; + case 7: Assert.Equal(Math.Round(40d / 70d, 4), Math.Round(nextValue, 4)); break; + case 8: Assert.Equal(Math.Round(50d / 70d, 4), Math.Round(nextValue, 4)); break; + case 9: Assert.Equal(Math.Round(60d / 70d, 4), Math.Round(nextValue, 4)); break; + case 10: Assert.Equal(1d, nextValue); break; + } + }); + + for (int z = 0; z <= 10; z++) + { clock.Pulse(TimeSpan.FromMilliseconds(10)); } } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs index 6e175707e4..784046ac0b 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs @@ -322,7 +322,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { "bar", new BindingNotification( - new MissingMemberException("Could not find CLR property 'Bar' on 'Avalonia.Base.UnitTests.Data.Core.ExpressionObserverTests_Property+WithoutBar'"), + new MissingMemberException("Could not find a matching property accessor for 'Bar' on 'Avalonia.Base.UnitTests.Data.Core.ExpressionObserverTests_Property+WithoutBar'"), BindingErrorType.Error), "baz", }, @@ -578,6 +578,28 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.True(true); } + [Fact] + public void Should_Not_Throw_Exception_When_Enabling_Data_Validation_On_Missing_Member() + { + var source = new Class1(); + var target = new PropertyAccessorNode("NotFound", true); + + target.Target = new WeakReference(source); + + var result = new List(); + + target.Subscribe(x => result.Add(x)); + + Assert.Equal( + new object[] + { + new BindingNotification( + new MissingMemberException("Could not find a matching property accessor for 'NotFound' on 'Avalonia.Base.UnitTests.Data.Core.ExpressionObserverTests_Property+Class1'"), + BindingErrorType.Error), + }, + result); + } + private interface INext { int PropertyChangedSubscriptionCount { get; } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/PropertyPathGrammarTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/PropertyPathGrammarTests.cs new file mode 100644 index 0000000000..99144b7381 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Data/Core/PropertyPathGrammarTests.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Markup.Parsers; +using Xunit; + +namespace Avalonia.Base.UnitTests.Data.Core +{ + public class PropertyPathGrammarTests + { + static void Check(string s, params PropertyPathGrammar.ISyntax[] expected) + { + var parsed = PropertyPathGrammar.Parse(s).ToList(); + Assert.Equal(expected.Length, parsed.Count); + for (var c = 0; c < parsed.Count; c++) + Assert.Equal(expected[c], parsed[c]); + } + + [Fact] + public void PropertyPath_Should_Support_Simple_Properties() + { + Check("SomeProperty", new PropertyPathGrammar.PropertySyntax {Name = "SomeProperty"}); + } + + [Fact] + public void PropertyPath_Should_Ignore_Trailing_Whitespace() + { + Check(" SomeProperty ", new PropertyPathGrammar.PropertySyntax {Name = "SomeProperty"}); + } + + [Fact] + public void PropertyPath_Should_Support_Qualified_Properties() + { + Check(" ( somens:SomeType.SomeProperty ) ", + new PropertyPathGrammar.TypeQualifiedPropertySyntax() + { + Name = "SomeProperty", TypeName = "SomeType", TypeNamespace = "somens" + }); + } + + [Fact] + public void PropertyPath_Should_Support_Property_Paths() + { + Check(" ( somens:SomeType.SomeProperty ).Child . SubChild ", + new PropertyPathGrammar.TypeQualifiedPropertySyntax() + { + Name = "SomeProperty", TypeName = "SomeType", TypeNamespace = "somens" + }, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "Child"}, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "SubChild"} + ); + } + + [Fact] + public void PropertyPath_Should_Support_Casts() + { + Check(" ( somens:SomeType.SomeProperty ) :> SomeType.Child as somens:SomeType . SubChild ", + new PropertyPathGrammar.TypeQualifiedPropertySyntax() + { + Name = "SomeProperty", TypeName = "SomeType", TypeNamespace = "somens" + }, + new PropertyPathGrammar.CastTypeSyntax + { + TypeName = "SomeType" + }, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "Child"}, + new PropertyPathGrammar.CastTypeSyntax + { + TypeName = "SomeType", + TypeNamespace = "somens" + }, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "SubChild"} + ); + } + + [Fact] + public void PropertyPath_Should_Support_Ensure_Type() + { + Check(" ( somens:SomeType.SomeProperty ) := SomeType.Child := somens:SomeType . SubChild ", + new PropertyPathGrammar.TypeQualifiedPropertySyntax() + { + Name = "SomeProperty", TypeName = "SomeType", TypeNamespace = "somens" + }, + new PropertyPathGrammar.EnsureTypeSyntax + { + TypeName = "SomeType" + }, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "Child"}, + new PropertyPathGrammar.EnsureTypeSyntax + { + TypeName = "SomeType", + TypeNamespace = "somens" + }, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "SubChild"} + ); + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs new file mode 100644 index 0000000000..de3fadd157 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs @@ -0,0 +1,203 @@ +using System; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Base.UnitTests.Utilities +{ + public class MathUtilitiesTests + { + private const double AnyValue = 42.42; + private readonly double _calculatedAnyValue; + private readonly double _one; + private readonly double _zero; + + public MathUtilitiesTests() + { + _calculatedAnyValue = 0.0; + _one = 0.0; + _zero = 1.0; + + const int N = 10; + var dxAny = AnyValue / N; + var dxOne = 1.0 / N; + var dxZero = _zero / N; + + for (var i = 0; i < N; ++i) + { + _calculatedAnyValue += dxAny; + _one += dxOne; + _zero -= dxZero; + } + } + + [Fact] + public void Two_Equivalent_Double_Values_Are_Close() + { + var actual = MathUtilities.AreClose(AnyValue, _calculatedAnyValue); + + Assert.True(actual); + Assert.Equal(AnyValue, Math.Round(_calculatedAnyValue, 14)); + } + + [Fact] + public void Two_Equivalent_Single_Values_Are_Close() + { + var expectedValue = (float)AnyValue; + var actualValue = (float)_calculatedAnyValue; + + var actual = MathUtilities.AreClose(expectedValue, actualValue); + + Assert.True(actual); + Assert.Equal((float) Math.Round(expectedValue, 5), (float) Math.Round(actualValue, 4)); + } + + [Fact] + public void Calculated_Double_One_Is_One() + { + var actual = MathUtilities.IsOne(_one); + + Assert.True(actual); + Assert.Equal(1.0, Math.Round(_one, 15)); + } + + [Fact] + public void Calculated_Single_One_Is_One() + { + var actualValue = (float)_one; + + var actual = MathUtilities.IsOne(actualValue); + + Assert.True(actual); + Assert.Equal(1.0f, (float) Math.Round(actualValue, 7)); + } + + [Fact] + public void Calculated_Double_Zero_Is_Zero() + { + var actual = MathUtilities.IsZero(_zero); + + Assert.True(actual); + Assert.Equal(0.0, Math.Round(_zero, 15)); + } + + [Fact] + public void Calculated_Single_Zero_Is_Zero() + { + var actualValue = (float)_zero; + + var actual = MathUtilities.IsZero(actualValue); + + Assert.True(actual); + Assert.Equal(0.0f, (float) Math.Round(actualValue, 7)); + } + + [Fact] + public void Float_Clamp_Input_NaN_Return_NaN() + { + var clamp = MathUtilities.Clamp(double.NaN, 0.0, 1.0); + Assert.True(double.IsNaN(clamp)); + } + + [Fact] + public void Float_Clamp_Input_NegativeInfinity_Return_Min() + { + const double min = 0.0; + const double max = 1.0; + var actual = MathUtilities.Clamp(double.NegativeInfinity, min, max); + Assert.Equal(min, actual); + } + + [Fact] + public void Float_Clamp_Input_PositiveInfinity_Return_Max() + { + const double min = 0.0; + const double max = 1.0; + var actual = MathUtilities.Clamp(double.PositiveInfinity, min, max); + Assert.Equal(max, actual); + } + + [Fact] + public void Double_Float_Zero_Less_Than_One() + { + var actual = MathUtilities.LessThan(0d, 1d); + Assert.True(actual); + } + + [Fact] + public void Single_Float_Zero_Less_Than_One() + { + var actual = MathUtilities.LessThan(0f, 1f); + Assert.True(actual); + } + + [Fact] + public void Double_Float_One_Not_Less_Than_Zero() + { + var actual = MathUtilities.LessThan(1d, 0d); + Assert.False(actual); + } + + [Fact] + public void Single_Float_One_Not_Less_Than_Zero() + { + var actual = MathUtilities.LessThan(1f, 0f); + Assert.False(actual); + } + + [Fact] + public void Double_Float_Zero_Not_Greater_Than_One() + { + var actual = MathUtilities.GreaterThan(0d, 1d); + Assert.False(actual); + } + + [Fact] + public void Single_Float_Zero_Not_Greater_Than_One() + { + var actual = MathUtilities.GreaterThan(0f, 1f); + Assert.False(actual); + } + + [Fact] + public void Double_Float_One_Greater_Than_Zero() + { + var actual = MathUtilities.GreaterThan(1d, 0d); + Assert.True(actual); + } + + [Fact] + public void Single_Float_One_Greater_Than_Zero() + { + var actual = MathUtilities.GreaterThan(1f, 0f); + Assert.True(actual); + } + + [Fact] + public void Double_Float_One_Less_Than_Or_Close_One() + { + var actual = MathUtilities.LessThanOrClose(1d, 1d); + Assert.True(actual); + } + + [Fact] + public void Single_Float_One_Less_Than_Or_Close_One() + { + var actual = MathUtilities.LessThanOrClose(1f, 1f); + Assert.True(actual); + } + + [Fact] + public void Double_Float_One_Greater_Than_Or_Close_One() + { + var actual = MathUtilities.GreaterThanOrClose(1d, 1d); + Assert.True(actual); + } + + [Fact] + public void Single_Float_One_Greater_Than_Or_Close_One() + { + var actual = MathUtilities.GreaterThanOrClose(1f, 1f); + Assert.True(actual); + } + } +} diff --git a/tests/Avalonia.Benchmarks/Data/AccessorTestObject.cs b/tests/Avalonia.Benchmarks/Data/AccessorTestObject.cs new file mode 100644 index 0000000000..0039f5670c --- /dev/null +++ b/tests/Avalonia.Benchmarks/Data/AccessorTestObject.cs @@ -0,0 +1,47 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; + +namespace Avalonia.Benchmarks.Data +{ + internal class AccessorTestObject : INotifyPropertyChanged + { + private string _test; + + public string Test + { + get => _test; + set + { + if (_test == value) + { + return; + } + + _test = value; + + OnPropertyChanged(); + } + } + + public event PropertyChangedEventHandler PropertyChanged; + + public void Execute() + { + } + + public void Execute(object p0) + { + } + + public void Execute(object p0, object p1) + { + } + + [NotifyPropertyChangedInvocator] + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/tests/Avalonia.Benchmarks/Data/PropertyAccessorBenchmarks.cs b/tests/Avalonia.Benchmarks/Data/PropertyAccessorBenchmarks.cs new file mode 100644 index 0000000000..6c5e0d3b53 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Data/PropertyAccessorBenchmarks.cs @@ -0,0 +1,44 @@ +using System; +using Avalonia.Data.Core.Plugins; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Data +{ + [MemoryDiagnoser, InProcess] + public class PropertyAccessorBenchmarks + { + private readonly InpcPropertyAccessorPlugin _inpcPlugin = new InpcPropertyAccessorPlugin(); + private readonly MethodAccessorPlugin _methodPlugin = new MethodAccessorPlugin(); + private readonly AccessorTestObject _targetStrongRef = new AccessorTestObject(); + private readonly WeakReference _targetWeakRef; + + public PropertyAccessorBenchmarks() + { + _targetWeakRef = new WeakReference(_targetStrongRef); + } + + [Benchmark] + public void InpcAccessorMatch() + { + _inpcPlugin.Match(_targetWeakRef, nameof(AccessorTestObject.Test)); + } + + [Benchmark] + public void InpcAccessorStart() + { + _inpcPlugin.Start(_targetWeakRef, nameof(AccessorTestObject.Test)); + } + + [Benchmark] + public void MethodAccessorMatch() + { + _methodPlugin.Match(_targetWeakRef, nameof(AccessorTestObject.Execute)); + } + + [Benchmark] + public void MethodAccessorStart() + { + _methodPlugin.Start(_targetWeakRef, nameof(AccessorTestObject.Execute)); + } + } +} diff --git a/tests/Avalonia.Benchmarks/Data/PropertyAccessorPluginBenchmarks.cs b/tests/Avalonia.Benchmarks/Data/PropertyAccessorPluginBenchmarks.cs new file mode 100644 index 0000000000..75e3269e3d --- /dev/null +++ b/tests/Avalonia.Benchmarks/Data/PropertyAccessorPluginBenchmarks.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using Avalonia.Data.Core.Plugins; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Data +{ + [MemoryDiagnoser, InProcess] + public class PropertyAccessorPluginBenchmarks + { + private readonly AccessorTestObject _targetStrongRef = new AccessorTestObject(); + + private readonly List _oldPlugins; + private readonly List _newPlugins; + + public PropertyAccessorPluginBenchmarks() + { + _oldPlugins = new List + { + new AvaloniaPropertyAccessorPlugin(), + new MethodAccessorPlugin(), + new InpcPropertyAccessorPlugin() + }; + + _newPlugins = new List + { + new AvaloniaPropertyAccessorPlugin(), + new InpcPropertyAccessorPlugin(), + new MethodAccessorPlugin() + }; + } + + [Benchmark] + public void MatchAccessorOld() + { + var propertyName = nameof(AccessorTestObject.Test); + + foreach (IPropertyAccessorPlugin x in _oldPlugins) + { + if (x.Match(_targetStrongRef, propertyName)) + { + break; + } + } + } + + [Benchmark] + public void MatchAccessorNew() + { + var propertyName = nameof(AccessorTestObject.Test); + + foreach (IPropertyAccessorPlugin x in _newPlugins) + { + if (x.Match(_targetStrongRef, propertyName)) + { + break; + } + } + } + } +} diff --git a/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs b/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs index de40c247e6..7170f6d7d4 100644 --- a/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs @@ -24,7 +24,7 @@ namespace Avalonia.Benchmarks.Layout Renderer = new NullRenderer() }; - _root.LayoutManager.ExecuteInitialLayoutPass(_root); + _root.LayoutManager.ExecuteInitialLayoutPass(); } [Benchmark] diff --git a/tests/Avalonia.Benchmarks/Layout/Measure.cs b/tests/Avalonia.Benchmarks/Layout/Measure.cs index d03d17b4d3..fce2cddec9 100644 --- a/tests/Avalonia.Benchmarks/Layout/Measure.cs +++ b/tests/Avalonia.Benchmarks/Layout/Measure.cs @@ -25,7 +25,7 @@ namespace Avalonia.Benchmarks.Layout _controls.Add(panel); _controls = ControlHierarchyCreator.CreateChildren(_controls, panel, 3, 5, 5); - _root.LayoutManager.ExecuteInitialLayoutPass(_root); + _root.LayoutManager.ExecuteInitialLayoutPass(); } [Benchmark, MethodImpl(MethodImplOptions.NoInlining)] diff --git a/tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs b/tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs index fc2380d670..c6da3a941f 100644 --- a/tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs +++ b/tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs @@ -26,7 +26,7 @@ namespace Avalonia.Benchmarks.Traversal _shuffledControls = _controls.OrderBy(r => random.Next()).ToList(); - _root.LayoutManager.ExecuteInitialLayoutPass(_root); + _root.LayoutManager.ExecuteInitialLayoutPass(); } [Benchmark] diff --git a/tests/Avalonia.Benchmarks/Visuals/MatrixBenchmarks.cs b/tests/Avalonia.Benchmarks/Visuals/MatrixBenchmarks.cs new file mode 100644 index 0000000000..17e2237eb0 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Visuals/MatrixBenchmarks.cs @@ -0,0 +1,16 @@ +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Visuals +{ + [MemoryDiagnoser, InProcess] + public class MatrixBenchmarks + { + private static readonly Matrix s_data = Matrix.Identity; + + [Benchmark(Baseline = true)] + public bool Decompose() + { + return Matrix.TryDecomposeTransform(s_data, out Matrix.Decomposed decomposed); + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs new file mode 100644 index 0000000000..f41a3e7581 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Markup.Data; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Moq; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class CalendarDatePickerTests + { + private static bool CompareDates(DateTime first, DateTime second) + { + return first.Year == second.Year && + first.Month == second.Month && + first.Day == second.Day; + } + + [Fact(Skip = "FIX ME ASAP")] + public void SelectedDateChanged_Should_Fire_When_SelectedDate_Set() + { + using (UnitTestApplication.Start(Services)) + { + bool handled = false; + CalendarDatePicker datePicker = CreateControl(); + datePicker.SelectedDateChanged += (s,e) => + { + handled = true; + }; + DateTime value = new DateTime(2000, 10, 10); + datePicker.SelectedDate = value; + Threading.Dispatcher.UIThread.RunJobs(); + Assert.True(handled); + } + } + + [Fact] + public void Setting_Selected_Date_To_Blackout_Date_Should_Throw() + { + using (UnitTestApplication.Start(Services)) + { + CalendarDatePicker datePicker = CreateControl(); + datePicker.BlackoutDates.AddDatesInPast(); + + DateTime goodValue = DateTime.Today.AddDays(1); + datePicker.SelectedDate = goodValue; + Assert.True(CompareDates(datePicker.SelectedDate.Value, goodValue)); + + DateTime badValue = DateTime.Today.AddDays(-1); + Assert.ThrowsAny( + () => datePicker.SelectedDate = badValue); + } + } + + [Fact] + public void Adding_Blackout_Dates_Containing_Selected_Date_Should_Throw() + { + using (UnitTestApplication.Start(Services)) + { + CalendarDatePicker datePicker = CreateControl(); + datePicker.SelectedDate = DateTime.Today.AddDays(5); + + Assert.ThrowsAny( + () => datePicker.BlackoutDates.Add(new CalendarDateRange(DateTime.Today, DateTime.Today.AddDays(10)))); + } + } + + private static TestServices Services => TestServices.MockThreadingInterface.With( + standardCursorFactory: Mock.Of()); + + private CalendarDatePicker CreateControl() + { + var datePicker = + new CalendarDatePicker + { + Template = CreateTemplate() + }; + + datePicker.ApplyTemplate(); + return datePicker; + } + + private IControlTemplate CreateTemplate() + { + return new FuncControlTemplate((control, scope) => + { + var textBox = + new TextBox + { + Name = "PART_TextBox" + }.RegisterInNameScope(scope); + var button = + new Button + { + Name = "PART_Button" + }.RegisterInNameScope(scope); + var calendar = + new Calendar + { + Name = "PART_Calendar" + }.RegisterInNameScope(scope); + var popup = + new Popup + { + Name = "PART_Popup" + }.RegisterInNameScope(scope); + + var panel = new Panel(); + panel.Children.Add(textBox); + panel.Children.Add(button); + panel.Children.Add(popup); + panel.Children.Add(calendar); + + return panel; + }); + + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs index 3d396a9726..7bcb120850 100644 --- a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs @@ -1,15 +1,10 @@ using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; using System.Linq; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Presenters; +using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; -using Avalonia.Data; -using Avalonia.Markup.Data; using Avalonia.Platform; using Avalonia.UnitTests; +using Avalonia.VisualTree; using Moq; using Xunit; @@ -17,25 +12,18 @@ namespace Avalonia.Controls.UnitTests { public class DatePickerTests { - private static bool CompareDates(DateTime first, DateTime second) - { - return first.Year == second.Year && - first.Month == second.Month && - first.Day == second.Day; - } - - [Fact(Skip = "FIX ME ASAP")] + [Fact] public void SelectedDateChanged_Should_Fire_When_SelectedDate_Set() { using (UnitTestApplication.Start(Services)) { bool handled = false; - DatePicker datePicker = CreateControl(); - datePicker.SelectedDateChanged += (s,e) => + DatePicker datePicker = new DatePicker(); + datePicker.SelectedDateChanged += (s, e) => { handled = true; }; - DateTime value = new DateTime(2000, 10, 10); + DateTimeOffset value = new DateTimeOffset(2000, 10, 10, 0, 0, 0, TimeSpan.Zero); datePicker.SelectedDate = value; Threading.Dispatcher.UIThread.RunJobs(); Assert.True(handled); @@ -43,85 +31,225 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Setting_Selected_Date_To_Blackout_Date_Should_Throw() + public void DayVisible_False_Should_Hide_Day() { using (UnitTestApplication.Start(Services)) { - DatePicker datePicker = CreateControl(); - datePicker.BlackoutDates.AddDatesInPast(); + DatePicker datePicker = new DatePicker + { + Template = CreateTemplate(), + DayVisible = false + }; + datePicker.ApplyTemplate(); + Threading.Dispatcher.UIThread.RunJobs(); + + var desc = datePicker.GetVisualDescendants(); + Assert.True(desc.Count() > 1);//Should be layoutroot grid & button + TextBlock dayText = null; + Grid container = null; + + Assert.True(desc.ElementAt(1) is Button); - DateTime goodValue = DateTime.Today.AddDays(1); - datePicker.SelectedDate = goodValue; - Assert.True(CompareDates(datePicker.SelectedDate.Value, goodValue)); + container = (desc.ElementAt(1) as Button).Content as Grid; + Assert.True(container != null); - DateTime badValue = DateTime.Today.AddDays(-1); - Assert.ThrowsAny( - () => datePicker.SelectedDate = badValue); + for(int i = 0; i < container.Children.Count; i++) + { + if(container.Children[i] is TextBlock tb && tb.Name == "DayText") + { + dayText = tb; + break; + } + } + + Assert.True(dayText != null); + Assert.True(!dayText.IsVisible); + Assert.True(container.ColumnDefinitions.Count == 3); } } [Fact] - public void Adding_Blackout_Dates_Containing_Selected_Date_Should_Throw() + public void MonthVisible_False_Should_Hide_Month() { using (UnitTestApplication.Start(Services)) { - DatePicker datePicker = CreateControl(); - datePicker.SelectedDate = DateTime.Today.AddDays(5); + DatePicker datePicker = new DatePicker + { + Template = CreateTemplate(), + MonthVisible = false + }; + datePicker.ApplyTemplate(); + Threading.Dispatcher.UIThread.RunJobs(); + + var desc = datePicker.GetVisualDescendants(); + Assert.True(desc.Count() > 1);//Should be layoutroot grid & button + TextBlock monthText = null; + Grid container = null; + + Assert.True(desc.ElementAt(1) is Button); - Assert.ThrowsAny( - () => datePicker.BlackoutDates.Add(new CalendarDateRange(DateTime.Today, DateTime.Today.AddDays(10)))); + container = (desc.ElementAt(1) as Button).Content as Grid; + Assert.True(container != null); + + for (int i = 0; i < container.Children.Count; i++) + { + if (container.Children[i] is TextBlock tb && tb.Name == "MonthText") + { + monthText = tb; + break; + } + } + + Assert.True(monthText != null); + Assert.True(!monthText.IsVisible); + Assert.True(container.ColumnDefinitions.Count == 3); } } - private static TestServices Services => TestServices.MockThreadingInterface.With( - standardCursorFactory: Mock.Of()); - - private DatePicker CreateControl() + [Fact] + public void YearVisible_False_Should_Hide_Year() { - var datePicker = - new DatePicker + using (UnitTestApplication.Start(Services)) + { + DatePicker datePicker = new DatePicker { - Template = CreateTemplate() + Template = CreateTemplate(), + YearVisible = false }; + datePicker.ApplyTemplate(); + Threading.Dispatcher.UIThread.RunJobs(); + + var desc = datePicker.GetVisualDescendants(); + Assert.True(desc.Count() > 1);//Should be layoutroot grid & button + TextBlock yearText = null; + Grid container = null; + + Assert.True(desc.ElementAt(1) is Button); + + container = (desc.ElementAt(1) as Button).Content as Grid; + Assert.True(container != null); + + for (int i = 0; i < container.Children.Count; i++) + { + if (container.Children[i] is TextBlock tb && tb.Name == "YearText") + { + yearText = tb; + break; + } + } - datePicker.ApplyTemplate(); - return datePicker; + Assert.True(yearText != null); + Assert.True(!yearText.IsVisible); + Assert.True(container.ColumnDefinitions.Count == 3); + } } - private IControlTemplate CreateTemplate() + [Fact] + public void SelectedDate_null_Should_Use_Placeholders() { - return new FuncControlTemplate((control, scope) => + using (UnitTestApplication.Start(Services)) { - var textBox = - new TextBox - { - Name = "PART_TextBox" - }.RegisterInNameScope(scope); - var button = - new Button + DatePicker datePicker = new DatePicker + { + Template = CreateTemplate(), + YearVisible = false + }; + datePicker.ApplyTemplate(); + Threading.Dispatcher.UIThread.RunJobs(); + + var desc = datePicker.GetVisualDescendants(); + Assert.True(desc.Count() > 1);//Should be layoutroot grid & button + TextBlock yearText = null; + TextBlock monthText = null; + TextBlock dayText = null; + Grid container = null; + + Assert.True(desc.ElementAt(1) is Button); + + container = (desc.ElementAt(1) as Button).Content as Grid; + Assert.True(container != null); + + for (int i = 0; i < container.Children.Count; i++) + { + if (container.Children[i] is TextBlock tb && tb.Name == "YearText") { - Name = "PART_Button" - }.RegisterInNameScope(scope); - var calendar = - new Calendar + yearText = tb; + } + else if (container.Children[i] is TextBlock tb1 && tb1.Name == "MonthText") { - Name = "PART_Calendar" - }.RegisterInNameScope(scope); - var popup = - new Popup + monthText = tb1; + } + else if (container.Children[i] is TextBlock tb2 && tb2.Name == "DayText") { - Name = "PART_Popup" - }.RegisterInNameScope(scope); + dayText = tb2; + } + } - var panel = new Panel(); - panel.Children.Add(textBox); - panel.Children.Add(button); - panel.Children.Add(popup); - panel.Children.Add(calendar); + DateTimeOffset value = new DateTimeOffset(2000, 10, 10, 0, 0, 0, TimeSpan.Zero); + datePicker.SelectedDate = value; - return panel; - }); + Assert.False(dayText.Text == "day"); + Assert.False(monthText.Text == "month"); + Assert.False(yearText.Text == "year"); + Assert.False(datePicker.Classes.Contains(":hasnodate")); + datePicker.SelectedDate = null; + + Assert.True(dayText.Text == "day"); + Assert.True(monthText.Text == "month"); + Assert.True(yearText.Text == "year"); + Assert.True(datePicker.Classes.Contains(":hasnodate")); + } + } + + private static TestServices Services => TestServices.MockThreadingInterface.With( + fontManagerImpl: new MockFontManagerImpl(), + standardCursorFactory: Mock.Of(), + textShaperImpl: new MockTextShaperImpl()); + + private IControlTemplate CreateTemplate() + { + return new FuncControlTemplate((control, scope) => + { + var layoutRoot = new Grid + { + Name = "LayoutRoot" + }.RegisterInNameScope(scope); + //Skip contentpresenter + var flyoutButton = new Button + { + Name = "FlyoutButton" + }.RegisterInNameScope(scope); + var contentGrid = new Grid + { + Name = "ButtonContentGrid" + }.RegisterInNameScope(scope); + var dayText = new TextBlock + { + Name = "DayText" + }.RegisterInNameScope(scope); + var monthText = new TextBlock + { + Name = "MonthText" + }.RegisterInNameScope(scope); + var yearText = new TextBlock + { + Name = "YearText" + }.RegisterInNameScope(scope); + var firstSpacer = new Rectangle + { + Name = "FirstSpacer" + }.RegisterInNameScope(scope); + var secondSpacer = new Rectangle + { + Name = "SecondSpacer" + }.RegisterInNameScope(scope); + + contentGrid.Children.AddRange(new IControl[] { dayText, monthText, yearText, firstSpacer, secondSpacer }); + flyoutButton.Content = contentGrid; + layoutRoot.Children.Add(flyoutButton); + return layoutRoot; + }); } } } diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index 353bb9c98d..4ffa526b85 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Avalonia.UnitTests; using Xunit; using Xunit.Abstractions; @@ -1182,13 +1183,18 @@ namespace Avalonia.Controls.UnitTests foreach (var xgrids in grids) scope.Children.Add(xgrids); - var root = new Grid(); - root.UseLayoutRounding = false; - root.SetValue(Grid.IsSharedSizeScopeProperty, true); - root.Children.Add(scope); + var rootGrid = new Grid(); + rootGrid.UseLayoutRounding = false; + rootGrid.SetValue(Grid.IsSharedSizeScopeProperty, true); + rootGrid.Children.Add(scope); - root.Measure(new Size(50, 50)); - root.Arrange(new Rect(new Point(), new Point(50, 50))); + var root = new TestRoot(rootGrid) + { + Width = 50, + Height = 50, + }; + + root.LayoutManager.ExecuteInitialLayoutPass(); PrintColumnDefinitions(grids[0]); Assert.Equal(5, grids[0].ColumnDefinitions[0].ActualWidth); diff --git a/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs b/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs index e0f46d9fa9..e01c752658 100644 --- a/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs @@ -127,6 +127,88 @@ namespace Avalonia.Controls.UnitTests Assert.Empty(selected); } + [Fact] + public void Intersect_Should_Remove_Items_From_Beginning() + { + var ranges = new List { new IndexRange(0, 10) }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(2, 12), removed); + + Assert.Equal(2, result); + Assert.Equal(new[] { new IndexRange(2, 10) }, ranges); + Assert.Equal(new[] { new IndexRange(0, 1) }, removed); + } + + [Fact] + public void Intersect_Should_Remove_Items_From_End() + { + var ranges = new List { new IndexRange(0, 10) }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(0, 8), removed); + + Assert.Equal(2, result); + Assert.Equal(new[] { new IndexRange(0, 8) }, ranges); + Assert.Equal(new[] { new IndexRange(9, 10) }, removed); + } + + [Fact] + public void Intersect_Should_Remove_Entire_Range_Start() + { + var ranges = new List { new IndexRange(0, 5), new IndexRange(6, 10) }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(6, 10), removed); + + Assert.Equal(6, result); + Assert.Equal(new[] { new IndexRange(6, 10) }, ranges); + Assert.Equal(new[] { new IndexRange(0, 5) }, removed); + } + + [Fact] + public void Intersect_Should_Remove_Entire_Range_End() + { + var ranges = new List { new IndexRange(0, 5), new IndexRange(6, 10) }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(0, 4), removed); + + Assert.Equal(6, result); + Assert.Equal(new[] { new IndexRange(0, 4) }, ranges); + Assert.Equal(new[] { new IndexRange(5, 10) }, removed); + } + + [Fact] + public void Intersect_Should_Remove_Entire_Range_Start_End() + { + var ranges = new List + { + new IndexRange(0, 2), + new IndexRange(3, 7), + new IndexRange(8, 10) + }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(3, 7), removed); + + Assert.Equal(6, result); + Assert.Equal(new[] { new IndexRange(3, 7) }, ranges); + Assert.Equal(new[] { new IndexRange(0, 2), new IndexRange(8, 10) }, removed); + } + + [Fact] + public void Intersect_Should_Remove_Entire_And_Partial_Range_Start_End() + { + var ranges = new List + { + new IndexRange(0, 2), + new IndexRange(3, 7), + new IndexRange(8, 10) + }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(4, 6), removed); + + Assert.Equal(8, result); + Assert.Equal(new[] { new IndexRange(4, 6) }, ranges); + Assert.Equal(new[] { new IndexRange(0, 3), new IndexRange(7, 10) }, removed); + } + [Fact] public void Remove_Should_Remove_Entire_Range() { diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index a7679ba388..c4346e571b 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -329,7 +329,7 @@ namespace Avalonia.Controls.UnitTests return tb; }, true); - lm.ExecuteInitialLayoutPass(wnd); + lm.ExecuteInitialLayoutPass(); target.Items = items; diff --git a/tests/Avalonia.Controls.UnitTests/Mixins/PressedMixinTests.cs b/tests/Avalonia.Controls.UnitTests/Mixins/PressedMixinTests.cs new file mode 100644 index 0000000000..0ff1f40121 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Mixins/PressedMixinTests.cs @@ -0,0 +1,37 @@ +using Avalonia.Controls.Mixins; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Mixins +{ + public class PressedMixinTests + { + private MouseTestHelper _mouse = new MouseTestHelper(); + + [Fact] + public void Selected_Class_Should_Not_Initially_Be_Added() + { + var target = new TestControl(); + + Assert.Empty(target.Classes); + } + + [Fact] + public void Setting_IsSelected_Should_Add_Selected_Class() + { + var target = new TestControl(); + + _mouse.Down(target); + + Assert.Equal(new[] { ":pressed" }, target.Classes); + } + + private class TestControl : Control + { + static TestControl() + { + PressedMixin.Attach(); + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 9caae89cfe..d529cc4f75 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -232,7 +232,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var scroll = (TestScroller)target.Parent; scroll.Width = scroll.Height = 100; - scroll.LayoutManager.ExecuteInitialLayoutPass(scroll); + scroll.LayoutManager.ExecuteInitialLayoutPass(); // Ensure than an intermediate measure pass doesn't add more controls than it // should. This can happen if target gets measured with Size.Infinity which @@ -324,6 +324,11 @@ namespace Avalonia.Controls.UnitTests.Presenters private class TestScroller : ScrollContentPresenter, IRenderRoot, ILayoutRoot { + public TestScroller() + { + LayoutManager = new LayoutManager(this); + } + public IRenderer Renderer { get; } public Size ClientSize { get; } public double RenderScaling => 1; @@ -332,7 +337,7 @@ namespace Avalonia.Controls.UnitTests.Presenters public double LayoutScaling => 1; - public ILayoutManager LayoutManager { get; } = new LayoutManager(); + public ILayoutManager LayoutManager { get; } public IRenderTarget CreateRenderTarget() => throw new NotImplementedException(); public void Invalidate(Rect rect) => throw new NotImplementedException(); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 5a2cb60a56..a467c6dd03 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -723,7 +723,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var scroller = (TestScroller)target.Parent; scroller.Width = scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + scroller.LayoutManager.ExecuteInitialLayoutPass(); var last = (target.Items as IList)[10]; @@ -740,7 +740,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var scroller = (TestScroller)target.Parent; scroller.Width = scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + scroller.LayoutManager.ExecuteInitialLayoutPass(); var last = (target.Items as IList)[10]; @@ -838,7 +838,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var scroller = (TestScroller)target.Parent; scroller.Width = scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + scroller.LayoutManager.ExecuteInitialLayoutPass(); var from = target.Panel.Children[5]; var result = ((ILogicalScrollable)target).GetControlInDirection( @@ -855,7 +855,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var scroller = (TestScroller)target.Parent; scroller.Width = scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + scroller.LayoutManager.ExecuteInitialLayoutPass(); var from = target.Panel.Children[9]; var result = ((ILogicalScrollable)target).GetControlInDirection( @@ -874,7 +874,7 @@ namespace Avalonia.Controls.UnitTests.Presenters scroller.Width = 100; scroller.Height = 95; - scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + scroller.LayoutManager.ExecuteInitialLayoutPass(); var from = target.Panel.Children[8]; var result = ((ILogicalScrollable)target).GetControlInDirection( @@ -893,7 +893,7 @@ namespace Avalonia.Controls.UnitTests.Presenters scroller.Width = 100; scroller.Height = 95; - scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + scroller.LayoutManager.ExecuteInitialLayoutPass(); ((ILogicalScrollable)target).Offset = new Vector(0, 11); var from = target.Panel.Children[1]; @@ -946,7 +946,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var scroller = (TestScroller)target.Parent; scroller.Width = scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + scroller.LayoutManager.ExecuteInitialLayoutPass(); var from = target.Panel.Children[5]; var result = ((ILogicalScrollable)target).GetControlInDirection( @@ -963,7 +963,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var scroller = (TestScroller)target.Parent; scroller.Width = scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + scroller.LayoutManager.ExecuteInitialLayoutPass(); var from = target.Panel.Children[9]; var result = ((ILogicalScrollable)target).GetControlInDirection( @@ -982,7 +982,7 @@ namespace Avalonia.Controls.UnitTests.Presenters scroller.Width = 95; scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + scroller.LayoutManager.ExecuteInitialLayoutPass(); var from = target.Panel.Children[8]; var result = ((ILogicalScrollable)target).GetControlInDirection( @@ -1001,7 +1001,7 @@ namespace Avalonia.Controls.UnitTests.Presenters scroller.Width = 95; scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + scroller.LayoutManager.ExecuteInitialLayoutPass(); ((ILogicalScrollable)target).Offset = new Vector(11, 0); var from = target.Panel.Children[1]; @@ -1062,6 +1062,11 @@ namespace Avalonia.Controls.UnitTests.Presenters private class TestScroller : ScrollContentPresenter, IRenderRoot, ILayoutRoot, ILogicalRoot { + public TestScroller() + { + LayoutManager = new LayoutManager(this); + } + public IRenderer Renderer { get; } public Size ClientSize { get; } public double RenderScaling => 1; @@ -1070,7 +1075,7 @@ namespace Avalonia.Controls.UnitTests.Presenters public double LayoutScaling => 1; - public ILayoutManager LayoutManager { get; } = new LayoutManager(); + public ILayoutManager LayoutManager { get; } public IRenderTarget CreateRenderTarget() => throw new NotImplementedException(); public void Invalidate(Rect rect) => throw new NotImplementedException(); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs index 57f0230f48..f6130d49d6 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs @@ -117,13 +117,17 @@ namespace Avalonia.Controls.UnitTests.Presenters Width = 150, Height = 150, }, - Offset = new Vector(25, 25), }; target.UpdateChild(); target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); + target.Offset = new Vector(25, 25); + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + Assert.Equal(new Rect(-25, -25, 150, 150), content.Bounds); } @@ -345,4 +349,4 @@ namespace Avalonia.Controls.UnitTests.Presenters } } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs index 53591eda5b..a3292644b3 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs @@ -59,13 +59,17 @@ namespace Avalonia.Controls.UnitTests CanHorizontallyScroll = true, CanVerticallyScroll = true, Content = scrollable, - Offset = new Vector(25, 25), }; target.UpdateChild(); target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); + target.Offset = new Vector(25, 25); + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + Assert.Equal(new Rect(-25, -25, 150, 150), scrollable.Bounds); } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index 08b9c75dbc..f27ff3928c 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; @@ -181,18 +182,21 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Child_Should_Be_Measured_With_Infinity() + public void Child_Should_Be_Measured_With_MaxAutoSizeHint() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { var child = new ChildControl(); var window = new Window(); - var target = CreateTarget(window); + var popupImpl = MockWindowingPlatform.CreatePopupMock(window.PlatformImpl); + popupImpl.Setup(x => x.MaxAutoSizeHint).Returns(new Size(1200, 1000)); + var target = CreateTarget(window, popupImpl.Object); target.Content = child; target.Show(); - Assert.Equal(Size.Infinity, child.MeasureSize); + Assert.Equal(1, child.MeasureSizes.Count); + Assert.Equal(new Size(1200, 1000), child.MeasureSizes[0]); } } @@ -210,7 +214,8 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Content = child; target.Show(); - Assert.Equal(new Size(500, 600), child.MeasureSize); + Assert.Equal(1, child.MeasureSizes.Count); + Assert.Equal(new Size(500, 600), child.MeasureSizes[0]); } } @@ -228,7 +233,8 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Content = child; target.Show(); - Assert.Equal(new Size(500, 600), child.MeasureSize); + Assert.Equal(1, child.MeasureSizes.Count); + Assert.Equal(new Size(500, 600), child.MeasureSizes[0]); } } @@ -365,11 +371,11 @@ namespace Avalonia.Controls.UnitTests.Primitives private class ChildControl : Control { - public Size MeasureSize { get; private set; } + public List MeasureSizes { get; } = new List(); protected override Size MeasureOverride(Size availableSize) { - MeasureSize = availableSize; + MeasureSizes.Add(availableSize); return base.MeasureOverride(availableSize); } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 04d0ac0a7a..fe9c7b1261 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -994,7 +994,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Presenter.ApplyTemplate(); _helper.Down((Interactive)target.Presenter.Panel.Children[3]); - Assert.Equal(new[] { ":selected" }, target.Presenter.Panel.Children[3].Classes); + Assert.Equal(new[] { ":pressed", ":selected" }, target.Presenter.Panel.Children[3].Classes); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs index 8da1e26f0d..ab21c5d330 100644 --- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Layout; +using Avalonia.UnitTests; using Moq; using Xunit; @@ -150,12 +151,15 @@ namespace Avalonia.Controls.UnitTests public void Changing_Extent_Should_Raise_ScrollChanged() { var target = new ScrollViewer(); + var root = new TestRoot(target); var raised = 0; target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100)); target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); target.Offset = new Vector(10, 10); + root.LayoutManager.ExecuteInitialLayoutPass(); + target.ScrollChanged += (s, e) => { Assert.Equal(new Vector(11, 12), e.ExtentDelta); @@ -166,20 +170,26 @@ namespace Avalonia.Controls.UnitTests target.SetValue(ScrollViewer.ExtentProperty, new Size(111, 112)); - Assert.Equal(1, raised); + Assert.Equal(0, raised); + + root.LayoutManager.ExecuteLayoutPass(); + Assert.Equal(1, raised); } [Fact] public void Changing_Offset_Should_Raise_ScrollChanged() { var target = new ScrollViewer(); + var root = new TestRoot(target); var raised = 0; target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100)); target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); target.Offset = new Vector(10, 10); + root.LayoutManager.ExecuteInitialLayoutPass(); + target.ScrollChanged += (s, e) => { Assert.Equal(default, e.ExtentDelta); @@ -190,20 +200,26 @@ namespace Avalonia.Controls.UnitTests target.Offset = new Vector(22, 24); - Assert.Equal(1, raised); + Assert.Equal(0, raised); + root.LayoutManager.ExecuteLayoutPass(); + + Assert.Equal(1, raised); } [Fact] public void Changing_Viewport_Should_Raise_ScrollChanged() { var target = new ScrollViewer(); + var root = new TestRoot(target); var raised = 0; target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100)); target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); target.Offset = new Vector(10, 10); + root.LayoutManager.ExecuteInitialLayoutPass(); + target.ScrollChanged += (s, e) => { Assert.Equal(default, e.ExtentDelta); @@ -214,8 +230,11 @@ namespace Avalonia.Controls.UnitTests target.SetValue(ScrollViewer.ViewportProperty, new Size(56, 58)); - Assert.Equal(1, raised); + Assert.Equal(0, raised); + + root.LayoutManager.ExecuteLayoutPass(); + Assert.Equal(1, raised); } private Control CreateTemplate(ScrollViewer control, INameScope scope) diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs index ebf9c40012..24e82a69d0 100644 --- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs @@ -1512,6 +1512,47 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, raised); } + [Fact] + public void Batch_Update_Selection_Is_Correct_Throughout() + { + var data = new[] { "foo", "bar", "baz", "qux" }; + var target = new SelectionModel { Source = data }; + var raised = 0; + + using (target.Update()) + { + target.Select(1); + + Assert.Equal(new IndexPath(1), target.SelectedIndex); + Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new[] { "bar" }, target.SelectedItems); + + target.Deselect(1); + + Assert.Equal(new IndexPath(), target.SelectedIndex); + Assert.Empty(target.SelectedIndices); + Assert.Null(target.SelectedItem); + Assert.Empty(target.SelectedItems); + + target.SelectRange(new IndexPath(1), new IndexPath(1)); + + Assert.Equal(new IndexPath(1), target.SelectedIndex); + Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new[] { "bar" }, target.SelectedItems); + + target.ClearSelection(); + + Assert.Equal(new IndexPath(), target.SelectedIndex); + Assert.Empty(target.SelectedIndices); + Assert.Null(target.SelectedItem); + Assert.Empty(target.SelectedItems); + } + + Assert.Equal(0, raised); + } + [Fact] public void AutoSelect_Selects_When_Enabled() { @@ -1713,6 +1754,30 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, raised); } + [Fact] + public void AutoSelect_Is_Applied_At_End_Of_Batch_Update() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel { AutoSelect = true, Source = data }; + + using (target.Update()) + { + target.ClearSelection(); + + Assert.Equal(new IndexPath(), target.SelectedIndex); + Assert.Empty(target.SelectedIndices); + Assert.Null(target.SelectedItem); + Assert.Empty(target.SelectedItems); + } + + Assert.Equal(new IndexPath(0), target.SelectedIndex); + Assert.Equal(new[] { new IndexPath(0) }, target.SelectedIndices); + Assert.Equal("foo", target.SelectedItem); + Assert.Equal(new[] { "foo" }, target.SelectedItems); + + Assert.Equal(new IndexPath(0), target.SelectedIndex); + } + [Fact] public void Can_Replace_Parent_Children_Collection() { @@ -1806,6 +1871,87 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, node.PropertyChangedSubscriptions); } + [Fact] + public void Setting_SelectedIndex_To_Minus_1_Clears_Selection() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel { Source = data }; + target.SelectedIndex = new IndexPath(1); + target.SelectedIndex = new IndexPath(-1); + Assert.Empty(target.SelectedIndices); + } + + [Fact] + public void Assigning_Source_With_Less_Items_Than_Previous_Clears_Selection() + { + var data = new[] { "foo", "bar", "baz", "boo", "hoo" }; + var smallerData = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel { RetainSelectionOnReset = true }; + target.Source = data; + target.SelectedIndex = new IndexPath(4); + target.Source = smallerData; + Assert.Empty(target.SelectedIndices); + } + + [Fact] + public void Initializing_Source_With_Less_Items_Than_Selection_Trims_Selection() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel(); + target.SelectedIndex = new IndexPath(4); + target.Source = data; + Assert.Empty(target.SelectedIndices); + } + + [Fact] + public void Initializing_Source_With_Less_Items_Than_Selection_Trims_Selection_RetainSelection() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel { RetainSelectionOnReset = true }; + target.SelectedIndex = new IndexPath(4); + target.Source = data; + Assert.Empty(target.SelectedIndices); + } + + [Fact] + public void Initializing_Source_With_Less_Items_Than_Multiple_Selection_Trims_Selection() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel { RetainSelectionOnReset = true }; + target.Select(4); + target.Select(2); + target.Source = data; + Assert.Equal(1, target.SelectedIndices.Count); + Assert.Equal(new IndexPath(2), target.SelectedIndices.First()); + } + + [Fact] + public void Initializing_Source_With_Less_Items_Than_Selection_Raises_SelectionChanged() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel(); + var raised = 0; + + target.SelectedIndex = new IndexPath(4); + + target.SelectionChanged += (s, e) => + { + if (raised == 0) + { + Assert.Equal(new[] { Path(4) }, e.DeselectedIndices); + Assert.Equal(new object[] { null }, e.DeselectedItems); + Assert.Empty(e.SelectedIndices); + Assert.Empty(e.SelectedItems); + } + + ++raised; + }; + + target.Source = data; + + Assert.Equal(2, raised); + } + private int GetSubscriberCount(AvaloniaList list) { return ((INotifyCollectionChangedDebug)list).GetCollectionChangedSubscribers()?.Length ?? 0; diff --git a/tests/Avalonia.Controls.UnitTests/SplitViewTests.cs b/tests/Avalonia.Controls.UnitTests/SplitViewTests.cs new file mode 100644 index 0000000000..03653ec42c --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/SplitViewTests.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + + public class SplitViewTests + { + [Fact] + public void SplitView_PaneOpening_Should_Fire_Before_PaneOpened() + { + var splitView = new SplitView(); + + bool handledOpening = false; + splitView.PaneOpening += (x, e) => + { + handledOpening = true; + }; + + splitView.PaneOpened += (x, e) => + { + Assert.True(handledOpening); + }; + + splitView.IsPaneOpen = true; + } + + [Fact] + public void SplitView_PaneClosing_Should_Fire_Before_PaneClosed() + { + var splitView = new SplitView(); + splitView.IsPaneOpen = true; + + bool handledClosing = false; + splitView.PaneClosing += (x, e) => + { + handledClosing = true; + }; + + splitView.PaneClosed += (x, e) => + { + Assert.True(handledClosing); + }; + + splitView.IsPaneOpen = false; + } + + [Fact] + public void SplitView_Cancel_Close_Should_Prevent_Pane_From_Closing() + { + var splitView = new SplitView(); + splitView.IsPaneOpen = true; + + splitView.PaneClosing += (x, e) => + { + e.Cancel = true; + }; + + splitView.IsPaneOpen = false; + + Assert.True(splitView.IsPaneOpen); + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 24aacd4000..db9211ac3c 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -1,10 +1,12 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Controls.Utils; using Avalonia.LogicalTree; using Avalonia.Styling; using Avalonia.UnitTests; @@ -325,6 +327,28 @@ namespace Avalonia.Controls.UnitTests Assert.NotEqual(dataContext, tabItem.Content); } + [Fact] + public void Can_Have_Empty_Tab_Control() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new Markup.Xaml.AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var tabControl = window.FindControl("tabs"); + + tabControl.DataContext = new { Tabs = new List() }; + window.ApplyTemplate(); + + Assert.Equal(0, tabControl.Items.Count()); + } + } + private IControlTemplate TabControlTemplate() { return new FuncControlTemplate((parent, scope) => diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index d2f62cde04..217dec2d6d 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -654,6 +654,11 @@ namespace Avalonia.Controls.UnitTests public Task SetTextAsync(string text) => Task.CompletedTask; public Task ClearAsync() => Task.CompletedTask; + public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask; + + public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); + + public Task GetDataAsync(string format) => Task.FromResult((object)null); } } } diff --git a/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs b/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs new file mode 100644 index 0000000000..682f0eaadb --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs @@ -0,0 +1,173 @@ +using System; +using System.Linq; +using Avalonia.Controls.Shapes; +using Avalonia.Controls.Templates; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Moq; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class TimePickerTests + { + [Fact] + public void SelectedTimeChanged_Should_Fire_When_SelectedTime_Set() + { + using (UnitTestApplication.Start(Services)) + { + bool handled = false; + TimePicker timePicker = new TimePicker(); + timePicker.SelectedTimeChanged += (s, e) => + { + handled = true; + }; + TimeSpan value = TimeSpan.FromHours(10); + timePicker.SelectedTime = value; + Threading.Dispatcher.UIThread.RunJobs(); + Assert.True(handled); + } + } + + [Fact] + public void Using_24HourClock_Should_Hide_Period() + { + using (UnitTestApplication.Start(Services)) + { + TimePicker timePicker = new TimePicker() + { + ClockIdentifier = "12HourClock", + Template = CreateTemplate() + }; + timePicker.ApplyTemplate(); + + var desc = timePicker.GetVisualDescendants(); + Assert.True(desc.Count() > 1);//Should be layoutroot grid & button + Grid container = null; + + Assert.True(desc.ElementAt(1) is Button); + + container = (desc.ElementAt(1) as Button).Content as Grid; + Assert.True(container != null); + + var periodTextHost = container.Children[4] as Border; + Assert.True(periodTextHost != null); + Assert.True(periodTextHost.IsVisible); + + timePicker.ClockIdentifier = "24HourClock"; + Assert.False(periodTextHost.IsVisible); + } + } + + [Fact] + public void SelectedTime_null_Should_Use_Placeholders() + { + using (UnitTestApplication.Start(Services)) + { + TimePicker timePicker = new TimePicker() + { + Template = CreateTemplate() + }; + timePicker.ApplyTemplate(); + + var desc = timePicker.GetVisualDescendants(); + Assert.True(desc.Count() > 1);//Should be layoutroot grid & button + Grid container = null; + + Assert.True(desc.ElementAt(1) is Button); + + container = (desc.ElementAt(1) as Button).Content as Grid; + Assert.True(container != null); + + var hourTextHost = container.Children[0] as Border; + Assert.True(hourTextHost != null); + var hourText = hourTextHost.Child as TextBlock; + var minuteTextHost = container.Children[2] as Border; + Assert.True(minuteTextHost != null); + var minuteText = minuteTextHost.Child as TextBlock; + + TimeSpan ts = TimeSpan.FromHours(10); + timePicker.SelectedTime = ts; + Assert.False(hourText.Text == "hour"); + Assert.False(minuteText.Text == "minute"); + + timePicker.SelectedTime = null; + Assert.True(hourText.Text == "hour"); + Assert.True(minuteText.Text == "minute"); + } + } + + private static TestServices Services => TestServices.MockThreadingInterface.With( + fontManagerImpl: new MockFontManagerImpl(), + standardCursorFactory: Mock.Of(), + textShaperImpl: new MockTextShaperImpl()); + + private IControlTemplate CreateTemplate() + { + return new FuncControlTemplate((control, scope) => + { + var layoutRoot = new Grid + { + Name = "LayoutRoot" + }.RegisterInNameScope(scope); + //Skip contentpresenter + var flyoutButton = new Button + { + Name = "FlyoutButton" + }.RegisterInNameScope(scope); + var contentGrid = new Grid + { + Name = "FlyoutButtonContentGrid" + }.RegisterInNameScope(scope); + + var firstPickerHost = new Border + { + Name = "FirstPickerHost", + Child = new TextBlock + { + Name = "HourTextBlock" + }.RegisterInNameScope(scope) + }.RegisterInNameScope(scope); + Grid.SetColumn(firstPickerHost, 0); + + var secondPickerHost = new Border + { + Name = "SecondPickerHost", + Child = new TextBlock + { + Name = "MinuteTextBlock" + }.RegisterInNameScope(scope) + }.RegisterInNameScope(scope); + Grid.SetColumn(secondPickerHost, 2); + + var thirdPickerHost = new Border + { + Name = "ThirdPickerHost", + Child = new TextBlock + { + Name = "PeriodTextBlock" + }.RegisterInNameScope(scope) + }.RegisterInNameScope(scope); + Grid.SetColumn(thirdPickerHost, 4); + + var firstSpacer = new Rectangle + { + Name = "FirstColumnDivider" + }.RegisterInNameScope(scope); + Grid.SetColumn(firstSpacer, 1); + + var secondSpacer = new Rectangle + { + Name = "SecondColumnDivider" + }.RegisterInNameScope(scope); + Grid.SetColumn(secondSpacer, 3); + + contentGrid.Children.AddRange(new IControl[] { firstPickerHost, firstSpacer, secondPickerHost, secondSpacer, thirdPickerHost }); + flyoutButton.Content = contentGrid; + layoutRoot.Children.Add(flyoutButton); + return layoutRoot; + }); + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs index dc5b574db7..34b37e7635 100644 --- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs @@ -102,5 +102,76 @@ namespace Avalonia.Controls.UnitTests Assert.True(ToolTip.GetIsOpen(target)); } } + + [Fact] + public void Open_Class_Should_Not_Initially_Be_Added() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var toolTip = new ToolTip(); + var window = new Window(); + + var decorator = new Decorator() + { + [ToolTip.TipProperty] = toolTip + }; + + window.Content = decorator; + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + Assert.Empty(toolTip.Classes); + } + } + + [Fact] + public void Setting_IsOpen_Should_Add_Open_Class() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var toolTip = new ToolTip(); + var window = new Window(); + + var decorator = new Decorator() + { + [ToolTip.TipProperty] = toolTip + }; + + window.Content = decorator; + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + ToolTip.SetIsOpen(decorator, true); + + Assert.Equal(new[] { ":open" }, toolTip.Classes); + } + } + + [Fact] + public void Clearing_IsOpen_Should_Remove_Open_Class() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var toolTip = new ToolTip(); + var window = new Window(); + + var decorator = new Decorator() + { + [ToolTip.TipProperty] = toolTip + }; + + window.Content = decorator; + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + ToolTip.SetIsOpen(decorator, true); + ToolTip.SetIsOpen(decorator, false); + + Assert.Empty(toolTip.Classes); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 6b107b0187..e49e273bec 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -106,7 +106,7 @@ namespace Avalonia.Controls.UnitTests } }; - target.LayoutManager.ExecuteInitialLayoutPass(target); + target.LayoutManager.ExecuteInitialLayoutPass(); Assert.Equal(new Rect(0, 0, 321, 432), target.Bounds); } @@ -267,6 +267,23 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Close_Should_Dispose_LayoutManager() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var impl = new Mock(); + impl.SetupAllProperties(); + + var layoutManager = new Mock(); + var target = new TestTopLevel(impl.Object, layoutManager.Object); + + impl.Object.Closed(); + + layoutManager.Verify(x => x.Dispose()); + } + } + [Fact] public void Reacts_To_Changes_In_Global_Styles() { @@ -282,7 +299,7 @@ namespace Avalonia.Controls.UnitTests Content = child, }; - target.LayoutManager.ExecuteInitialLayoutPass(target); + target.LayoutManager.ExecuteInitialLayoutPass(); Assert.Equal(new Thickness(0), child.BorderThickness); @@ -295,7 +312,7 @@ namespace Avalonia.Controls.UnitTests }; Application.Current.Styles.Add(style); - target.LayoutManager.ExecuteInitialLayoutPass(target); + target.LayoutManager.ExecuteInitialLayoutPass(); Assert.Equal(new Thickness(2), child.BorderThickness); @@ -323,7 +340,7 @@ namespace Avalonia.Controls.UnitTests public TestTopLevel(ITopLevelImpl impl, ILayoutManager layoutManager = null) : base(impl) { - _layoutManager = layoutManager ?? new LayoutManager(); + _layoutManager = layoutManager ?? new LayoutManager(this); } protected override ILayoutManager CreateLayoutManager() => _layoutManager; diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 373f3e6861..c1bd45bcad 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -118,313 +118,340 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Clicking_Item_Should_Select_It() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var item = tree[0].Children[1].Children[0]; - var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + var item = tree[0].Children[1].Children[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); - Assert.NotNull(container); + Assert.NotNull(container); - _mouse.Click(container); + _mouse.Click(container); - Assert.Equal(item, target.SelectedItem); - Assert.True(container.IsSelected); + Assert.Equal(item, target.SelectedItem); + Assert.True(container.IsSelected); + } } [Fact] public void Clicking_WithControlModifier_Selected_Item_Should_Deselect_It() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var item = tree[0].Children[1].Children[0]; - var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + var item = tree[0].Children[1].Children[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); - Assert.NotNull(container); + Assert.NotNull(container); - target.SelectedItem = item; + target.SelectedItem = item; - Assert.True(container.IsSelected); + Assert.True(container.IsSelected); - _mouse.Click(container, modifiers: KeyModifiers.Control); + _mouse.Click(container, modifiers: KeyModifiers.Control); - Assert.Null(target.SelectedItem); - Assert.False(container.IsSelected); + Assert.Null(target.SelectedItem); + Assert.False(container.IsSelected); + } } [Fact] public void Clicking_WithControlModifier_Not_Selected_Item_Should_Select_It() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var item1 = tree[0].Children[1].Children[0]; - var container1 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); + var item1 = tree[0].Children[1].Children[0]; + var container1 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); - var item2 = tree[0].Children[1]; - var container2 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2); + var item2 = tree[0].Children[1]; + var container2 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2); - Assert.NotNull(container1); - Assert.NotNull(container2); + Assert.NotNull(container1); + Assert.NotNull(container2); - target.SelectedItem = item1; + target.SelectedItem = item1; - Assert.True(container1.IsSelected); + Assert.True(container1.IsSelected); - _mouse.Click(container2, modifiers: KeyModifiers.Control); - - Assert.Equal(item2, target.SelectedItem); - Assert.False(container1.IsSelected); - Assert.True(container2.IsSelected); + _mouse.Click(container2, modifiers: KeyModifiers.Control); + + Assert.Equal(item2, target.SelectedItem); + Assert.False(container1.IsSelected); + Assert.True(container2.IsSelected); + } } [Fact] public void Clicking_WithControlModifier_Selected_Item_Should_Deselect_And_Remove_From_SelectedItems() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var rootNode = tree[0]; + var rootNode = tree[0]; - var item1 = rootNode.Children[0]; - var item2 = rootNode.Children.Last(); + var item1 = rootNode.Children[0]; + var item2 = rootNode.Children.Last(); - var item1Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); - var item2Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2); + var item1Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); + var item2Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2); - ClickContainer(item1Container, KeyModifiers.Control); - Assert.True(item1Container.IsSelected); + ClickContainer(item1Container, KeyModifiers.Control); + Assert.True(item1Container.IsSelected); - ClickContainer(item2Container, KeyModifiers.Control); - Assert.True(item2Container.IsSelected); + ClickContainer(item2Container, KeyModifiers.Control); + Assert.True(item2Container.IsSelected); - Assert.Equal(new[] {item1, item2}, target.Selection.SelectedItems.OfType()); + Assert.Equal(new[] { item1, item2 }, target.Selection.SelectedItems.OfType()); - ClickContainer(item1Container, KeyModifiers.Control); - Assert.False(item1Container.IsSelected); + ClickContainer(item1Container, KeyModifiers.Control); + Assert.False(item1Container.IsSelected); - Assert.DoesNotContain(item1, target.Selection.SelectedItems.OfType()); + Assert.DoesNotContain(item1, target.Selection.SelectedItems.OfType()); + } } [Fact] public void Clicking_WithShiftModifier_DownDirection_Should_Select_Range_Of_Items() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var rootNode = tree[0]; + var rootNode = tree[0]; - var from = rootNode.Children[0]; - var to = rootNode.Children.Last(); + var from = rootNode.Children[0]; + var to = rootNode.Children.Last(); - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(fromContainer, KeyModifiers.None); - Assert.True(fromContainer.IsSelected); + Assert.True(fromContainer.IsSelected); - ClickContainer(toContainer, KeyModifiers.Shift); - AssertChildrenSelected(target, rootNode); + ClickContainer(toContainer, KeyModifiers.Shift); + AssertChildrenSelected(target, rootNode); + } } [Fact] public void Clicking_WithShiftModifier_UpDirection_Should_Select_Range_Of_Items() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var rootNode = tree[0]; + var rootNode = tree[0]; - var from = rootNode.Children.Last(); - var to = rootNode.Children[0]; + var from = rootNode.Children.Last(); + var to = rootNode.Children[0]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(fromContainer, KeyModifiers.None); - Assert.True(fromContainer.IsSelected); + Assert.True(fromContainer.IsSelected); - ClickContainer(toContainer, KeyModifiers.Shift); - AssertChildrenSelected(target, rootNode); + ClickContainer(toContainer, KeyModifiers.Shift); + AssertChildrenSelected(target, rootNode); + } } [Fact] public void Clicking_First_Item_Of_SelectedItems_Should_Select_Only_It() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var rootNode = tree[0]; + var rootNode = tree[0]; - var from = rootNode.Children.Last(); - var to = rootNode.Children[0]; + var from = rootNode.Children.Last(); + var to = rootNode.Children[0]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(fromContainer, KeyModifiers.None); - ClickContainer(toContainer, KeyModifiers.Shift); - AssertChildrenSelected(target, rootNode); + ClickContainer(toContainer, KeyModifiers.Shift); + AssertChildrenSelected(target, rootNode); - ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(fromContainer, KeyModifiers.None); - Assert.True(fromContainer.IsSelected); + Assert.True(fromContainer.IsSelected); - foreach (var child in rootNode.Children) - { - if (child == from) + foreach (var child in rootNode.Children) { - continue; - } + if (child == from) + { + continue; + } - var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(child); + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(child); - Assert.False(container.IsSelected); + Assert.False(container.IsSelected); + } } } [Fact] public void Setting_SelectedItem_Should_Set_Container_Selected() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var item = tree[0].Children[1].Children[0]; - var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + var item = tree[0].Children[1].Children[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); - Assert.NotNull(container); + Assert.NotNull(container); - target.SelectedItem = item; + target.SelectedItem = item; - Assert.True(container.IsSelected); + Assert.True(container.IsSelected); + } } [Fact] public void Setting_SelectedItem_Should_Raise_SelectedItemChanged_Event() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var item = tree[0].Children[1].Children[0]; + var item = tree[0].Children[1].Children[0]; - var called = false; - target.SelectionChanged += (s, e) => - { - Assert.Empty(e.RemovedItems); - Assert.Equal(1, e.AddedItems.Count); - Assert.Same(item, e.AddedItems[0]); - called = true; - }; + var called = false; + target.SelectionChanged += (s, e) => + { + Assert.Empty(e.RemovedItems); + Assert.Equal(1, e.AddedItems.Count); + Assert.Same(item, e.AddedItems[0]); + called = true; + }; - target.SelectedItem = item; - Assert.True(called); + target.SelectedItem = item; + Assert.True(called); + } } [Fact] @@ -564,7 +591,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Keyboard_Navigation_Should_Move_To_Last_Selected_Node() { - using (UnitTestApplication.Start(TestServices.RealFocus)) + using (Application()) { var focus = FocusManager.Instance; var navigation = AvaloniaLocator.Current.GetService(); @@ -647,7 +674,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Pressing_SelectAll_Gesture_With_Downward_Range_Selected_Should_Select_All_Nodes() { - using (UnitTestApplication.Start()) + using (Application()) { var tree = CreateTestTreeData(); var target = new TreeView @@ -694,7 +721,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Pressing_SelectAll_Gesture_With_Upward_Range_Selected_Should_Select_All_Nodes() { - using (UnitTestApplication.Start()) + using (Application()) { var tree = CreateTestTreeData(); var target = new TreeView @@ -768,97 +795,106 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Right_Click_On_UnselectedItem_Should_Clear_Existing_Selection() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); - var rootNode = tree[0]; - var to = rootNode.Children[0]; - var then = rootNode.Children[1]; + var rootNode = tree[0]; + var to = rootNode.Children[0]; + var then = rootNode.Children[1]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(rootNode); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - var thenContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(then); + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(rootNode); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var thenContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(then); - ClickContainer(fromContainer, KeyModifiers.None); - ClickContainer(toContainer, KeyModifiers.Shift); + ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(toContainer, KeyModifiers.Shift); - Assert.Equal(2, target.Selection.SelectedItems.Count); + Assert.Equal(2, target.Selection.SelectedItems.Count); - _mouse.Click(thenContainer, MouseButton.Right); + _mouse.Click(thenContainer, MouseButton.Right); - Assert.Equal(1, target.Selection.SelectedItems.Count); + Assert.Equal(1, target.Selection.SelectedItems.Count); + } } [Fact] public void Shift_Right_Click_Should_Not_Select_Multiple() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); - var rootNode = tree[0]; - var from = rootNode.Children[0]; - var to = rootNode.Children[1]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var rootNode = tree[0]; + var from = rootNode.Children[0]; + var to = rootNode.Children[1]; + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - _mouse.Click(fromContainer); - _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Shift); + _mouse.Click(fromContainer); + _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Shift); - Assert.Equal(1, target.Selection.SelectedItems.Count); + Assert.Equal(1, target.Selection.SelectedItems.Count); + } } [Fact] public void Ctrl_Right_Click_Should_Not_Select_Multiple() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); - var rootNode = tree[0]; - var from = rootNode.Children[0]; - var to = rootNode.Children[1]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var rootNode = tree[0]; + var from = rootNode.Children[0]; + var to = rootNode.Children[1]; + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - _mouse.Click(fromContainer); - _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Control); + _mouse.Click(fromContainer); + _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Control); - Assert.Equal(1, target.Selection.SelectedItems.Count); + Assert.Equal(1, target.Selection.SelectedItems.Count); + } } [Fact] @@ -944,7 +980,7 @@ namespace Avalonia.Controls.UnitTests public void Auto_Expanding_In_Style_Should_Not_Break_Range_Selection() { /// Issue #2980. - using (UnitTestApplication.Start(TestServices.RealStyler)) + using (Application()) { var target = new DerivedTreeView { @@ -1183,12 +1219,12 @@ namespace Avalonia.Controls.UnitTests } } - void ClickContainer(IControl container, KeyModifiers modifiers) + private void ClickContainer(IControl container, KeyModifiers modifiers) { _mouse.Click(container, modifiers: modifiers); } - void AssertChildrenSelected(TreeView treeView, Node rootNode) + private void AssertChildrenSelected(TreeView treeView, Node rootNode) { foreach (var child in rootNode.Children) { @@ -1198,6 +1234,16 @@ namespace Avalonia.Controls.UnitTests } } + private IDisposable Application() + { + return UnitTestApplication.Start( + TestServices.MockThreadingInterface.With( + focusManager: new FocusManager(), + keyboardDevice: () => new KeyboardDevice(), + keyboardNavigation: new KeyboardNavigationHandler(), + inputManager: new InputManager())); + } + private class Node : NotifyingBase { private IAvaloniaList _children; diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 80c8a34ffd..e2b0def00b 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -297,12 +297,12 @@ namespace Avalonia.Controls.UnitTests { var parentWindowImpl = MockWindowingPlatform.CreateWindowMock(); parentWindowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480)); - parentWindowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080)); + parentWindowImpl.Setup(x => x.MaxAutoSizeHint).Returns(new Size(1920, 1080)); parentWindowImpl.Setup(x => x.Scaling).Returns(1); var windowImpl = MockWindowingPlatform.CreateWindowMock(); windowImpl.Setup(x => x.ClientSize).Returns(new Size(320, 200)); - windowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080)); + windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(new Size(1920, 1080)); windowImpl.Setup(x => x.Scaling).Returns(1); var parentWindowServices = TestServices.StyledWindow.With( @@ -381,12 +381,15 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight() + public void Child_Should_Be_Measured_With_MaxAutoSizeHint_If_SizeToContent_Is_WidthAndHeight() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { + var windowImpl = MockWindowingPlatform.CreateWindowMock(); + windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(new Size(1200, 1000)); + var child = new ChildControl(); - var target = new Window + var target = new Window(windowImpl.Object) { Width = 100, Height = 50, @@ -394,10 +397,10 @@ namespace Avalonia.Controls.UnitTests Content = child }; - Show(target); + target.Show(); Assert.Equal(1, child.MeasureSizes.Count); - Assert.Equal(Size.Infinity, child.MeasureSizes[0]); + Assert.Equal(new Size(1200, 1000), child.MeasureSizes[0]); } } diff --git a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs index a9c991b8b9..25e8c82b1a 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs @@ -20,11 +20,11 @@ namespace Avalonia.Controls.UnitTests return _windowImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1); } - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public IWindowImpl CreateEmbeddableWindow() { throw new NotImplementedException(); } public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1); } -} \ No newline at end of file +} diff --git a/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj b/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj index 73a1d586b5..523c5e7ff4 100644 --- a/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj +++ b/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj @@ -32,5 +32,4 @@ - diff --git a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs index 572749a58a..c6ecc0a7e5 100644 --- a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs +++ b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs @@ -41,7 +41,7 @@ namespace Avalonia.Direct2D1.UnitTests.Media var fontManager = new FontManagerImpl(); var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(new FontFamily("A, B, Arial"), FontWeight.Bold)); + new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold)); var font = glyphTypeface.DWFont; @@ -105,7 +105,7 @@ namespace Avalonia.Direct2D1.UnitTests.Media var fontManager = new FontManagerImpl(); var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(s_fontUri, FontWeight.Black, FontStyle.Italic)); + new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black)); var font = glyphTypeface.DWFont; diff --git a/tests/Avalonia.Input.UnitTests/InputElement_Focus.cs b/tests/Avalonia.Input.UnitTests/InputElement_Focus.cs index d396119555..09fae7207f 100644 --- a/tests/Avalonia.Input.UnitTests/InputElement_Focus.cs +++ b/tests/Avalonia.Input.UnitTests/InputElement_Focus.cs @@ -42,5 +42,84 @@ namespace Avalonia.Input.UnitTests Assert.Null(FocusManager.Instance.Current); } } + + [Fact] + public void Focus_Pseudoclass_Should_Be_Applied_On_Focus() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var target1 = new Decorator(); + var target2 = new Decorator(); + var root = new TestRoot + { + Child = new StackPanel + { + Children = + { + target1, + target2 + } + } + }; + + target1.ApplyTemplate(); + target2.ApplyTemplate(); + + + FocusManager.Instance?.Focus(target1); + Assert.True(target1.IsFocused); + Assert.True(target1.Classes.Contains(":focus")); + Assert.False(target2.IsFocused); + Assert.False(target2.Classes.Contains(":focus")); + + FocusManager.Instance?.Focus(target2, NavigationMethod.Tab); + Assert.False(target1.IsFocused); + Assert.False(target1.Classes.Contains(":focus")); + Assert.True(target2.IsFocused); + Assert.True(target2.Classes.Contains(":focus")); + } + } + + [Fact] + public void Control_FocusVsisible_Pseudoclass_Should_Be_Applied_On_Tab_And_DirectionalFocus() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var target1 = new Decorator(); + var target2 = new Decorator(); + var root = new TestRoot + { + Child = new StackPanel + { + Children = + { + target1, + target2 + } + } + }; + + target1.ApplyTemplate(); + target2.ApplyTemplate(); + + FocusManager.Instance?.Focus(target1); + Assert.True(target1.IsFocused); + Assert.False(target1.Classes.Contains(":focus-visible")); + Assert.False(target2.IsFocused); + Assert.False(target2.Classes.Contains(":focus-visible")); + + FocusManager.Instance?.Focus(target2, NavigationMethod.Tab); + Assert.False(target1.IsFocused); + Assert.False(target1.Classes.Contains(":focus-visible")); + Assert.True(target2.IsFocused); + Assert.True(target2.Classes.Contains(":focus-visible")); + + FocusManager.Instance?.Focus(target1, NavigationMethod.Directional); + Assert.True(target1.IsFocused); + Assert.True(target1.Classes.Contains(":focus-visible")); + Assert.False(target2.IsFocused); + Assert.False(target2.Classes.Contains(":focus-visible")); + } + } } } diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs index 01f0ec247f..1efbbed2e8 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs @@ -185,6 +185,35 @@ namespace Avalonia.Input.UnitTests Assert.Equal(next, result); } + [Fact] + public void Next_Skips_Non_TabStop_Siblings() + { + Button current; + Button next; + + var top = new StackPanel + { + Children = + { + new StackPanel + { + Children = + { + new Button { Name = "Button1" }, + new Button { Name = "Button2" }, + (current = new Button { Name = "Button3" }), + new Button { Name="Button4", [KeyboardNavigation.IsTabStopProperty] = false } + } + }, + (next = new Button { Name = "Button5" }), + } + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); + + Assert.Equal(next, result); + } + [Fact] public void Next_Continue_Returns_First_Control_In_Next_Uncle_Container() { diff --git a/tests/Avalonia.Layout.UnitTests/LayoutHelperTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutHelperTests.cs new file mode 100644 index 0000000000..f9187edc42 --- /dev/null +++ b/tests/Avalonia.Layout.UnitTests/LayoutHelperTests.cs @@ -0,0 +1,27 @@ +using System; +using Xunit; + +namespace Avalonia.Layout.UnitTests +{ + public class LayoutHelperTests + { + [Fact] + public void Round_Layout_Value_Without_DPI_Aware() + { + const double value = 42.5; + var expectedValue = Math.Round(value); + var actualValue = LayoutHelper.RoundLayoutValue(value, 1.0); + Assert.Equal(expectedValue, actualValue); + } + + [Fact] + public void Round_Layout_Value_With_DPI_Aware() + { + const double dpiScale = 1.25; + const double value = 42.5; + var expectedValue = Math.Round(value * dpiScale) / dpiScale; + var actualValue = LayoutHelper.RoundLayoutValue(value, dpiScale); + Assert.Equal(expectedValue, actualValue); + } + } +} diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index 332e8a751d..e429adce85 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -13,7 +13,7 @@ namespace Avalonia.Layout.UnitTests var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); control.Measured = control.Arranged = false; control.InvalidateMeasure(); @@ -23,13 +23,29 @@ namespace Avalonia.Layout.UnitTests Assert.True(control.Arranged); } + [Fact] + public void Doesnt_Measure_And_Arrange_InvalidateMeasured_Control_When_TopLevel_Is_Not_Visible() + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control, IsVisible = false }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + control.Measured = control.Arranged = false; + + control.InvalidateMeasure(); + root.LayoutManager.ExecuteLayoutPass(); + + Assert.False(control.Measured); + Assert.False(control.Arranged); + } + [Fact] public void Arranges_InvalidateArranged_Control() { var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); control.Measured = control.Arranged = false; control.InvalidateArrange(); @@ -45,7 +61,7 @@ namespace Avalonia.Layout.UnitTests var control = new LayoutTestControl(); var root = new LayoutTestRoot(); - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); root.Child = control; root.Measured = root.Arranged = false; @@ -80,7 +96,7 @@ namespace Avalonia.Layout.UnitTests root.DoMeasureOverride = MeasureOverride; control1.DoMeasureOverride = MeasureOverride; control2.DoMeasureOverride = MeasureOverride; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); control2.InvalidateMeasure(); control1.InvalidateMeasure(); @@ -115,7 +131,7 @@ namespace Avalonia.Layout.UnitTests root.DoMeasureOverride = MeasureOverride; control1.DoMeasureOverride = MeasureOverride; control2.DoMeasureOverride = MeasureOverride; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); control2.InvalidateMeasure(); root.InvalidateMeasure(); @@ -132,7 +148,7 @@ namespace Avalonia.Layout.UnitTests var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); root.Measured = root.Arranged = false; control.Measured = control.Arranged = false; @@ -151,7 +167,7 @@ namespace Avalonia.Layout.UnitTests var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); control.Measured = control.Arranged = false; control.InvalidateMeasure(); @@ -177,7 +193,7 @@ namespace Avalonia.Layout.UnitTests return new Size(100, 100); }; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); Assert.Equal(Size.Infinity, availableSize); } @@ -199,7 +215,7 @@ namespace Avalonia.Layout.UnitTests return s; }; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); Assert.Equal(new Size(100, 100), arrangeSize); root.Width = 120; @@ -225,7 +241,7 @@ namespace Avalonia.Layout.UnitTests } }; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); Assert.Equal(new Size(0, 0), root.DesiredSize); border.Width = 100; @@ -241,7 +257,7 @@ namespace Avalonia.Layout.UnitTests var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); control.Measured = false; int cnt = 0; @@ -272,7 +288,7 @@ namespace Avalonia.Layout.UnitTests var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); control.Arranged = false; int cnt = 0; @@ -313,7 +329,7 @@ namespace Avalonia.Layout.UnitTests panel.Children.AddRange(nonArrageableTargets); panel.Children.AddRange(targets); - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); foreach (var c in panel.Children.OfType()) { @@ -347,7 +363,7 @@ namespace Avalonia.Layout.UnitTests var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); control.Measured = false; control.DoMeasureOverride = (l, s) => @@ -380,7 +396,7 @@ namespace Avalonia.Layout.UnitTests var root = new LayoutTestRoot { Child = control }; var count = 0; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); control.Measured = false; control.DoMeasureOverride = (l, s) => @@ -399,7 +415,7 @@ namespace Avalonia.Layout.UnitTests root.InvalidateMeasure(); control.InvalidateMeasure(); - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); Assert.Equal(new Size(200, 200), control.Bounds.Size); Assert.Equal(new Size(200, 200), control.DesiredSize); diff --git a/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs index a1c1e62f58..44a5af94b9 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs @@ -203,6 +203,123 @@ namespace Avalonia.Layout.UnitTests Assert.Equal(new Rect(expectedX, 0, childWidth, 100), target.Bounds); } + [Fact] + public void LayoutUpdated_Is_Called_At_End_Of_Layout_Pass() + { + Border border1; + Border border2; + var root = new TestRoot + { + Child = border1 = new Border + { + Child = border2 = new Border(), + }, + }; + var raised = 0; + + void ValidateBounds(object sender, EventArgs e) + { + Assert.Equal(new Rect(0, 0, 100, 100), border1.Bounds); + Assert.Equal(new Rect(0, 0, 100, 100), border2.Bounds); + ++raised; + } + + root.LayoutUpdated += ValidateBounds; + border1.LayoutUpdated += ValidateBounds; + border2.LayoutUpdated += ValidateBounds; + + root.Measure(new Size(100, 100)); + root.Arrange(new Rect(0, 0, 100, 100)); + + root.LayoutManager.ExecuteLayoutPass(); + + Assert.Equal(3, raised); + Assert.Equal(new Rect(0, 0, 100, 100), border1.Bounds); + Assert.Equal(new Rect(0, 0, 100, 100), border2.Bounds); + } + + [Fact] + public void LayoutUpdated_Subscribes_To_LayoutManager() + { + Border target; + var layoutManager = new Mock(); + layoutManager.SetupAdd(m => m.LayoutUpdated += (sender, args) => { }); + + var root = new TestRoot + { + Child = new Border + { + Child = target = new Border(), + }, + LayoutManager = layoutManager.Object, + }; + + void Handler(object sender, EventArgs e) {} + + layoutManager.Invocations.Clear(); + target.LayoutUpdated += Handler; + + layoutManager.VerifyAdd( + x => x.LayoutUpdated += It.IsAny(), + Times.Once); + + layoutManager.Invocations.Clear(); + target.LayoutUpdated -= Handler; + + layoutManager.VerifyRemove( + x => x.LayoutUpdated -= It.IsAny(), + Times.Once); + } + + [Fact] + public void LayoutManager_LayoutUpdated_Is_Subscribed_When_Attached_To_Tree() + { + Border border1; + var layoutManager = new Mock(); + layoutManager.SetupAdd(m => m.LayoutUpdated += (sender, args) => { }); + + var root = new TestRoot + { + Child = border1 = new Border(), + LayoutManager = layoutManager.Object, + }; + + var border2 = new Border(); + border2.LayoutUpdated += (s, e) => { }; + + layoutManager.Invocations.Clear(); + border1.Child = border2; + + layoutManager.VerifyAdd( + x => x.LayoutUpdated += It.IsAny(), + Times.Once); + } + + [Fact] + public void LayoutManager_LayoutUpdated_Is_Unsubscribed_When_Detached_From_Tree() + { + Border border1; + var layoutManager = new Mock(); + layoutManager.SetupAdd(m => m.LayoutUpdated += (sender, args) => { }); + + var root = new TestRoot + { + Child = border1 = new Border(), + LayoutManager = layoutManager.Object, + }; + + var border2 = new Border(); + border2.LayoutUpdated += (s, e) => { }; + border1.Child = border2; + + layoutManager.Invocations.Clear(); + border1.Child = null; + + layoutManager.VerifyRemove( + x => x.LayoutUpdated -= It.IsAny(), + Times.Once); + } + private class TestLayoutable : Layoutable { public Size ArrangeSize { get; private set; } diff --git a/tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs b/tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs new file mode 100644 index 0000000000..504e3fa585 --- /dev/null +++ b/tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs @@ -0,0 +1,424 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Media; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Layout.UnitTests +{ + public class LayoutableTests_EffectiveViewportChanged + { + [Fact] + public async Task EffectiveViewportChanged_Not_Raised_When_Control_Added_To_Tree() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas(); + var raised = 0; + + target.EffectiveViewportChanged += (s, e) => + { + ++raised; + }; + + root.Child = target; + + Assert.Equal(0, raised); + }); + } + + [Fact] + public async Task EffectiveViewportChanged_Raised_Before_LayoutUpdated() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas(); + var raised = 0; + + target.EffectiveViewportChanged += (s, e) => + { + ++raised; + }; + + root.Child = target; + + await ExecuteInitialLayoutPass(root); + + Assert.Equal(1, raised); + }); + } + + [Fact] + public async Task Parent_Affects_EffectiveViewport() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas { Width = 100, Height = 100 }; + var parent = new Border { Width = 200, Height = 200, Child = target }; + var raised = 0; + + root.Child = parent; + + target.EffectiveViewportChanged += (s, e) => + { + Assert.Equal(new Rect(-550, -400, 1200, 900), e.EffectiveViewport); + ++raised; + }; + + await ExecuteInitialLayoutPass(root); + }); + } + + [Fact] + public async Task Invalidating_In_Handler_Causes_Layout_To_Be_Rerun_Before_LayoutUpdated_Raised() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new TestCanvas(); + var raised = 0; + var layoutUpdatedRaised = 0; + + root.LayoutUpdated += (s, e) => + { + Assert.Equal(2, target.MeasureCount); + Assert.Equal(2, target.ArrangeCount); + ++layoutUpdatedRaised; + }; + + target.EffectiveViewportChanged += (s, e) => + { + target.InvalidateMeasure(); + ++raised; + }; + + root.Child = target; + + await ExecuteInitialLayoutPass(root); + + Assert.Equal(1, raised); + Assert.Equal(1, layoutUpdatedRaised); + }); + } + + [Fact] + public async Task Viewport_Extends_Beyond_Centered_Control() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas { Width = 52, Height = 52, }; + var raised = 0; + + target.EffectiveViewportChanged += (s, e) => + { + Assert.Equal(new Rect(-574, -424, 1200, 900), e.EffectiveViewport); + ++raised; + }; + + root.Child = target; + + await ExecuteInitialLayoutPass(root); + Assert.Equal(1, raised); + }); + } + + [Fact] + public async Task Viewport_Extends_Beyond_Nested_Centered_Control() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas { Width = 52, Height = 52 }; + var parent = new Border { Width = 100, Height = 100, Child = target }; + var raised = 0; + + target.EffectiveViewportChanged += (s, e) => + { + Assert.Equal(new Rect(-574, -424, 1200, 900), e.EffectiveViewport); + ++raised; + }; + + root.Child = parent; + + await ExecuteInitialLayoutPass(root); + Assert.Equal(1, raised); + }); + } + + [Fact] + public async Task ScrollViewer_Determines_EffectiveViewport() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas { Width = 200, Height = 200 }; + var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate() }; + var raised = 0; + + target.EffectiveViewportChanged += (s, e) => + { + Assert.Equal(new Rect(0, 0, 100, 100), e.EffectiveViewport); + ++raised; + }; + + root.Child = scroller; + + await ExecuteInitialLayoutPass(root); + Assert.Equal(1, raised); + }); + } + + [Fact] + public async Task Scrolled_ScrollViewer_Determines_EffectiveViewport() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas { Width = 200, Height = 200 }; + var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate() }; + var raised = 0; + + root.Child = scroller; + + await ExecuteInitialLayoutPass(root); + scroller.Offset = new Vector(0, 10); + + await ExecuteScrollerLayoutPass(root, scroller, target, (s, e) => + { + Assert.Equal(new Rect(0, 10, 100, 100), e.EffectiveViewport); + ++raised; + }); + + Assert.Equal(1, raised); + }); + } + + [Fact] + public async Task Moving_Parent_Updates_EffectiveViewport() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas { Width = 100, Height = 100 }; + var parent = new Border { Width = 200, Height = 200, Child = target }; + var raised = 0; + + root.Child = parent; + + await ExecuteInitialLayoutPass(root); + + target.EffectiveViewportChanged += (s, e) => + { + Assert.Equal(new Rect(-554, -400, 1200, 900), e.EffectiveViewport); + ++raised; + }; + + parent.Margin = new Thickness(8, 0, 0, 0); + await ExecuteLayoutPass(root); + + Assert.Equal(1, raised); + }); + } + + [Fact] + public async Task Translate_Transform_Doesnt_Affect_EffectiveViewport() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas { Width = 100, Height = 100 }; + var parent = new Border { Width = 200, Height = 200, Child = target }; + var raised = 0; + + root.Child = parent; + + await ExecuteInitialLayoutPass(root); + target.EffectiveViewportChanged += (s, e) => ++raised; + target.RenderTransform = new TranslateTransform { X = 8 }; + target.InvalidateMeasure(); + await ExecuteLayoutPass(root); + + Assert.Equal(0, raised); + }); + } + + [Fact] + public async Task Translate_Transform_On_Parent_Affects_EffectiveViewport() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas { Width = 100, Height = 100 }; + var parent = new Border { Width = 200, Height = 200, Child = target }; + var raised = 0; + + root.Child = parent; + + await ExecuteInitialLayoutPass(root); + + target.EffectiveViewportChanged += (s, e) => + { + Assert.Equal(new Rect(-558, -400, 1200, 900), e.EffectiveViewport); + ++raised; + }; + + // Change the parent render transform to move it. A layout is then needed before + // EffectiveViewportChanged is raised. + parent.RenderTransform = new TranslateTransform { X = 8 }; + parent.InvalidateMeasure(); + await ExecuteLayoutPass(root); + + Assert.Equal(1, raised); + }); + } + + [Fact] + public async Task Rotate_Transform_On_Parent_Affects_EffectiveViewport() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas { Width = 100, Height = 100 }; + var parent = new Border { Width = 200, Height = 200, Child = target }; + var raised = 0; + + root.Child = parent; + + await ExecuteInitialLayoutPass(root); + + target.EffectiveViewportChanged += (s, e) => + { + AssertArePixelEqual(new Rect(-651, -792, 1484, 1484), e.EffectiveViewport); + ++raised; + }; + + parent.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Absolute); + parent.RenderTransform = new RotateTransform { Angle = 45 }; + parent.InvalidateMeasure(); + await ExecuteLayoutPass(root); + + Assert.Equal(1, raised); + }); + } + + private TestRoot CreateRoot() => new TestRoot { Width = 1200, Height = 900 }; + + private Task ExecuteInitialLayoutPass(TestRoot root) + { + root.LayoutManager.ExecuteInitialLayoutPass(); + return Task.CompletedTask; + } + + private Task ExecuteLayoutPass(TestRoot root) + { + root.LayoutManager.ExecuteLayoutPass(); + return Task.CompletedTask; + } + + private Task ExecuteScrollerLayoutPass( + TestRoot root, + ScrollViewer scroller, + Control target, + Action handler) + { + void ViewportChanged(object sender, EffectiveViewportChangedEventArgs e) + { + handler(sender, e); + } + + target.EffectiveViewportChanged += ViewportChanged; + root.LayoutManager.ExecuteLayoutPass(); + return Task.CompletedTask; + } + private IControlTemplate ScrollViewerTemplate() + { + return new FuncControlTemplate((control, scope) => new Grid + { + ColumnDefinitions = new ColumnDefinitions + { + new ColumnDefinition(1, GridUnitType.Star), + new ColumnDefinition(GridLength.Auto), + }, + RowDefinitions = new RowDefinitions + { + new RowDefinition(1, GridUnitType.Star), + new RowDefinition(GridLength.Auto), + }, + Children = + { + new ScrollContentPresenter + { + Name = "PART_ContentPresenter", + [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], + [~~ScrollContentPresenter.ExtentProperty] = control[~~ScrollViewer.ExtentProperty], + [~~ScrollContentPresenter.OffsetProperty] = control[~~ScrollViewer.OffsetProperty], + [~~ScrollContentPresenter.ViewportProperty] = control[~~ScrollViewer.ViewportProperty], + [~ScrollContentPresenter.CanHorizontallyScrollProperty] = control[~ScrollViewer.CanHorizontallyScrollProperty], + [~ScrollContentPresenter.CanVerticallyScrollProperty] = control[~ScrollViewer.CanVerticallyScrollProperty], + }.RegisterInNameScope(scope), + new ScrollBar + { + Name = "horizontalScrollBar", + Orientation = Orientation.Horizontal, + [~RangeBase.MaximumProperty] = control[~ScrollViewer.HorizontalScrollBarMaximumProperty], + [~~RangeBase.ValueProperty] = control[~~ScrollViewer.HorizontalScrollBarValueProperty], + [~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.HorizontalScrollBarViewportSizeProperty], + [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.HorizontalScrollBarVisibilityProperty], + [Grid.RowProperty] = 1, + }.RegisterInNameScope(scope), + new ScrollBar + { + Name = "verticalScrollBar", + Orientation = Orientation.Vertical, + [~RangeBase.MaximumProperty] = control[~ScrollViewer.VerticalScrollBarMaximumProperty], + [~~RangeBase.ValueProperty] = control[~~ScrollViewer.VerticalScrollBarValueProperty], + [~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.VerticalScrollBarViewportSizeProperty], + [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.VerticalScrollBarVisibilityProperty], + [Grid.ColumnProperty] = 1, + }.RegisterInNameScope(scope), + }, + }); + } + + private void AssertArePixelEqual(Rect expected, Rect actual) + { + var expectedRounded = new Rect((int)expected.X, (int)expected.Y, (int)expected.Width, (int)expected.Height); + var actualRounded = new Rect((int)actual.X, (int)actual.Y, (int)actual.Width, (int)actual.Height); + Assert.Equal(expectedRounded, actualRounded); + } + + private class TestCanvas : Canvas + { + public int MeasureCount { get; private set; } + public int ArrangeCount { get; private set; } + + protected override Size MeasureOverride(Size availableSize) + { + ++MeasureCount; + return base.MeasureOverride(availableSize); + } + + protected override Size ArrangeOverride(Size finalSize) + { + ++ArrangeCount; + return base.ArrangeOverride(finalSize); + } + } + + private static class RunOnUIThread + { + public static async Task Execute(Func func) + { + await func(); + } + } + } +} diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 9bb9fd7145..00ef503b8d 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -44,7 +44,7 @@ namespace Avalonia.LeakTests window.Show(); // Do a layout and make sure that Canvas gets added to visual tree. - window.LayoutManager.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Presenter.Child); // Clear the content and ensure the Canvas is removed. @@ -82,7 +82,7 @@ namespace Avalonia.LeakTests window.Show(); // Do a layout and make sure that Canvas gets added to visual tree. - window.LayoutManager.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Find("foo")); Assert.IsType(window.Presenter.Child); @@ -122,7 +122,7 @@ namespace Avalonia.LeakTests // Do a layout and make sure that ScrollViewer gets added to visual tree and its // template applied. - window.LayoutManager.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Presenter.Child); Assert.IsType(((ScrollViewer)window.Presenter.Child).Presenter.Child); @@ -159,7 +159,7 @@ namespace Avalonia.LeakTests // Do a layout and make sure that TextBox gets added to visual tree and its // template applied. - window.LayoutManager.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Presenter.Child); Assert.NotEmpty(window.Presenter.Child.GetVisualChildren()); @@ -203,7 +203,7 @@ namespace Avalonia.LeakTests // Do a layout and make sure that TextBox gets added to visual tree and its // Text property set. - window.LayoutManager.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Presenter.Child); Assert.Equal("foo", ((TextBox)window.Presenter.Child).Text); @@ -241,7 +241,7 @@ namespace Avalonia.LeakTests // Do a layout and make sure that TextBox gets added to visual tree and its // template applied. - window.LayoutManager.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(); Assert.Same(textBox, window.Presenter.Child); // Get the border from the TextBox template. @@ -295,7 +295,7 @@ namespace Avalonia.LeakTests window.Show(); // Do a layout and make sure that TreeViewItems get realized. - window.LayoutManager.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(); Assert.Single(target.ItemContainerGenerator.Containers); // Clear the content and ensure the TreeView is removed. @@ -329,7 +329,7 @@ namespace Avalonia.LeakTests window.Show(); // Do a layout and make sure that Slider gets added to visual tree. - window.LayoutManager.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Presenter.Child); // Clear the content and ensure the Slider is removed. @@ -403,7 +403,7 @@ namespace Avalonia.LeakTests // Do a layout and make sure that Canvas gets added to visual tree with // its render transform. - window.LayoutManager.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(); var canvas = Assert.IsType(window.Presenter.Child); Assert.IsType(canvas.RenderTransform); @@ -451,6 +451,7 @@ namespace Avalonia.LeakTests AttachShowAndDetachContextMenu(window); + Mock.Get(window.PlatformImpl).ResetCalls(); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); dotMemory.Check(memory => @@ -486,6 +487,7 @@ namespace Avalonia.LeakTests BuildAndShowContextMenu(window); BuildAndShowContextMenu(window); + Mock.Get(window.PlatformImpl).ResetCalls(); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); dotMemory.Check(memory => @@ -512,7 +514,7 @@ namespace Avalonia.LeakTests window.Show(); - window.LayoutManager.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Presenter.Child); window.Content = null; diff --git a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs index 5773f5e6b0..64f5eb2a0e 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs @@ -113,6 +113,50 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("(null)", target.Text); } + [Fact] + public void Should_Pass_UnsetValue_To_Converter_For_Broken_Binding() + { + var source = new { A = 1, B = 2, C = 3 }; + var target = new TextBlock { DataContext = source }; + + var binding = new MultiBinding + { + Converter = new ConcatConverter(), + Bindings = new[] + { + new Binding { Path = "A" }, + new Binding { Path = "B" }, + new Binding { Path = "Missing" }, + }, + }; + + target.Bind(TextBlock.TextProperty, binding); + + Assert.Equal("1,2,(unset)", target.Text); + } + + [Fact] + public void Should_Pass_FallbackValue_To_Converter_For_Broken_Binding() + { + var source = new { A = 1, B = 2, C = 3 }; + var target = new TextBlock { DataContext = source }; + + var binding = new MultiBinding + { + Converter = new ConcatConverter(), + Bindings = new[] + { + new Binding { Path = "A" }, + new Binding { Path = "B" }, + new Binding { Path = "Missing", FallbackValue = "Fallback" }, + }, + }; + + target.Bind(TextBlock.TextProperty, binding); + + Assert.Equal("1,2,Fallback", target.Text); + } + private class ConcatConverter : IMultiValueConverter { public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs index 757cd628b7..0be3bbbb9f 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.ComponentModel; using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Data; @@ -89,6 +91,69 @@ namespace Avalonia.Markup.UnitTests.Parsers GC.KeepAlive(data); } + [Fact] + public async Task Should_Negate_BindingNotification_Value() + { + var data = new { Foo = true }; + var target = ExpressionObserverBuilder.Build(data, "!Foo", enableDataValidation: true); + var result = await target.Take(1); + + Assert.Equal(new BindingNotification(false), result); + + GC.KeepAlive(data); + } + + [Fact] + public async Task Should_Pass_Through_BindingNotification_Error() + { + var data = new { }; + var target = ExpressionObserverBuilder.Build(data, "!Foo", enableDataValidation: true); + var result = await target.Take(1); + + Assert.Equal( + new BindingNotification( + new MissingMemberException("Could not find a matching property accessor for 'Foo' on '{ }'"), + BindingErrorType.Error), + result); + + GC.KeepAlive(data); + } + + [Fact] + public async Task Should_Negate_BindingNotification_Error_FallbackValue() + { + var data = new Test { DataValidationError = "Test error" }; + var target = ExpressionObserverBuilder.Build(data, "!Foo", enableDataValidation: true); + var result = await target.Take(1); + + Assert.Equal( + new BindingNotification( + new DataValidationException("Test error"), + BindingErrorType.DataValidationError, + true), + result); + + GC.KeepAlive(data); + } + + [Fact] + public async Task Should_Add_Error_To_BindingNotification_For_FallbackValue_Not_Convertible_To_Boolean() + { + var data = new Test { Bar = new object(), DataValidationError = "Test error" }; + var target = ExpressionObserverBuilder.Build(data, "!Bar", enableDataValidation: true); + var result = await target.Take(1); + + Assert.Equal( + new BindingNotification( + new AggregateException( + new DataValidationException("Test error"), + new InvalidCastException($"Unable to convert 'System.Object' to bool.")), + BindingErrorType.Error), + result); + + GC.KeepAlive(data); + } + [Fact] public void SetValue_Should_Return_False_For_Invalid_Value() { @@ -101,9 +166,20 @@ namespace Avalonia.Markup.UnitTests.Parsers GC.KeepAlive(data); } - private class Test + private class Test : INotifyDataErrorInfo { public bool Foo { get; set; } + public object Bar { get; set; } + + public string DataValidationError { get; set; } + public bool HasErrors => !string.IsNullOrWhiteSpace(DataValidationError); + + public event EventHandler ErrorsChanged; + + public IEnumerable GetErrors(string propertyName) + { + return DataValidationError is object ? new[] { DataValidationError } : null; + } } } } diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs index 4f7264f2f2..8cf74c36f4 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs @@ -22,7 +22,7 @@ namespace Avalonia.Markup.UnitTests.Parsers Assert.Equal( new BindingNotification( - new MissingMemberException("Could not find CLR property 'Baz' on '1'"), BindingErrorType.Error), + new MissingMemberException("Could not find a matching property accessor for 'Baz' on '1'"), BindingErrorType.Error), result); GC.KeepAlive(data); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs index 43c7525939..6730e3134d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs @@ -1,6 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; using Avalonia.Controls; +using Avalonia.Controls.Presenters; using Avalonia.Data; +using Avalonia.Data.Converters; using Avalonia.UnitTests; +using Avalonia.VisualTree; using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Data @@ -54,5 +61,51 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data Assert.Equal("bar", textBlock.Text); } } + + [Fact] + public void MultiBinding_TemplatedParent_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBox = window.FindControl("textBox"); + + window.ApplyTemplate(); + textBox.ApplyTemplate(); + + var target = (TextPresenter)textBox.GetVisualChildren().Single(); + Assert.Equal("Foo,Bar", target.Text); + } + } + } + + public class ConcatConverter : IMultiValueConverter + { + public static ConcatConverter Instance { get; } = new ConcatConverter(); + + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + return string.Join(",", values); + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs new file mode 100644 index 0000000000..37aa123cfc --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -0,0 +1,621 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Reactive.Subjects; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Data.Core; +using Avalonia.Markup.Data; +using Avalonia.UnitTests; +using XamlX; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions +{ + public class CompiledBindingExtensionTests + { + [Fact] + public void ResolvesClrPropertyBasedOnDataContextType() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } + + [Fact] + public void ResolvesPathPassedByProperty() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } + + [Fact] + public void ResolvesStreamTaskBindingCorrectly() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + TaskProperty = Task.FromResult("foobar") + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.TaskProperty.Result, textBlock.Text); + } + } + + [Fact] + public void ResolvesStreamObservableBindingCorrectly() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + DelayedBinding.ApplyBindings(textBlock); + + var subject = new Subject(); + var dataContext = new TestDataContext + { + ObservableProperty = subject + }; + + window.DataContext = dataContext; + + subject.OnNext("foobar"); + + Assert.Equal("foobar", textBlock.Text); + } + } + + [Fact] + public void ResolvesIndexerBindingCorrectly() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + ListProperty = { "A", "B", "C", "D", "E" } + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.ListProperty[3], textBlock.Text); + } + } + + [Fact] + public void ResolvesArrayIndexerBindingCorrectly() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + ArrayProperty = new[] { "A", "B", "C", "D", "E" } + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.ArrayProperty[3], textBlock.Text); + } + } + + [Fact] + public void ResolvesObservableIndexerBindingCorrectly() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + ObservableCollectionProperty = { "A", "B", "C", "D", "E" } + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.ObservableCollectionProperty[3], textBlock.Text); + + dataContext.ObservableCollectionProperty[3] = "New Value"; + + Assert.Equal(dataContext.ObservableCollectionProperty[3], textBlock.Text); + } + } + + [Fact] + public void InfersCompiledBindingDataContextFromDataContextBinding() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + var dataContext = new TestDataContext + { + StringProperty = "A" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } + + [Fact] + public void ResolvesNonIntegerIndexerBindingCorrectly() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext(); + + dataContext.NonIntegerIndexerProperty["Test"] = "Initial Value"; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.NonIntegerIndexerProperty["Test"], textBlock.Text); + + dataContext.NonIntegerIndexerProperty["Test"] = "New Value"; + + Assert.Equal(dataContext.NonIntegerIndexerProperty["Test"], textBlock.Text); + } + } + + [Fact] + public void InfersDataTemplateTypeFromDataTypeProperty() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var target = window.FindControl("target"); + + var dataContext = new TestDataContext(); + + dataContext.StringProperty = "Initial Value"; + + window.DataContext = dataContext; + + window.ApplyTemplate(); + target.ApplyTemplate(); + ((ContentPresenter)target.Presenter).UpdateChild(); + + Assert.Equal(dataContext.StringProperty, ((TextBlock)target.Presenter.Child).Text); + } + } + + + [Fact] + public void ThrowsOnUninferrableLooseDataTemplateNoDataTypeWithCompiledBindingPath() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + Assert.Throws(() => loader.Load(xaml)); + } + } + + [Fact] + public void ThrowsOnUninferrableDataTypeFromNonCompiledDataContextBindingWithCompiledBindingPath() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + +"; + var loader = new AvaloniaXamlLoader(); + Assert.Throws(() => loader.Load(xaml)); + } + } + + [Fact] + public void InfersDataTemplateTypeFromParentCollectionItemsType() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var target = window.FindControl("target"); + + var dataContext = new TestDataContext(); + + dataContext.ListProperty.Add("Test"); + + window.DataContext = dataContext; + + window.ApplyTemplate(); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + Assert.Equal(dataContext.ListProperty[0], (string)((ContentPresenter)target.Presenter.Panel.Children[0]).Content); + } + } + + [Fact] + public void ThrowsOnUninferrableDataTemplateInItemsControlWithoutItemsBinding() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + Assert.Throws(() => loader.Load(xaml)); + } + } + + [Fact] + public void ResolvesElementNameBinding() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("text2"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } + + [Fact] + public void ResolvesElementNameBindingFromLongForm() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("text2"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } + + [Fact] + public void ResolvesRelativeSourceBindingLongForm() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var target = window.FindControl("text"); + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + target.ApplyTemplate(); + + Assert.Equal("test", target.Text); + } + } + + [Fact] + public void ResolvesSourceBindingLongForm() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var target = window.FindControl("text"); + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + target.ApplyTemplate(); + + Assert.Equal("Test".Length.ToString(), target.Text); + } + } + + [Fact] + public void CompilesBindingWhenRequested() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } + + [Fact] + public void ThrowsOnInvalidBindingPathOnCompiledBindingEnabledViaDirective() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + Assert.Throws(() => loader.Load(xaml)); + } + } + + [Fact] + public void ThrowsOnInvalidCompileBindingsDirective() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + +"; + var loader = new AvaloniaXamlLoader(); + Assert.Throws(() => loader.Load(xaml)); + } + } + } + + public class TestDataContext + { + public string StringProperty { get; set; } + + public Task TaskProperty { get; set; } + + public IObservable ObservableProperty { get; set; } + + public ObservableCollection ObservableCollectionProperty { get; set; } = new ObservableCollection(); + + public string[] ArrayProperty { get; set; } + + public List ListProperty { get; set; } = new List(); + + public NonIntegerIndexer NonIntegerIndexerProperty { get; set; } = new NonIntegerIndexer(); + + public class NonIntegerIndexer : NotifyingBase + { + private readonly Dictionary _storage = new Dictionary(); + + public string this[string key] + { + get + { + return _storage[key]; + } + set + { + _storage[key] = value; + RaisePropertyChanged(CommonPropertyNames.IndexerName); + } + } + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs index ed3e4c986b..e651c5f29c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -797,6 +797,27 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void Automatically_Converts_Color_To_SolidColorBrush() + { + var xaml = @" + + + #ff506070 + + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + var brush = (ISolidColorBrush)border.Background; + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + private IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With( diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs index 7ab6c2de40..1514d5bd95 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Media; using Avalonia.UnitTests; @@ -44,6 +45,43 @@ namespace Avalonia.Markup.Xaml.UnitTests.MakrupExtensions } } + [Fact] + public void Missing_ResourceKey_In_ResourceInclude_Does_Not_Cause_StackOverflow() + { + var styleXaml = @" + + +"; + + using (StartWithResources(("test:style.xaml", styleXaml))) + { + var xaml = @" + + + + + + + + +"; + + var loader = new AvaloniaXamlLoader(); + var app = Application.Current; + + try + { + loader.Load(xaml, null, app); + } + catch (KeyNotFoundException) + { + + } + } + } + private IDisposable StartWithResources(params (string, string)[] assets) { var assetLoader = new MockAssetLoader(assets); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs index ddfa96c094..efcda47f0f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs @@ -511,6 +511,27 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal(0xff506070, brush.Color.ToUint32()); } + [Fact] + public void Automatically_Converts_Color_To_SolidColorBrush() + { + var xaml = @" + + + #ff506070 + + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + var brush = (ISolidColorBrush)border.Background; + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + private IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With( diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/StyleIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/StyleIncludeTests.cs new file mode 100644 index 0000000000..dca936d229 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/StyleIncludeTests.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests +{ + public class StyleIncludeTests : XamlTestBase + { + [Fact] + public void Missing_ResourceKey_In_StyleInclude_Does_Not_Cause_StackOverflow() + { + var styleXaml = @" +"; + + using (StartWithResources(("test:style.xaml", styleXaml))) + { + var xaml = @" + + + + +"; + + var loader = new AvaloniaXamlLoader(); + var app = Application.Current; + + try + { + loader.Load(xaml, null, app); + } + catch (KeyNotFoundException) + { + + } + } + } + + private IDisposable StartWithResources(params (string, string)[] assets) + { + var assetLoader = new MockAssetLoader(assets); + var services = new TestServices(assetLoader: assetLoader); + return UnitTestApplication.Start(services); + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 9642f5719d..184bc3bae9 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -382,5 +382,59 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(Border.WidthProperty, border.Transitions[0].Property); } } + + [Fact] + public void Style_Can_Use_Class_Selector_With_Dash() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var foo = window.FindControl("foo"); + + Assert.Equal(Colors.Red, ((ISolidColorBrush)foo.Background).Color); + } + } + + [Fact] + public void Style_Can_Use_Pseudolass_Selector_With_Dash() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var foo = window.FindControl("foo"); + + Assert.Null(foo.Background); + + ((IPseudoClasses)foo.Classes).Add(":foo-bar"); + + Assert.Equal(Colors.Red, ((ISolidColorBrush)foo.Background).Color); + } + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index a408069cb0..ddeb558754 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -6,7 +6,11 @@ using System.Linq; using System.Runtime.CompilerServices; using Avalonia.Controls; using Avalonia.Data.Converters; +using Avalonia.Data.Core; +using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Media; +using Avalonia.Styling; using Avalonia.Threading; using Avalonia.UnitTests; using Avalonia.VisualTree; @@ -234,6 +238,30 @@ namespace Avalonia.Markup.Xaml.UnitTests } } + [Fact] + public void Provide_Value_Target_Should_Provide_Clr_Property_Info() + { + var parsed = AvaloniaXamlLoader.Parse(@" +", typeof(XamlIlClassWithClrPropertyWithValue).Assembly); + Assert.Equal(6, parsed.Count); + } + + [Fact] + public void DataContextType_Resolution() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parsed = AvaloniaXamlLoader.Parse(@" +"); + } + } + [Fact] public void DataTemplates_Should_Resolve_Named_Controls_From_Parent_Scope() { @@ -345,4 +373,22 @@ namespace Avalonia.Markup.Xaml.UnitTests return (int)control.GetValue(TestIntProperty); } } + + public class XamlIlCheckClrPropertyInfoExtension + { + public string ExpectedPropertyName { get; set; } + + public object ProvideValue(IServiceProvider prov) + { + var pvt = prov.GetService(); + var info = (ClrPropertyInfo)pvt.TargetProperty; + var v = (int)info.Get(pvt.TargetObject); + return v + 1; + } + } + + public class XamlIlClassWithClrPropertyWithValue + { + public int Count { get; set; }= 5; + } } diff --git a/tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs similarity index 98% rename from tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs rename to tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index 8d64190ebd..feed1179ef 100644 --- a/tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -6,7 +6,7 @@ using Avalonia.Media.Fonts; using Avalonia.Platform; using SkiaSharp; -namespace Avalonia.Skia.UnitTests +namespace Avalonia.Skia.UnitTests.Media { public class CustomFontManagerImpl : IFontManagerImpl { diff --git a/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs similarity index 95% rename from tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs rename to tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs index 8f80d89ac6..df286d709e 100644 --- a/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs @@ -1,13 +1,11 @@ using System; using System.Linq; -using System.Reflection; using Avalonia.Media; -using Avalonia.Platform; using Avalonia.UnitTests; using SkiaSharp; using Xunit; -namespace Avalonia.Skia.UnitTests +namespace Avalonia.Skia.UnitTests.Media { public class FontManagerImplTests { @@ -39,7 +37,7 @@ namespace Avalonia.Skia.UnitTests string fontName = fontManager.GetInstalledFontFamilyNames().First(); var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(new FontFamily($"A, B, {fontName}"), FontWeight.Bold)); + new Typeface(new FontFamily($"A, B, {fontName}"), weight: FontWeight.Bold)); var skTypeface = glyphTypeface.Typeface; @@ -88,7 +86,7 @@ namespace Avalonia.Skia.UnitTests var fontManager = new FontManagerImpl(); var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(s_fontUri, FontWeight.Black, FontStyle.Italic)); + new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black)); var skTypeface = glyphTypeface.Typeface; diff --git a/tests/Avalonia.Skia.UnitTests/SKTypefaceCollectionCacheTests.cs b/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs similarity index 89% rename from tests/Avalonia.Skia.UnitTests/SKTypefaceCollectionCacheTests.cs rename to tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs index 726052351b..f9f924e782 100644 --- a/tests/Avalonia.Skia.UnitTests/SKTypefaceCollectionCacheTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs @@ -2,7 +2,7 @@ using Avalonia.UnitTests; using Xunit; -namespace Avalonia.Skia.UnitTests +namespace Avalonia.Skia.UnitTests.Media { public class SKTypefaceCollectionCacheTests { @@ -19,7 +19,7 @@ namespace Avalonia.Skia.UnitTests var notoMonoCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(notoMono); - var typeface = new Typeface("ABC", FontWeight.Bold, FontStyle.Italic); + var typeface = new Typeface("ABC", FontStyle.Italic, FontWeight.Bold); Assert.Equal("Noto Mono", notoMonoCollection.Get(typeface).FamilyName); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattableTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattableTextSource.cs new file mode 100644 index 0000000000..6a5065939e --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattableTextSource.cs @@ -0,0 +1,38 @@ +using System; +using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; + +namespace Avalonia.Skia.UnitTests.Media.TextFormatting +{ + internal class FormattableTextSource : ITextSource + { + private readonly ReadOnlySlice _text; + private readonly TextRunProperties _defaultStyle; + private ReadOnlySlice> _styleSpans; + + public FormattableTextSource(string text, TextRunProperties defaultStyle, + ReadOnlySlice> styleSpans) + { + _text = text.AsMemory(); + + _defaultStyle = defaultStyle; + + _styleSpans = styleSpans; + } + + public TextRun GetTextRun(int textSourceIndex) + { + if (_styleSpans.IsEmpty) + { + return new TextEndOfParagraph(); + } + + var currentSpan = _styleSpans[0]; + + _styleSpans = _styleSpans.Skip(1); + + return new TextCharacters(_text.AsSlice(currentSpan.Start, currentSpan.Length), + _defaultStyle); + } + } +} diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs new file mode 100644 index 0000000000..40aa862906 --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs @@ -0,0 +1,36 @@ +using System; +using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; + +namespace Avalonia.Skia.UnitTests.Media.TextFormatting +{ + internal class MultiBufferTextSource : ITextSource + { + private readonly string[] _runTexts; + private readonly GenericTextRunProperties _defaultStyle; + + public MultiBufferTextSource(GenericTextRunProperties defaultStyle) + { + _defaultStyle = defaultStyle; + + _runTexts = new[] { "A123456789", "B123456789", "C123456789", "D123456789", "E123456789" }; + } + + public static TextRange TextRange => new TextRange(0, 50); + + public TextRun GetTextRun(int textSourceIndex) + { + if (textSourceIndex == 50) + { + return new TextEndOfParagraph(); + } + + var index = textSourceIndex / 10; + + var runText = _runTexts[index]; + + return new TextCharacters( + new ReadOnlySlice(runText.AsMemory(), textSourceIndex, runText.Length), _defaultStyle); + } + } +} diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs new file mode 100644 index 0000000000..045deacd0b --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs @@ -0,0 +1,30 @@ +using System; +using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; + +namespace Avalonia.Skia.UnitTests.Media.TextFormatting +{ + internal class SingleBufferTextSource : ITextSource + { + private readonly ReadOnlySlice _text; + private readonly GenericTextRunProperties _defaultGenericPropertiesRunProperties; + + public SingleBufferTextSource(string text, GenericTextRunProperties defaultProperties) + { + _text = text.AsMemory(); + _defaultGenericPropertiesRunProperties = defaultProperties; + } + + public TextRun GetTextRun(int textSourceIndex) + { + var runText = _text.Skip(textSourceIndex); + + if (runText.IsEmpty) + { + return new TextEndOfParagraph(); + } + + return new TextCharacters(runText, _defaultGenericPropertiesRunProperties); + } + } +} diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs new file mode 100644 index 0000000000..697cc4fec7 --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.UnitTests; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Skia.UnitTests.Media.TextFormatting +{ + public class TextFormatterTests + { + [Fact] + public void Should_Format_TextRuns_With_Default_Style() + { + using (Start()) + { + const string text = "0123456789"; + + var defaultProperties = + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.Single(textLine.TextRuns); + + var textRun = textLine.TextRuns[0]; + + Assert.Equal(defaultProperties.Typeface, textRun.Properties.Typeface); + + Assert.Equal(defaultProperties.ForegroundBrush, textRun.Properties.ForegroundBrush); + + Assert.Equal(text.Length, textRun.Text.Length); + } + } + + [Fact] + public void Should_Format_TextRuns_With_Multiple_Buffers() + { + using (Start()) + { + var defaultProperties = + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black); + + var textSource = new MultiBufferTextSource(defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.Equal(5, textLine.TextRuns.Count); + + Assert.Equal(50, textLine.TextRange.Length); + } + } + + [Fact] + public void Should_Format_TextRuns_With_TextRunStyles() + { + using (Start()) + { + const string text = "0123456789"; + + var defaultProperties = + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black); + + var GenericTextRunPropertiesRuns = new[] + { + new ValueSpan(0, 3, defaultProperties), + new ValueSpan(3, 3, + new GenericTextRunProperties(Typeface.Default, 13, foregroundBrush: Brushes.Black)), + new ValueSpan(6, 3, + new GenericTextRunProperties(Typeface.Default, 14, foregroundBrush: Brushes.Black)), + new ValueSpan(9, 1, defaultProperties) + }; + + var textSource = new FormattableTextSource(text, defaultProperties, GenericTextRunPropertiesRuns); + + var formatter = new TextFormatterImpl(); + + var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.Equal(text.Length, textLine.TextRange.Length); + + for (var i = 0; i < GenericTextRunPropertiesRuns.Length; i++) + { + var GenericTextRunPropertiesRun = GenericTextRunPropertiesRuns[i]; + + var textRun = textLine.TextRuns[i]; + + Assert.Equal(GenericTextRunPropertiesRun.Length, textRun.Text.Length); + } + } + } + + [Theory] + [InlineData("0123", 1)] + [InlineData("\r\n", 1)] + [InlineData("👍b", 2)] + [InlineData("a👍b", 3)] + [InlineData("a👍子b", 4)] + public void Should_Produce_Unique_Runs(string text, int numberOfRuns) + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.Equal(numberOfRuns, textLine.TextRuns.Count); + } + } + + [Fact] + public void Should_Split_Run_On_Script() + { + using (Start()) + { + const string text = "1234الدولي"; + + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.Equal(4, textLine.TextRuns[0].Text.Length); + } + } + + [InlineData("𐐷𐐷𐐷𐐷𐐷", 10, 1)] + [InlineData("01234 56789 01234 56789", 6, 4)] + [Theory] + public void Should_Wrap_With_Overflow(string text, int expectedCharactersPerLine, int expectedNumberOfLines) + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var numberOfLines = 0; + + var currentPosition = 0; + + while (currentPosition < text.Length) + { + var textLine = + formatter.FormatLine(textSource, currentPosition, 1, + new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.WrapWithOverflow)); + + if (text.Length - currentPosition > expectedCharactersPerLine) + { + Assert.Equal(expectedCharactersPerLine, textLine.TextRange.Length); + } + + currentPosition += textLine.TextRange.Length; + + numberOfLines++; + } + + Assert.Equal(expectedNumberOfLines, numberOfLines); + } + } + + [InlineData("Whether to turn off HTTPS. This option only applies if Individual, " + + "IndividualB2C, SingleOrg, or MultiOrg aren't used for ‑‑auth." + , "Noto Sans", 40)] + [InlineData("01234 56789 01234 56789", "Noto Mono", 7)] + [Theory] + public void Should_Wrap(string text, string familyName, int numberOfCharactersPerLine) + { + using (Start()) + { + var lineBreaker = new LineBreakEnumerator(text.AsMemory()); + + var expected = new List(); + + while (lineBreaker.MoveNext()) + { + expected.Add(lineBreaker.Current.PositionWrap - 1); + } + + var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#" + + familyName); + + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var glyph = typeface.GlyphTypeface.GetGlyph('a'); + + var advance = typeface.GlyphTypeface.GetGlyphAdvance(glyph) * + (12.0 / typeface.GlyphTypeface.DesignEmHeight); + + var paragraphWidth = advance * numberOfCharactersPerLine; + + var currentPosition = 0; + + while (currentPosition < text.Length) + { + var textLine = + formatter.FormatLine(textSource, currentPosition, paragraphWidth, + new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap)); + + Assert.True(expected.Contains(textLine.TextRange.End)); + + var index = expected.IndexOf(textLine.TextRange.End); + + for (var i = 0; i <= index; i++) + { + expected.RemoveAt(0); + } + + currentPosition += textLine.TextRange.Length; + } + } + } + + [Fact] + public void Should_Produce_Fixed_Height_Lines() + { + using (Start()) + { + const string text = "012345"; + + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties, lineHeight: 50)); + + Assert.Equal(50, textLine.LineMetrics.Size.Height); + } + } + + public static IDisposable Start() + { + var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface + .With(renderInterface: new PlatformRenderInterface(null), + textShaperImpl: new TextShaperImpl())); + + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(new FontManager(new CustomFontManagerImpl())); + + return disposable; + } + } +} diff --git a/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs similarity index 74% rename from tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs rename to tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 0d9fd31e52..5d9aa2cf97 100644 --- a/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -4,15 +4,33 @@ using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.UnitTests; +using Avalonia.Utilities; using Xunit; -namespace Avalonia.Skia.UnitTests +namespace Avalonia.Skia.UnitTests.Media.TextFormatting { public class TextLayoutTests { private static readonly string s_singleLineText = "0123456789"; private static readonly string s_multiLineText = "012345678\r\r0123456789"; + [InlineData("01234\r01234\r", 3)] + [InlineData("01234\r01234", 2)] + [Theory] + public void Should_Break_Lines(string text, int numberOfLines) + { + using (Start()) + { + var layout = new TextLayout( + text, + Typeface.Default, + 12.0f, + Brushes.Black); + + Assert.Equal(numberOfLines, layout.TextLines.Count); + } + } + [Fact] public void Should_Apply_TextStyleSpan_To_Text_In_Between() { @@ -22,17 +40,16 @@ namespace Avalonia.Skia.UnitTests var spans = new[] { - new TextStyleRun( - new TextPointer(1, 2), - new TextStyle(Typeface.Default, 12, foreground)) + new ValueSpan(1, 2, + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)) }; var layout = new TextLayout( s_multiLineText, - Typeface.Default, + Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textStyleOverrides : spans); + textStyleOverrides: spans); var textLine = layout.TextLines[0]; @@ -46,7 +63,7 @@ namespace Avalonia.Skia.UnitTests Assert.Equal("12", actual); - Assert.Equal(foreground, textRun.Style.Foreground); + Assert.Equal(foreground, textRun.Properties.ForegroundBrush); } } @@ -61,9 +78,8 @@ namespace Avalonia.Skia.UnitTests { var spans = new[] { - new TextStyleRun( - new TextPointer(0, i), - new TextStyle(Typeface.Default, 12, foreground)) + new ValueSpan(0, i, + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)) }; var expected = new TextLayout( @@ -72,22 +88,22 @@ namespace Avalonia.Skia.UnitTests 12.0f, Brushes.Black.ToImmutable(), textWrapping: TextWrapping.Wrap, - maxWidth : 25); + maxWidth: 25); var actual = new TextLayout( s_multiLineText, Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textWrapping : TextWrapping.Wrap, - maxWidth : 25, - textStyleOverrides : spans); + textWrapping: TextWrapping.Wrap, + maxWidth: 25, + textStyleOverrides: spans); Assert.Equal(expected.TextLines.Count, actual.TextLines.Count); for (var j = 0; j < actual.TextLines.Count; j++) { - Assert.Equal(expected.TextLines[j].Text.Length, actual.TextLines[j].Text.Length); + Assert.Equal(expected.TextLines[j].TextRange.Length, actual.TextLines[j].TextRange.Length); Assert.Equal(expected.TextLines[j].TextRuns.Sum(x => x.Text.Length), actual.TextLines[j].TextRuns.Sum(x => x.Text.Length)); @@ -105,9 +121,8 @@ namespace Avalonia.Skia.UnitTests var spans = new[] { - new TextStyleRun( - new TextPointer(0, 2), - new TextStyle(Typeface.Default, 12, foreground)) + new ValueSpan(0, 2, + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)) }; var layout = new TextLayout( @@ -115,7 +130,7 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textStyleOverrides : spans); + textStyleOverrides: spans); var textLine = layout.TextLines[0]; @@ -130,7 +145,7 @@ namespace Avalonia.Skia.UnitTests Assert.Equal("01", actual); - Assert.Equal(foreground, textRun.Style.Foreground); + Assert.Equal(foreground, textRun.Properties.ForegroundBrush); } } @@ -143,9 +158,8 @@ namespace Avalonia.Skia.UnitTests var spans = new[] { - new TextStyleRun( - new TextPointer(8, 2), - new TextStyle(Typeface.Default, 12, foreground)) + new ValueSpan(8, 2, + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)), }; var layout = new TextLayout( @@ -153,7 +167,7 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textStyleOverrides : spans); + textStyleOverrides: spans); var textLine = layout.TextLines[0]; @@ -167,7 +181,7 @@ namespace Avalonia.Skia.UnitTests Assert.Equal("89", actual); - Assert.Equal(foreground, textRun.Style.Foreground); + Assert.Equal(foreground, textRun.Properties.ForegroundBrush); } } @@ -180,9 +194,8 @@ namespace Avalonia.Skia.UnitTests var spans = new[] { - new TextStyleRun( - new TextPointer(0, 1), - new TextStyle(Typeface.Default, 12, foreground)) + new ValueSpan(0, 1, + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)) }; var layout = new TextLayout( @@ -190,7 +203,7 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textStyleOverrides : spans); + textStyleOverrides: spans); var textLine = layout.TextLines[0]; @@ -200,7 +213,7 @@ namespace Avalonia.Skia.UnitTests Assert.Equal(1, textRun.Text.Length); - Assert.Equal(foreground, textRun.Style.Foreground); + Assert.Equal(foreground, textRun.Properties.ForegroundBrush); } } @@ -215,9 +228,8 @@ namespace Avalonia.Skia.UnitTests var spans = new[] { - new TextStyleRun( - new TextPointer(2, 2), - new TextStyle(Typeface.Default, 12, foreground)) + new ValueSpan(2, 2, + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)) }; var layout = new TextLayout( @@ -239,7 +251,7 @@ namespace Avalonia.Skia.UnitTests Assert.Equal("😄", actual); - Assert.Equal(foreground, textRun.Style.Foreground); + Assert.Equal(foreground, textRun.Properties.ForegroundBrush); } } @@ -254,7 +266,7 @@ namespace Avalonia.Skia.UnitTests 12.0f, Brushes.Black.ToImmutable()); - Assert.Equal(s_multiLineText.Length, layout.TextLines.Sum(x => x.Text.Length)); + Assert.Equal(s_multiLineText.Length, layout.TextLines.Sum(x => x.TextRange.Length)); } } @@ -291,9 +303,8 @@ namespace Avalonia.Skia.UnitTests var spans = new[] { - new TextStyleRun( - new TextPointer(0, 24), - new TextStyle(Typeface.Default, 12, foreground)) + new ValueSpan(0, 24, + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)) }; var layout = new TextLayout( @@ -301,8 +312,8 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textWrapping : TextWrapping.Wrap, - maxWidth : 180, + textWrapping: TextWrapping.Wrap, + maxWidth: 180, textStyleOverrides: spans); Assert.Equal( @@ -322,9 +333,8 @@ namespace Avalonia.Skia.UnitTests var spans = new[] { - new TextStyleRun( - new TextPointer(5, 20), - new TextStyle(Typeface.Default, 12, foreground)) + new ValueSpan(5, 20, + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)) }; var layout = new TextLayout( @@ -332,13 +342,13 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - maxWidth : 200, - maxHeight : 125, + maxWidth: 200, + maxHeight: 125, textStyleOverrides: spans); - Assert.Equal(foreground, layout.TextLines[0].TextRuns[1].Style.Foreground); - Assert.Equal(foreground, layout.TextLines[1].TextRuns[0].Style.Foreground); - Assert.Equal(foreground, layout.TextLines[2].TextRuns[0].Style.Foreground); + Assert.Equal(foreground, layout.TextLines[0].TextRuns[1].Properties.ForegroundBrush); + Assert.Equal(foreground, layout.TextLines[1].TextRuns[0].Properties.ForegroundBrush); + Assert.Equal(foreground, layout.TextLines[2].TextRuns[0].Properties.ForegroundBrush); } } @@ -355,7 +365,7 @@ namespace Avalonia.Skia.UnitTests 12.0f, Brushes.Black.ToImmutable()); - var shapedRun = (ShapedTextRun)layout.TextLines[0].TextRuns[0]; + var shapedRun = (ShapedTextCharacters)layout.TextLines[0].TextRuns[0]; var glyphRun = shapedRun.GlyphRun; @@ -390,7 +400,7 @@ namespace Avalonia.Skia.UnitTests foreach (var textRun in textLine.TextRuns) { - var shapedRun = (ShapedTextRun)textRun; + var shapedRun = (ShapedTextCharacters)textRun; var glyphRun = shapedRun.GlyphRun; @@ -426,13 +436,13 @@ namespace Avalonia.Skia.UnitTests Assert.Equal(1, layout.TextLines[0].TextRuns.Count); - Assert.Equal(expectedLength, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Length); + Assert.Equal(expectedLength, ((ShapedTextCharacters)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Length); - Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[5]); + Assert.Equal(5, ((ShapedTextCharacters)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[5]); - if(expectedLength == 7) + if (expectedLength == 7) { - Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[6]); + Assert.Equal(5, ((ShapedTextCharacters)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[6]); } } } @@ -467,7 +477,7 @@ namespace Avalonia.Skia.UnitTests var textLine = layout.TextLines[0]; - var textRun = (ShapedTextRun)textLine.TextRuns[0]; + var textRun = (ShapedTextCharacters)textLine.TextRuns[0]; Assert.Equal(7, textRun.Text.Length); @@ -506,9 +516,48 @@ namespace Avalonia.Skia.UnitTests } } + [InlineData("0123456789\r\n0123456789\r\n0123456789", 0, 3)] + [InlineData("0123456789\r\n0123456789\r\n0123456789", 1, 1)] + [InlineData("0123456789\r\n0123456789\r\n0123456789", 4, 3)] + [Theory] + public void Should_Not_Exceed_MaxLines(string text, int maxLines, int expectedLines) + { + using (Start()) + { + var layout = new TextLayout( + text, + Typeface.Default, + 12, + Brushes.Black, + maxWidth: 50, + maxLines: maxLines); + + Assert.Equal(expectedLines, layout.TextLines.Count); + } + } + + [Fact] + public void Should_Produce_Fixed_Height_Lines() + { + using (Start()) + { + var layout = new TextLayout( + s_multiLineText, + Typeface.Default, + 12, + Brushes.Black, + lineHeight: 50); + + foreach (var line in layout.TextLines) + { + Assert.Equal(50, line.LineMetrics.Size.Height); + } + } + } + private const string Text = "日本でTest一番読まれている英字新聞・ジャパンタイムズが発信する国内外ニュースと、様々なジャンルの特集記事。"; - [Fact(Skip= "Only used for profiling.")] + [Fact(Skip = "Only used for profiling.")] public void Should_Wrap() { using (Start()) @@ -526,12 +575,12 @@ namespace Avalonia.Skia.UnitTests } } - public static IDisposable Start() + private static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface .With(renderInterface: new PlatformRenderInterface(null), textShaperImpl: new TextShaperImpl(), - fontManagerImpl : new CustomFontManagerImpl())); + fontManagerImpl: new CustomFontManagerImpl())); return disposable; } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs new file mode 100644 index 0000000000..ed00d6aaed --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -0,0 +1,175 @@ +using System; +using System.Linq; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Skia.UnitTests.Media.TextFormatting +{ + public class TextLineTests + { + [InlineData("𐐷𐐷𐐷𐐷𐐷")] + [InlineData("𐐷1234")] + [Theory] + public void Should_Get_Next_Caret_CharacterHit(string text) + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var clusters = textLine.TextRuns.Cast().SelectMany(x => x.GlyphRun.GlyphClusters) + .ToArray(); + + var nextCharacterHit = new CharacterHit(0); + + for (var i = 1; i < clusters.Length; i++) + { + nextCharacterHit = textLine.GetNextCaretCharacterHit(nextCharacterHit); + + Assert.Equal(clusters[i], nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength); + } + } + } + + [InlineData("𐐷𐐷𐐷𐐷𐐷")] + [InlineData("𐐷1234")] + [Theory] + public void Should_Get_Previous_Caret_CharacterHit(string text) + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var clusters = textLine.TextRuns.Cast().SelectMany(x => x.GlyphRun.GlyphClusters) + .ToArray(); + + var previousCharacterHit = new CharacterHit(clusters[^1]); + + for (var i = clusters.Length - 2; i > 0; i--) + { + previousCharacterHit = textLine.GetPreviousCaretCharacterHit(previousCharacterHit); + + Assert.Equal(clusters[i], previousCharacterHit.FirstCharacterIndex); + } + } + } + + [Fact] + public void Should_Get_Distance_From_CharacterHit() + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new MultiBufferTextSource(defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var currentDistance = 0.0; + + foreach (var run in textLine.TextRuns) + { + var textRun = (ShapedTextCharacters)run; + + var glyphRun = textRun.GlyphRun; + + for (var i = 0; i < glyphRun.GlyphClusters.Length; i++) + { + var cluster = glyphRun.GlyphClusters[i]; + + var glyph = glyphRun.GlyphIndices[i]; + + var advance = glyphRun.GlyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; + + var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); + + Assert.Equal(currentDistance, distance); + + currentDistance += advance; + } + } + + Assert.Equal(currentDistance, + textLine.GetDistanceFromCharacterHit(new CharacterHit(MultiBufferTextSource.TextRange.Length))); + } + } + + [Fact] + public void Should_Get_CharacterHit_From_Distance() + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new MultiBufferTextSource(defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var currentDistance = 0.0; + + CharacterHit characterHit; + + foreach (var run in textLine.TextRuns) + { + var textRun = (ShapedTextCharacters)run; + + var glyphRun = textRun.GlyphRun; + + for (var i = 0; i < glyphRun.GlyphClusters.Length; i++) + { + var cluster = glyphRun.GlyphClusters[i]; + + var glyph = glyphRun.GlyphIndices[i]; + + var advance = glyphRun.GlyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; + + characterHit = textLine.GetCharacterHitFromDistance(currentDistance); + + Assert.Equal(cluster, characterHit.FirstCharacterIndex + characterHit.TrailingLength); + + currentDistance += advance; + } + } + + characterHit = textLine.GetCharacterHitFromDistance(textLine.LineMetrics.Size.Width); + + Assert.Equal(MultiBufferTextSource.TextRange.End, characterHit.FirstCharacterIndex); + } + } + + private static IDisposable Start() + { + var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface + .With(renderInterface: new PlatformRenderInterface(null), + textShaperImpl: new TextShaperImpl(), + fontManagerImpl: new CustomFontManagerImpl())); + + return disposable; + } + } +} diff --git a/tests/Avalonia.Skia.UnitTests/SimpleTextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/SimpleTextFormatterTests.cs deleted file mode 100644 index 8e695a11c8..0000000000 --- a/tests/Avalonia.Skia.UnitTests/SimpleTextFormatterTests.cs +++ /dev/null @@ -1,373 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Media; -using Avalonia.Media.TextFormatting; -using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.UnitTests; -using Avalonia.Utility; -using Xunit; - -namespace Avalonia.Skia.UnitTests -{ - public class SimpleTextFormatterTests - { - [Fact] - public void Should_Format_TextRuns_With_Default_Style() - { - using (Start()) - { - const string text = "0123456789"; - - var defaultTextRunStyle = new TextStyle(Typeface.Default, 12, Brushes.Black); - - var textSource = new SimpleTextSource(text, defaultTextRunStyle); - - var formatter = new SimpleTextFormatter(); - - var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); - - Assert.Single(textLine.TextRuns); - - var textRun = textLine.TextRuns[0]; - - Assert.Equal(defaultTextRunStyle.TextFormat, textRun.Style.TextFormat); - - Assert.Equal(defaultTextRunStyle.Foreground, textRun.Style.Foreground); - - Assert.Equal(text.Length, textRun.Text.Length); - } - } - - [Fact] - public void Should_Format_TextRuns_With_Multiple_Buffers() - { - using (Start()) - { - var defaultTextRunStyle = new TextStyle(Typeface.Default, 12, Brushes.Black); - - var textSource = new MultiBufferTextSource(defaultTextRunStyle); - - var formatter = new SimpleTextFormatter(); - - var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, - new TextParagraphProperties(defaultTextRunStyle)); - - Assert.Equal(5, textLine.TextRuns.Count); - - Assert.Equal(50, textLine.Text.Length); - } - } - - private class MultiBufferTextSource : ITextSource - { - private readonly string[] _runTexts; - private readonly TextStyle _defaultStyle; - - public MultiBufferTextSource(TextStyle defaultStyle) - { - _defaultStyle = defaultStyle; - - _runTexts = new[] { "A123456789", "B123456789", "C123456789", "D123456789", "E123456789" }; - } - - public TextPointer TextPointer => new TextPointer(0, 50); - - public TextRun GetTextRun(int textSourceIndex) - { - if (textSourceIndex == 50) - { - return new TextEndOfParagraph(); - } - - var index = textSourceIndex / 10; - - var runText = _runTexts[index]; - - return new TextCharacters( - new ReadOnlySlice(runText.AsMemory(), textSourceIndex, runText.Length), _defaultStyle); - } - } - - [Fact] - public void Should_Format_TextRuns_With_TextRunStyles() - { - using (Start()) - { - const string text = "0123456789"; - - var defaultStyle = new TextStyle(Typeface.Default, 12, Brushes.Black); - - var textStyleRuns = new[] - { - new TextStyleRun(new TextPointer(0, 3), defaultStyle ), - new TextStyleRun(new TextPointer(3, 3), new TextStyle(Typeface.Default, 13, Brushes.Black) ), - new TextStyleRun(new TextPointer(6, 3), new TextStyle(Typeface.Default, 14, Brushes.Black) ), - new TextStyleRun(new TextPointer(9, 1), defaultStyle ) - }; - - var textSource = new FormattableTextSource(text, defaultStyle, textStyleRuns); - - var formatter = new SimpleTextFormatter(); - - var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); - - Assert.Equal(text.Length, textLine.Text.Length); - - for (var i = 0; i < textStyleRuns.Length; i++) - { - var textStyleRun = textStyleRuns[i]; - - var textRun = textLine.TextRuns[i]; - - Assert.Equal(textStyleRun.TextPointer.Length, textRun.Text.Length); - } - } - } - - private class FormattableTextSource : ITextSource - { - private readonly ReadOnlySlice _text; - private readonly TextStyle _defaultStyle; - private ReadOnlySlice _textStyleRuns; - - public FormattableTextSource(string text, TextStyle defaultStyle, ReadOnlySlice textStyleRuns) - { - _text = text.AsMemory(); - - _defaultStyle = defaultStyle; - - _textStyleRuns = textStyleRuns; - } - - public TextRun GetTextRun(int textSourceIndex) - { - if (_textStyleRuns.IsEmpty) - { - return new TextEndOfParagraph(); - } - - var styleRun = _textStyleRuns[0]; - - _textStyleRuns = _textStyleRuns.Skip(1); - - return new TextCharacters(_text.AsSlice(styleRun.TextPointer.Start, styleRun.TextPointer.Length), - _defaultStyle); - } - } - - [Theory] - [InlineData("0123", 1)] - [InlineData("\r\n", 1)] - [InlineData("👍b", 2)] - [InlineData("a👍b", 3)] - [InlineData("a👍子b", 4)] - public void Should_Produce_Unique_Runs(string text, int numberOfRuns) - { - using (Start()) - { - var textSource = new SimpleTextSource(text, new TextStyle(Typeface.Default)); - - var formatter = new SimpleTextFormatter(); - - var textLine = - formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); - - Assert.Equal(numberOfRuns, textLine.TextRuns.Count); - } - } - - private class SimpleTextSource : ITextSource - { - private readonly ReadOnlySlice _text; - private readonly TextStyle _defaultTextStyle; - - public SimpleTextSource(string text, TextStyle defaultText) - { - _text = text.AsMemory(); - _defaultTextStyle = defaultText; - } - - public TextRun GetTextRun(int textSourceIndex) - { - var runText = _text.Skip(textSourceIndex); - - if (runText.IsEmpty) - { - return new TextEndOfParagraph(); - } - - return new TextCharacters(runText, _defaultTextStyle); - } - } - - [Fact] - public void Should_Split_Run_On_Script() - { - using (Start()) - { - const string text = "1234الدولي"; - - var textSource = new SimpleTextSource(text, new TextStyle(Typeface.Default)); - - var formatter = new SimpleTextFormatter(); - - var textLine = - formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); - - Assert.Equal(4, textLine.TextRuns[0].Text.Length); - } - } - - [Fact] - public void Should_Get_Distance_From_CharacterHit() - { - using (Start()) - { - var textSource = new MultiBufferTextSource(new TextStyle(Typeface.Default)); - - var formatter = new SimpleTextFormatter(); - - var textLine = - formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); - - var currentDistance = 0.0; - - foreach (var run in textLine.TextRuns) - { - var textRun = (ShapedTextRun)run; - - var glyphRun = textRun.GlyphRun; - - for (var i = 0; i < glyphRun.GlyphClusters.Length; i++) - { - var cluster = glyphRun.GlyphClusters[i]; - - var glyph = glyphRun.GlyphIndices[i]; - - var advance = glyphRun.GlyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; - - var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); - - Assert.Equal(currentDistance, distance); - - currentDistance += advance; - } - } - - Assert.Equal(currentDistance, textLine.GetDistanceFromCharacterHit(new CharacterHit(textSource.TextPointer.Length))); - } - } - - [Fact] - public void Should_Get_CharacterHit_From_Distance() - { - using (Start()) - { - var textSource = new MultiBufferTextSource(new TextStyle(Typeface.Default)); - - var formatter = new SimpleTextFormatter(); - - var textLine = - formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); - - var currentDistance = 0.0; - - CharacterHit characterHit; - - foreach (var run in textLine.TextRuns) - { - var textRun = (ShapedTextRun)run; - - var glyphRun = textRun.GlyphRun; - - for (var i = 0; i < glyphRun.GlyphClusters.Length; i++) - { - var cluster = glyphRun.GlyphClusters[i]; - - var glyph = glyphRun.GlyphIndices[i]; - - var advance = glyphRun.GlyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; - - characterHit = textLine.GetCharacterHitFromDistance(currentDistance); - - Assert.Equal(cluster, characterHit.FirstCharacterIndex + characterHit.TrailingLength); - - currentDistance += advance; - } - } - - characterHit = textLine.GetCharacterHitFromDistance(textLine.LineMetrics.Size.Width); - - Assert.Equal(textSource.TextPointer.End, characterHit.FirstCharacterIndex); - } - } - - [InlineData("Whether to turn off HTTPS. This option only applies if Individual, " + - "IndividualB2C, SingleOrg, or MultiOrg aren't used for ‑‑auth." - , "Noto Sans", 40)] - [InlineData("01234 56789 01234 56789", "Noto Mono", 7)] - [Theory] - public void Should_Wrap_Text(string text, string familyName, int numberOfCharactersPerLine) - { - using (Start()) - { - var lineBreaker = new LineBreakEnumerator(text.AsMemory()); - - var expected = new List(); - - while (lineBreaker.MoveNext()) - { - expected.Add(lineBreaker.Current.PositionWrap - 1); - } - - var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#" + - familyName); - - var defaultStyle = new TextStyle(typeface); - - var textSource = new SimpleTextSource(text, defaultStyle); - - var formatter = new SimpleTextFormatter(); - - var glyph = typeface.GlyphTypeface.GetGlyph('a'); - - var advance = typeface.GlyphTypeface.GetGlyphAdvance(glyph) * - (12.0 / typeface.GlyphTypeface.DesignEmHeight); - - var paragraphWidth = advance * numberOfCharactersPerLine; - - var currentPosition = 0; - - while (currentPosition < text.Length) - { - var textLine = - formatter.FormatLine(textSource, currentPosition, paragraphWidth, - new TextParagraphProperties(defaultStyle, textWrapping: TextWrapping.Wrap)); - - Assert.True(expected.Contains(textLine.Text.End)); - - var index = expected.IndexOf(textLine.Text.End); - - for (var i = 0; i <= index; i++) - { - expected.RemoveAt(0); - } - - currentPosition += textLine.Text.Length; - } - } - } - - public static IDisposable Start() - { - var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface - .With(renderInterface: new PlatformRenderInterface(null), - textShaperImpl: new TextShaperImpl())); - - AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new FontManager(new CustomFontManagerImpl())); - - return disposable; - } - } -} diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs index 0772e0e9bd..fe1c34385f 100644 --- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs @@ -1,19 +1,19 @@ -using Avalonia.Media; -using Avalonia.Media.TextFormatting; +using System; +using System.Globalization; +using Avalonia.Media; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; -using Avalonia.Utility; +using Avalonia.Utilities; namespace Avalonia.UnitTests { public class MockTextShaperImpl : ITextShaperImpl { - public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat) + public GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize, CultureInfo culture) { - var glyphTypeface = textFormat.Typeface.GlyphTypeface; + var glyphTypeface = typeface.GlyphTypeface; var glyphIndices = new ushort[text.Length]; - var height = textFormat.FontMetrics.LineHeight; - var width = 0.0; + var glyphCount = 0; for (var i = 0; i < text.Length;) { @@ -27,10 +27,11 @@ namespace Avalonia.UnitTests glyphIndices[index] = glyph; - width += glyphTypeface.GetGlyphAdvance(glyph); + glyphCount++; } - return new GlyphRun(glyphTypeface, textFormat.FontRenderingEmSize, glyphIndices, characters: text); + return new GlyphRun(glyphTypeface, fontRenderingEmSize, + new ReadOnlySlice(glyphIndices.AsMemory(0, glyphCount)), characters: text); } } } diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index b3e4b4edbc..48a333dc54 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -28,6 +28,7 @@ namespace Avalonia.UnitTests windowImpl.SetupAllProperties(); windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize); + windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize); windowImpl.Setup(x => x.Scaling).Returns(1); windowImpl.Setup(x => x.Screen).Returns(CreateScreenMock().Object); windowImpl.Setup(x => x.Position).Returns(() => position); @@ -79,6 +80,7 @@ namespace Avalonia.UnitTests popupImpl.SetupAllProperties(); popupImpl.Setup(x => x.ClientSize).Returns(() => clientSize); + popupImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize); popupImpl.Setup(x => x.Scaling).Returns(1); popupImpl.Setup(x => x.PopupPositioner).Returns(positioner); @@ -116,7 +118,7 @@ namespace Avalonia.UnitTests } } - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public IWindowImpl CreateEmbeddableWindow() { throw new NotImplementedException(); } diff --git a/tests/Avalonia.UnitTests/TestLogSink.cs b/tests/Avalonia.UnitTests/TestLogSink.cs index 5c1dd293c4..e10292a59b 100644 --- a/tests/Avalonia.UnitTests/TestLogSink.cs +++ b/tests/Avalonia.UnitTests/TestLogSink.cs @@ -27,7 +27,7 @@ namespace Avalonia.UnitTests return Disposable.Create(() => Logger.Sink = null); } - public bool IsEnabled(LogEventLevel level) + public bool IsEnabled(LogEventLevel level, string area) { return true; } diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index f291d386aa..b6f3a020e8 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -19,6 +19,8 @@ namespace Avalonia.UnitTests public TestRoot() { Renderer = Mock.Of(); + LayoutManager = new LayoutManager(this); + IsVisible = true; } public TestRoot(IControl child) @@ -44,7 +46,7 @@ namespace Avalonia.UnitTests public double LayoutScaling { get; set; } = 1; - public ILayoutManager LayoutManager { get; set; } = new LayoutManager(); + public ILayoutManager LayoutManager { get; set; } public double RenderScaling => 1; diff --git a/tests/Avalonia.UnitTests/TestTemplatedRoot.cs b/tests/Avalonia.UnitTests/TestTemplatedRoot.cs index da4d92ce5e..38ab3c3c5d 100644 --- a/tests/Avalonia.UnitTests/TestTemplatedRoot.cs +++ b/tests/Avalonia.UnitTests/TestTemplatedRoot.cs @@ -16,6 +16,7 @@ namespace Avalonia.UnitTests public TestTemplatedRoot() { + LayoutManager = new LayoutManager(this); Template = new FuncControlTemplate((x, scope) => new ContentPresenter { Name = "PART_ContentPresenter", @@ -28,7 +29,7 @@ namespace Avalonia.UnitTests public double LayoutScaling => 1; - public ILayoutManager LayoutManager { get; set; } = new LayoutManager(); + public ILayoutManager LayoutManager { get; set; } public double RenderScaling => 1; diff --git a/tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs index e17fd47ff8..f3f3c9a4ca 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs @@ -17,6 +17,41 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Equal(0xff, result.A); } + [Fact] + public void Try_Parse_Parses_RGB_Hash_Color() + { + var success = Color.TryParse("#ff8844", out Color result); + + Assert.True(success); + Assert.Equal(0xff, result.R); + Assert.Equal(0x88, result.G); + Assert.Equal(0x44, result.B); + Assert.Equal(0xff, result.A); + } + + [Fact] + public void Parse_Parses_RGB_Hash_Shorthand_Color() + { + var result = Color.Parse("#f84"); + + Assert.Equal(0xff, result.R); + Assert.Equal(0x88, result.G); + Assert.Equal(0x44, result.B); + Assert.Equal(0xff, result.A); + } + + [Fact] + public void Try_Parse_Parses_RGB_Hash_Shorthand_Color() + { + var success = Color.TryParse("#f84", out Color result); + + Assert.True(success); + Assert.Equal(0xff, result.R); + Assert.Equal(0x88, result.G); + Assert.Equal(0x44, result.B); + Assert.Equal(0xff, result.A); + } + [Fact] public void Parse_Parses_ARGB_Hash_Color() { @@ -28,6 +63,41 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Equal(0x40, result.A); } + [Fact] + public void Try_Parse_Parses_ARGB_Hash_Color() + { + var success = Color.TryParse("#40ff8844", out Color result); + + Assert.True(success); + Assert.Equal(0xff, result.R); + Assert.Equal(0x88, result.G); + Assert.Equal(0x44, result.B); + Assert.Equal(0x40, result.A); + } + + [Fact] + public void Parse_Parses_ARGB_Hash_Shorthand_Color() + { + var result = Color.Parse("#4f84"); + + Assert.Equal(0xff, result.R); + Assert.Equal(0x88, result.G); + Assert.Equal(0x44, result.B); + Assert.Equal(0x44, result.A); + } + + [Fact] + public void Try_Parse_Parses_ARGB_Hash_Shorthand_Color() + { + var success = Color.TryParse("#4f84", out Color result); + + Assert.True(success); + Assert.Equal(0xff, result.R); + Assert.Equal(0x88, result.G); + Assert.Equal(0x44, result.B); + Assert.Equal(0x44, result.A); + } + [Fact] public void Parse_Parses_Named_Color_Lowercase() { @@ -39,6 +109,18 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Equal(0xff, result.A); } + [Fact] + public void TryParse_Parses_Named_Color_Lowercase() + { + var success = Color.TryParse("red", out Color result); + + Assert.True(success); + Assert.Equal(0xff, result.R); + Assert.Equal(0x00, result.G); + Assert.Equal(0x00, result.B); + Assert.Equal(0xff, result.A); + } + [Fact] public void Parse_Parses_Named_Color_Uppercase() { @@ -50,22 +132,52 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Equal(0xff, result.A); } + [Fact] + public void TryParse_Parses_Named_Color_Uppercase() + { + var success = Color.TryParse("RED", out Color result); + + Assert.True(success); + Assert.Equal(0xff, result.R); + Assert.Equal(0x00, result.G); + Assert.Equal(0x00, result.B); + Assert.Equal(0xff, result.A); + } + [Fact] public void Parse_Hex_Value_Doesnt_Accept_Too_Few_Chars() { Assert.Throws(() => Color.Parse("#ff")); } + [Fact] + public void TryParse_Hex_Value_Doesnt_Accept_Too_Few_Chars() + { + Assert.False(Color.TryParse("#ff", out _)); + } + [Fact] public void Parse_Hex_Value_Doesnt_Accept_Too_Many_Chars() { Assert.Throws(() => Color.Parse("#ff5555555")); } + [Fact] + public void TryParse_Hex_Value_Doesnt_Accept_Too_Many_Chars() + { + Assert.False(Color.TryParse("#ff5555555", out _)); + } + [Fact] public void Parse_Hex_Value_Doesnt_Accept_Invalid_Number() { Assert.Throws(() => Color.Parse("#ff808g80")); } + + [Fact] + public void TryParse_Hex_Value_Doesnt_Accept_Invalid_Number() + { + Assert.False(Color.TryParse("#ff808g80", out _)); + } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs index 028caa35c6..219c7ece46 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs @@ -1,7 +1,7 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.UnitTests; -using Avalonia.Utility; +using Avalonia.Utilities; using Xunit; namespace Avalonia.Visuals.UnitTests.Media diff --git a/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs index ff1d17164e..6ef48b6161 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using System; +using Avalonia.Utilities; using Xunit; namespace Avalonia.Visuals.UnitTests.Media @@ -6,11 +7,93 @@ namespace Avalonia.Visuals.UnitTests.Media public class MatrixTests { [Fact] - public void Parse_Parses() + public void Can_Parse() { var matrix = Matrix.Parse("1,2,3,-4,5 6"); var expected = new Matrix(1, 2, 3, -4, 5, 6); Assert.Equal(expected, matrix); } + + [Fact] + public void Singular_Has_No_Inverse() + { + var matrix = new Matrix(0, 0, 0, 0, 0, 0); + + Assert.False(matrix.HasInverse); + } + + [Fact] + public void Identity_Has_Inverse() + { + var matrix = Matrix.Identity; + + Assert.True(matrix.HasInverse); + } + + [Fact] + public void Can_Decompose_Translation() + { + var matrix = Matrix.CreateTranslation(5, 10); + + var result = Matrix.TryDecomposeTransform(matrix, out Matrix.Decomposed decomposed); + + Assert.Equal(true, result); + Assert.Equal(5, decomposed.Translate.X); + Assert.Equal(10, decomposed.Translate.Y); + } + + [Theory] + [InlineData(30d)] + [InlineData(0d)] + [InlineData(90d)] + [InlineData(270d)] + public void Can_Decompose_Angle(double angleDeg) + { + var angleRad = MathUtilities.Deg2Rad(angleDeg); + + var matrix = Matrix.CreateRotation(angleRad); + + var result = Matrix.TryDecomposeTransform(matrix, out Matrix.Decomposed decomposed); + + Assert.Equal(true, result); + + var expected = NormalizeAngle(angleRad); + var actual = NormalizeAngle(decomposed.Angle); + + Assert.Equal(expected, actual, 4); + } + + [Theory] + [InlineData(1d, 1d)] + [InlineData(-1d, 1d)] + [InlineData(1d, -1d)] + [InlineData(5d, 10d)] + public void Can_Decompose_Scale(double x, double y) + { + var matrix = Matrix.CreateScale(x, y); + + var result = Matrix.TryDecomposeTransform(matrix, out Matrix.Decomposed decomposed); + + Assert.Equal(true, result); + Assert.Equal(x, decomposed.Scale.X); + Assert.Equal(y, decomposed.Scale.Y); + } + + private static double NormalizeAngle(double rad) + { + double twoPi = 2 * Math.PI; + + while (rad < 0) + { + rad += twoPi; + } + + while (rad > twoPi) + { + rad -= twoPi; + } + + return rad; + } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BreakPairTable.txt b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BreakPairTable.txt index 90c1e2cee1..814ce15d0a 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BreakPairTable.txt +++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BreakPairTable.txt @@ -1,33 +1,34 @@ - OP CL CP QU GL NS EX SY IS PR PO NU AL HL ID IN HY BA BB B2 ZW CM WJ H2 H3 JL JV JT RI EB EM ZWJ -OP ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ @ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ -CL _ ^ ^ % % ^ ^ ^ ^ % % _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -CP _ ^ ^ % % ^ ^ ^ ^ % % % % % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -QU ^ ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % -GL % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % -NS _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -EX _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -SY _ ^ ^ % % % ^ ^ ^ _ _ % _ % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -IS _ ^ ^ % % % ^ ^ ^ _ _ % % % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -PR % ^ ^ % % % ^ ^ ^ _ _ % % % % _ % % _ _ ^ # ^ % % % % % _ % % % -PO % ^ ^ % % % ^ ^ ^ _ _ % % % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -NU % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -AL % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -HL % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -ID _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -IN _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -HY _ ^ ^ % _ % ^ ^ ^ _ _ % _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -BA _ ^ ^ % _ % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -BB % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % -B2 _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ ^ ^ # ^ _ _ _ _ _ _ _ _ % -ZW _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ^ _ _ _ _ _ _ _ _ _ _ _ -CM % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -WJ % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % -H2 _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ % % _ _ _ % -H3 _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ % _ _ _ % -JL _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ % % % % _ _ _ _ % -JV _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ % % _ _ _ % -JT _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ % _ _ _ % -RI _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ % _ _ % -EB _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ % % -EM _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -ZWJ _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ % _ % % _ _ ^ # ^ _ _ _ _ _ _ % % % + OP CL CP QU GL NS EX SY IS PR PO NU AL HL ID IN HY BA BB B2 ZW CM WJ H2 H3 JL JV JT RI EB EM ZWJ CB +OP ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ @ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ +CL _ ^ ^ % % ^ ^ ^ ^ % % _ _ _ _ ^ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +CP _ ^ ^ % % ^ ^ ^ ^ % % % % % _ ^ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +QU ^ ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % % +GL % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % % +NS _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +EX _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +SY _ ^ ^ % % % ^ ^ ^ _ _ % _ % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +IS _ ^ ^ % % % ^ ^ ^ _ _ % % % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +PR % ^ ^ % % % ^ ^ ^ _ _ % % % % _ % % _ _ ^ # ^ % % % % % _ % % % _ +PO % ^ ^ % % % ^ ^ ^ _ _ % % % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +NU % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +AL % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +HL % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +ID _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +IN _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +HY _ ^ ^ % _ % ^ ^ ^ _ _ % _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +BA _ ^ ^ % _ % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +BB % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % _ +B2 _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ ^ ^ # ^ _ _ _ _ _ _ _ _ % _ +ZW _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ^ _ _ _ _ _ _ _ _ _ _ _ _ +CM % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +WJ % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % % +H2 _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ % % _ _ _ % _ +H3 _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ % _ _ _ % _ +JL _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ % % % % _ _ _ _ % _ +JV _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ % % _ _ _ % _ +JT _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ % _ _ _ % _ +RI _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ % _ _ % _ +EB _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ % % _ +EM _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +ZWJ % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +CB _ ^ ^ % % _ ^ ^ ^ _ _ _ _ _ _ _ _ _ _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGenerator.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGenerator.cs index 94ab615130..c074e5a1f1 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGenerator.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGenerator.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net.Http; using System.Text.RegularExpressions; using Avalonia.Media.TextFormatting.Unicode; @@ -12,6 +11,11 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting { public static void Execute() { + if (!Directory.Exists("Generated")) + { + Directory.CreateDirectory("Generated"); + } + using (var stream = File.Create("Generated\\GraphemeBreak.trie")) { var trie = GenerateBreakTypeTrie(); @@ -22,48 +26,29 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting private static UnicodeTrie GenerateBreakTypeTrie() { - var graphemeBreakClassValues = UnicodeEnumsGenerator.GetPropertyValueAliases("# Grapheme_Cluster_Break (GCB)"); - - var graphemeBreakClassMapping = graphemeBreakClassValues.Select(x => x.name).ToList(); - var trieBuilder = new UnicodeTrieBuilder(); - var graphemeBreakData = ReadBreakData( - "https://www.unicode.org/Public/UCD/latest/ucd/auxiliary/GraphemeBreakProperty.txt"); - - foreach (var (start, end, graphemeBreakType) in graphemeBreakData) - { - if (!graphemeBreakClassMapping.Contains(graphemeBreakType)) - { - continue; - } - - if (start == end) - { - trieBuilder.Set(start, (uint)graphemeBreakClassMapping.IndexOf(graphemeBreakType)); - } - else - { - trieBuilder.SetRange(start, end, (uint)graphemeBreakClassMapping.IndexOf(graphemeBreakType)); - } - } + var graphemeBreakData = ReadBreakData(Path.Combine(UnicodeDataGenerator.Ucd, "auxiliary/GraphemeBreakProperty.txt")); - var emojiBreakData = ReadBreakData("https://unicode.org/Public/emoji/12.0/emoji-data.txt"); + var emojiBreakData = ReadBreakData(Path.Combine(UnicodeDataGenerator.Ucd, "emoji/emoji-data.txt")); - foreach (var (start, end, graphemeBreakType) in emojiBreakData) + foreach (var breakData in new [] { graphemeBreakData, emojiBreakData }) { - if (!graphemeBreakClassMapping.Contains(graphemeBreakType)) + foreach (var (start, end, graphemeBreakType) in breakData) { - continue; - } + if (!Enum.TryParse(graphemeBreakType, out var value)) + { + continue; + } - if (start == end) - { - trieBuilder.Set(start, (uint)graphemeBreakClassMapping.IndexOf(graphemeBreakType)); - } - else - { - trieBuilder.SetRange(start, end, (uint)graphemeBreakClassMapping.IndexOf(graphemeBreakType)); + if (start == end) + { + trieBuilder.Set(start, (uint)value); + } + else + { + trieBuilder.SetRange(start, end, (uint)value); + } } } @@ -113,7 +98,9 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting end = Convert.ToInt32(match.Groups[2].Value, 16); } - data.Add((start, end, match.Groups[3].Value)); + var breakType = match.Groups[3].Value; + + data.Add((start, end, breakType)); } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs index d9a9c82f85..848b60b341 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs @@ -1,9 +1,4 @@ using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; using Avalonia.Media.TextFormatting.Unicode; using Xunit; @@ -16,10 +11,12 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting public class GraphemeBreakClassTrieGeneratorTests { [Theory(Skip = "Only run when we update the trie.")] - [ClassData(typeof(GraphemeEnumeratorTestDataGenerator))] + [ClassData(typeof(GraphemeBreakTestDataGenerator))] public void Should_Enumerate(string text, int expectedLength) { - var enumerator = new GraphemeEnumerator(text.AsMemory()); + var textMemory = text.AsMemory(); + + var enumerator = new GraphemeEnumerator(textMemory); Assert.True(enumerator.MoveNext()); @@ -31,7 +28,9 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting { const string text = "ABCDEFGHIJ"; - var enumerator = new GraphemeEnumerator(text.AsMemory()); + var textMemory = text.AsMemory(); + + var enumerator = new GraphemeEnumerator(textMemory); var count = 0; @@ -51,73 +50,11 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting GraphemeBreakClassTrieGenerator.Execute(); } - public class GraphemeEnumeratorTestDataGenerator : IEnumerable + private class GraphemeBreakTestDataGenerator : TestDataGenerator { - private readonly List _testData; - - public GraphemeEnumeratorTestDataGenerator() - { - _testData = ReadTestData(); - } - - public IEnumerator GetEnumerator() - { - return _testData.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - private static List ReadTestData() + public GraphemeBreakTestDataGenerator() + : base("auxiliary/GraphemeBreakTest.txt") { - var testData = new List(); - - using (var client = new HttpClient()) - { - using (var result = client.GetAsync("https://www.unicode.org/Public/UNIDATA/auxiliary/GraphemeBreakTest.txt").GetAwaiter().GetResult()) - { - if (!result.IsSuccessStatusCode) - return testData; - - using (var stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult()) - using (var reader = new StreamReader(stream)) - { - while (!reader.EndOfStream) - { - var line = reader.ReadLine(); - - if (line == null) - { - break; - } - - if (line.StartsWith("#") || string.IsNullOrEmpty(line)) - { - continue; - } - - var elements = line.Split('#')[0].Replace("÷\t", "÷").Trim('÷').Split('÷'); - - var chars = elements[0].Replace(" × ", " ").Split(' '); - - var codepoints = chars.Where(x => x != "" && x != "×") - .Select(x => Convert.ToInt32(x, 16)).ToArray(); - - var text = string.Join(null, codepoints.Select(char.ConvertFromUtf32)); - - var length = codepoints.Select(x => x > ushort.MaxValue ? 2 : 1).Sum(); - - var data = new object[] { text, length }; - - testData.Add(data); - } - } - } - } - - return testData; } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakerTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakerTests.cs index fe7d7adc17..3d489af3a2 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakerTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakerTests.cs @@ -1,9 +1,9 @@ using System; using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Utility; +using Avalonia.Utilities; using Xunit; -namespace Avalonia.Visuals.UnitTests.Media.Text +namespace Avalonia.Visuals.UnitTests.Media.TextFormatting { public class LineBreakerTests { diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/TestDataGenerator.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/TestDataGenerator.cs new file mode 100644 index 0000000000..058de909df --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/TestDataGenerator.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; + +namespace Avalonia.Visuals.UnitTests.Media.TextFormatting +{ + public abstract class TestDataGenerator : IEnumerable + { + private readonly string _fileName; + private readonly List _testData; + + protected TestDataGenerator(string fileName) + { + _fileName = fileName; + _testData = ReadTestData(); + } + + public IEnumerator GetEnumerator() + { + return _testData.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + private List ReadTestData() + { + var testData = new List(); + + using (var client = new HttpClient()) + { + var url = Path.Combine(UnicodeDataGenerator.Ucd, _fileName); + + using (var result = client.GetAsync(url).GetAwaiter().GetResult()) + { + if (!result.IsSuccessStatusCode) + return testData; + + using (var stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult()) + using (var reader = new StreamReader(stream)) + { + while (!reader.EndOfStream) + { + var line = reader.ReadLine(); + + if (line == null) + { + break; + } + + if (line.StartsWith("#") || string.IsNullOrEmpty(line)) + { + continue; + } + + var elements = line.Split('#'); + + elements = elements[0].Replace("÷\t", "÷").Trim('÷').Split('÷'); + + var chars = elements[0].Replace(" × ", " ").Split(' '); + + var codepoints = chars.Where(x => x != "" && x != "×") + .Select(x => Convert.ToInt32(x, 16)).ToArray(); + + var text = string.Join(null, codepoints.Select(char.ConvertFromUtf32)); + + var length = codepoints.Select(x => x > ushort.MaxValue ? 2 : 1).Sum(); + + var data = new object[] { text, length }; + + testData.Add(data); + } + } + } + } + + return testData; + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGenerator.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGenerator.cs index f7b8b68ab3..cbe8edefb6 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGenerator.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGenerator.cs @@ -9,13 +9,16 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting { internal static class UnicodeDataGenerator { + public const string Ucd = "https://www.unicode.org/Public/13.0.0/ucd/"; + public static void Execute() { var codepoints = new Dictionary(); - var generalCategoryValues = UnicodeEnumsGenerator.CreateGeneralCategoryEnum(); + var generalCategoryEntries = + UnicodeEnumsGenerator.CreateGeneralCategoryEnum(); - var generalCategoryMappings = CreateTagToIndexMappings(generalCategoryValues); + var generalCategoryMappings = CreateTagToIndexMappings(generalCategoryEntries); var generalCategoryData = ReadGeneralCategoryData(); @@ -26,23 +29,23 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting AddGeneralCategoryRange(codepoints, range, generalCategory); } - var scriptValues = UnicodeEnumsGenerator.CreateScriptEnum(); + var scriptEntries = UnicodeEnumsGenerator.CreateScriptEnum(); - var scriptMappings = CreateNameToIndexMappings(scriptValues); + var scriptMappings = CreateNameToIndexMappings(scriptEntries); var scriptData = ReadScriptData(); foreach (var (range, name) in scriptData) { - var script = scriptMappings[name.Replace("_", "")]; + var script = scriptMappings[name]; AddScriptRange(codepoints, range, script); - } - var biDiClassValues = UnicodeEnumsGenerator.CreateBiDiClassEnum(); + var biDiClassEntries = + UnicodeEnumsGenerator.CreateBiDiClassEnum(); - var biDiClassMappings = CreateTagToIndexMappings(biDiClassValues); + var biDiClassMappings = CreateTagToIndexMappings(biDiClassEntries); var biDiData = ReadBiDiData(); @@ -53,9 +56,10 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting AddBiDiClassRange(codepoints, range, biDiClass); } - var lineBreakClassValues = UnicodeEnumsGenerator.CreateLineBreakClassEnum(); + var lineBreakClassEntries = + UnicodeEnumsGenerator.CreateLineBreakClassEnum(); - var lineBreakClassMappings = CreateTagToIndexMappings(lineBreakClassValues); + var lineBreakClassMappings = CreateTagToIndexMappings(lineBreakClassEntries); var lineBreakClassData = ReadLineBreakClassData(); @@ -66,11 +70,11 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting AddLineBreakClassRange(codepoints, range, lineBreakClass); } - const int initialValue = ((int)LineBreakClass.Unknown << UnicodeData.LINEBREAK_SHIFT) | - ((int)BiDiClass.LeftToRight << UnicodeData.BIDI_SHIFT) | - ((int)Script.Unknown << UnicodeData.SCRIPT_SHIFT) | (int)GeneralCategory.Other; + //const int initialValue = (0 << UnicodeData.LINEBREAK_SHIFT) | + // (0 << UnicodeData.BIDI_SHIFT) | + // (0 << UnicodeData.SCRIPT_SHIFT) | (int)GeneralCategory.Other; - var builder = new UnicodeTrieBuilder(initialValue); + var builder = new UnicodeTrieBuilder(/*initialValue*/); foreach (var properties in codepoints.Values) { @@ -88,27 +92,30 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting trie.Save(stream); } + + UnicodeEnumsGenerator.CreatePropertyValueAliasHelper(scriptEntries, generalCategoryEntries, + biDiClassEntries, lineBreakClassEntries); } - private static Dictionary CreateTagToIndexMappings(List<(string name, string tag, string comment)> values) + private static Dictionary CreateTagToIndexMappings(List entries) { var mappings = new Dictionary(); - for (var i = 0; i < values.Count; i++) + for (var i = 0; i < entries.Count; i++) { - mappings.Add(values[i].tag, i); + mappings.Add(entries[i].Tag, i); } return mappings; } - private static Dictionary CreateNameToIndexMappings(List<(string name, string tag, string comment)> values) + private static Dictionary CreateNameToIndexMappings(List entries) { var mappings = new Dictionary(); - for (var i = 0; i < values.Count; i++) + for (var i = 0; i < entries.Count; i++) { - mappings.Add(values[i].name, i); + mappings.Add(entries[i].Name, i); } return mappings; @@ -180,24 +187,22 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting public static List<(CodepointRange, string)> ReadGeneralCategoryData() { - return ReadUnicodeData( - "https://www.unicode.org/Public/UCD/latest/ucd/extracted/DerivedGeneralCategory.txt"); + return ReadUnicodeData("extracted/DerivedGeneralCategory.txt"); } public static List<(CodepointRange, string)> ReadScriptData() { - return ReadUnicodeData("https://www.unicode.org/Public/UCD/latest/ucd/Scripts.txt"); + return ReadUnicodeData("Scripts.txt"); } public static List<(CodepointRange, string)> ReadBiDiData() { - return ReadUnicodeData("https://www.unicode.org/Public/UCD/latest/ucd/extracted/DerivedBidiClass.txt"); + return ReadUnicodeData("extracted/DerivedBidiClass.txt"); } public static List<(CodepointRange, string)> ReadLineBreakClassData() { - return ReadUnicodeData( - "https://www.unicode.org/Public/UCD/latest/ucd/extracted/DerivedLineBreak.txt"); + return ReadUnicodeData("extracted/DerivedLineBreak.txt"); } private static List<(CodepointRange, string)> ReadUnicodeData(string file) @@ -208,7 +213,9 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting using (var client = new HttpClient()) { - using (var result = client.GetAsync(file).GetAwaiter().GetResult()) + var url = Path.Combine(Ucd, file); + + using (var result = client.GetAsync(url).GetAwaiter().GetResult()) { if (!result.IsSuccessStatusCode) { diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGeneratorTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGeneratorTests.cs index 47aef84533..5c705ba0c7 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGeneratorTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGeneratorTests.cs @@ -1,4 +1,6 @@ -using Xunit; +using System; +using Avalonia.Media.TextFormatting.Unicode; +using Xunit; namespace Avalonia.Visuals.UnitTests.Media.TextFormatting { @@ -13,5 +15,26 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting { UnicodeDataGenerator.Execute(); } + [Theory(Skip = "Only run when we update the trie.")] + [ClassData(typeof(LineBreakTestDataGenerator))] + + public void Should_Enumerate_LineBreaks(string text, int expectedLength) + { + var textMemory = text.AsMemory(); + + var enumerator = new LineBreakEnumerator(textMemory); + + Assert.True(enumerator.MoveNext()); + + Assert.Equal(expectedLength, enumerator.Current.PositionWrap); + } + + private class LineBreakTestDataGenerator : TestDataGenerator + { + public LineBreakTestDataGenerator() + : base("auxiliary/LineBreakTest.txt") + { + } + } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeEnumsGenerator.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeEnumsGenerator.cs index e141204d4c..3a936ff3b0 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeEnumsGenerator.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeEnumsGenerator.cs @@ -8,9 +8,16 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting { internal static class UnicodeEnumsGenerator { - public static List<(string name, string tag, string comment)> CreateScriptEnum() + public static List CreateScriptEnum() { - var scriptValues = GetPropertyValueAliases("# Script (sc)"); + var entries = new List + { + new DataEntry("Unknown", "Zzzz", string.Empty), + new DataEntry("Common", "Zyyy", string.Empty), + new DataEntry("Inherited", "Zinh", string.Empty) + }; + + ParseDataEntries("# Script (sc)", entries); using (var stream = File.Create("Generated\\Script.cs")) using (var writer = new StreamWriter(stream)) @@ -20,22 +27,24 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting writer.WriteLine(" public enum Script"); writer.WriteLine(" {"); - foreach (var (name, tag, comment) in scriptValues) + foreach (var entry in entries) { - writer.WriteLine(" " + name + ", //" + tag + - (string.IsNullOrEmpty(comment) ? string.Empty : "#" + comment)); + writer.WriteLine(" " + entry.Name.Replace("_", "") + ", //" + entry.Tag + + (string.IsNullOrEmpty(entry.Comment) ? string.Empty : "#" + entry.Comment)); } writer.WriteLine(" }"); writer.WriteLine("}"); } - return scriptValues; + return entries; } - public static List<(string name, string tag, string comment)> CreateGeneralCategoryEnum() + public static List CreateGeneralCategoryEnum() { - var generalCategoryValues = GetPropertyValueAliases("# General_Category (gc)"); + var entries = new List { new DataEntry("Other", "C", " Cc | Cf | Cn | Co | Cs") }; + + ParseDataEntries("# General_Category (gc)", entries); using (var stream = File.Create("Generated\\GeneralCategory.cs")) using (var writer = new StreamWriter(stream)) @@ -45,22 +54,24 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting writer.WriteLine(" public enum GeneralCategory"); writer.WriteLine(" {"); - foreach (var (name, tag, comment) in generalCategoryValues) + foreach (var entry in entries) { - writer.WriteLine(" " + name + ", //" + tag + - (string.IsNullOrEmpty(comment) ? string.Empty : "#" + comment)); + writer.WriteLine(" " + entry.Name.Replace("_", "") + ", //" + entry.Tag + + (string.IsNullOrEmpty(entry.Comment) ? string.Empty : "#" + entry.Comment)); } writer.WriteLine(" }"); writer.WriteLine("}"); } - return generalCategoryValues; + return entries; } - public static List<(string name, string tag, string comment)> CreateGraphemeBreakTypeEnum() + public static List CreateGraphemeBreakTypeEnum() { - var graphemeClusterBreakValues = GetPropertyValueAliases("# Grapheme_Cluster_Break (GCB)"); + var entries = new List { new DataEntry("Other", "XX", string.Empty) }; + + ParseDataEntries("# Grapheme_Cluster_Break (GCB)", entries); using (var stream = File.Create("Generated\\GraphemeBreakClass.cs")) using (var writer = new StreamWriter(stream)) @@ -70,10 +81,10 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting writer.WriteLine(" public enum GraphemeBreakClass"); writer.WriteLine(" {"); - foreach (var (name, tag, comment) in graphemeClusterBreakValues) + foreach (var entry in entries) { - writer.WriteLine(" " + name + ", //" + tag + - (string.IsNullOrEmpty(comment) ? string.Empty : "#" + comment)); + writer.WriteLine(" " + entry.Name.Replace("_", "") + ", //" + entry.Tag + + (string.IsNullOrEmpty(entry.Comment) ? string.Empty : "#" + entry.Comment)); } writer.WriteLine(" ExtendedPictographic"); @@ -82,7 +93,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting writer.WriteLine("}"); } - return graphemeClusterBreakValues; + return entries; } private static List GenerateBreakPairTable() @@ -185,20 +196,32 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting } } - public static List<(string name, string tag, string comment)> CreateLineBreakClassEnum() + public static List CreateLineBreakClassEnum() { var usedLineBreakClasses = GenerateBreakPairTable(); - var lineBreakValues = GetPropertyValueAliases("# Line_Break (lb)"); + var entries = new List { new DataEntry("Unknown", "XX", string.Empty) }; + + ParseDataEntries("# Line_Break (lb)", entries); - var lineBreakClassMappings = lineBreakValues.ToDictionary(x => x.tag, x => (x.name, x.tag, x.comment)); + var orderedLineBreakEntries = new Dictionary(); - var orderedLineBreakValues = usedLineBreakClasses.Select(x => + foreach (var tag in usedLineBreakClasses) { - var value = lineBreakClassMappings[x]; - lineBreakClassMappings.Remove(x); - return value; - }).ToList(); + var entry = entries.Single(x => x.Tag == tag); + + orderedLineBreakEntries.Add(tag, entry); + } + + foreach (var entry in entries) + { + if (orderedLineBreakEntries.ContainsKey(entry.Tag)) + { + continue; + } + + orderedLineBreakEntries.Add(entry.Tag, entry); + } using (var stream = File.Create("Generated\\LineBreakClass.cs")) using (var writer = new StreamWriter(stream)) @@ -208,32 +231,24 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting writer.WriteLine(" public enum LineBreakClass"); writer.WriteLine(" {"); - foreach (var (name, tag, comment) in orderedLineBreakValues) - { - writer.WriteLine(" " + name + ", //" + tag + - (string.IsNullOrEmpty(comment) ? string.Empty : "#" + comment)); - } - - writer.WriteLine(); - - foreach (var (name, tag, comment) in lineBreakClassMappings.Values) + foreach (var entry in orderedLineBreakEntries.Values) { - writer.WriteLine(" " + name + ", //" + tag + - (string.IsNullOrEmpty(comment) ? string.Empty : "#" + comment)); + writer.WriteLine(" " + entry.Name.Replace("_", "") + ", //" + entry.Tag + + (string.IsNullOrEmpty(entry.Comment) ? string.Empty : "#" + entry.Comment)); } writer.WriteLine(" }"); writer.WriteLine("}"); } - orderedLineBreakValues.AddRange(lineBreakClassMappings.Values); - - return orderedLineBreakValues; + return orderedLineBreakEntries.Values.ToList(); } - public static List<(string name, string tag, string comment)> CreateBiDiClassEnum() + public static List CreateBiDiClassEnum() { - var biDiClassValues = GetPropertyValueAliases("# Bidi_Class (bc)"); + var entries = new List { new DataEntry("Left_To_Right", "L", string.Empty) }; + + ParseDataEntries("# Bidi_Class (bc)", entries); using (var stream = File.Create("Generated\\BiDiClass.cs")) using (var writer = new StreamWriter(stream)) @@ -243,23 +258,21 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting writer.WriteLine(" public enum BiDiClass"); writer.WriteLine(" {"); - foreach (var (name, tag, comment) in biDiClassValues) + foreach (var entry in entries) { - writer.WriteLine(" " + name + ", //" + tag + - (string.IsNullOrEmpty(comment) ? string.Empty : "#" + comment)); + writer.WriteLine(" " + entry.Name.Replace("_", "") + ", //" + entry.Tag + + (string.IsNullOrEmpty(entry.Comment) ? string.Empty : "#" + entry.Comment)); } writer.WriteLine(" }"); writer.WriteLine("}"); } - return biDiClassValues; + return entries; } - public static void CreatePropertyValueAliasHelper(List<(string name, string tag, string comment)> scriptValues, - List<(string name, string tag, string comment)> generalCategoryValues, - List<(string name, string tag, string comment)> biDiClassValues, - List<(string name, string tag, string comment)> lineBreakValues) + public static void CreatePropertyValueAliasHelper(List scriptEntries, IEnumerable generalCategoryEntries, + IEnumerable biDiClassEntries, IEnumerable lineBreakClassEntries) { using (var stream = File.Create("Generated\\PropertyValueAliasHelper.cs")) using (var writer = new StreamWriter(stream)) @@ -269,35 +282,35 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting writer.WriteLine("namespace Avalonia.Media.TextFormatting.Unicode"); writer.WriteLine("{"); - writer.WriteLine(" public static class PropertyValueAliasHelper"); + writer.WriteLine(" internal static class PropertyValueAliasHelper"); writer.WriteLine(" {"); - WritePropertyValueAliasGetTag(writer, scriptValues, "Script", "Zzzz"); + WritePropertyValueAliasGetTag(writer, scriptEntries, "Script", "Zzzz"); - WritePropertyValueAlias(writer, scriptValues, "Script", "Unknown"); + WritePropertyValueAlias(writer, scriptEntries, "Script", "Unknown"); - WritePropertyValueAlias(writer, generalCategoryValues, "GeneralCategory", "Other"); + WritePropertyValueAlias(writer, generalCategoryEntries, "GeneralCategory", "Other"); - WritePropertyValueAlias(writer, biDiClassValues, "BiDiClass", "LeftToRight"); + WritePropertyValueAlias(writer, biDiClassEntries, "BiDiClass", "LeftToRight"); - WritePropertyValueAlias(writer, lineBreakValues, "LineBreakClass", "Unknown"); + WritePropertyValueAlias(writer, lineBreakClassEntries, "LineBreakClass", "Unknown"); writer.WriteLine(" }"); writer.WriteLine("}"); } } - public static List<(string name, string tag, string comment)> GetPropertyValueAliases(string property) + public static void ParseDataEntries(string property, List entries) { - var data = new List<(string name, string tag, string comment)>(); - using (var client = new HttpClient()) { - using (var result = client.GetAsync("https://www.unicode.org/Public/UCD/latest/ucd/PropertyValueAliases.txt").GetAwaiter().GetResult()) + var url = Path.Combine(UnicodeDataGenerator.Ucd, "PropertyValueAliases.txt"); + + using (var result = client.GetAsync(url).GetAwaiter().GetResult()) { if (!result.IsSuccessStatusCode) { - return data; + return; } using (var stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult()) @@ -337,7 +350,12 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting elements = elements[2].Split('#'); - var name = elements[0].Trim().Replace("_", string.Empty); + var name = elements[0].Trim(); + + if (entries.Any(x => x.Name == name)) + { + continue; + } var comment = string.Empty; @@ -346,24 +364,25 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting comment = elements[1]; } - data.Add((name, tag, comment)); + var entry = new DataEntry(name, tag, comment); + + entries.Add(entry); } } } } - - return data; } - private static void WritePropertyValueAliasGetTag(TextWriter writer, - IEnumerable<(string name, string tag, string comment)> values, string typeName, string defaultValue) + private static void WritePropertyValueAliasGetTag(TextWriter writer, IEnumerable entries, + string typeName, string defaultValue) { - writer.WriteLine($" private static readonly Dictionary<{typeName}, string> s_{typeName.ToLower()}ToTag = "); + writer.WriteLine( + $" private static readonly Dictionary<{typeName}, string> s_{typeName.ToLower()}ToTag = "); writer.WriteLine($" new Dictionary<{typeName}, string>{{"); - foreach (var (name, tag, comment) in values) + foreach (var entry in entries) { - writer.WriteLine($" {{ {typeName}.{name}, \"{tag}\"}},"); + writer.WriteLine($" {{ {typeName}.{entry.Name.Replace("_", "")}, \"{entry.Tag}\"}},"); } writer.WriteLine(" };"); @@ -382,15 +401,15 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting writer.WriteLine(); } - private static void WritePropertyValueAlias(TextWriter writer, - IEnumerable<(string name, string tag, string comment)> values, string typeName, string defaultValue) + private static void WritePropertyValueAlias(TextWriter writer, IEnumerable entries, string typeName, + string defaultValue) { writer.WriteLine($" private static readonly Dictionary s_tagTo{typeName} = "); writer.WriteLine($" new Dictionary{{"); - foreach (var (name, tag, comment) in values) + foreach (var entry in entries) { - writer.WriteLine($" {{ \"{tag}\", {typeName}.{name}}},"); + writer.WriteLine($" {{ \"{entry.Tag}\", {typeName}.{entry.Name.Replace("_", "")}}},"); } writer.WriteLine(" };"); @@ -409,4 +428,18 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting writer.WriteLine(); } } + + public readonly struct DataEntry + { + public DataEntry(string name, string tag, string comment) + { + Name = name; + Tag = tag; + Comment = comment; + } + + public string Name { get; } + public string Tag { get; } + public string Comment { get; } + } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs new file mode 100644 index 0000000000..856b4615a5 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs @@ -0,0 +1,229 @@ +using Avalonia.Media.Transformation; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Media +{ + public class TransformOperationsTests + { + [Theory] + [InlineData("translate(10px)", 10d, 0d)] + [InlineData("translate(10px, 10px)", 10d, 10d)] + [InlineData("translate(0px, 10px)", 0d, 10d)] + [InlineData("translate(10px, 0px)", 10d, 0d)] + [InlineData("translateX(10px)", 10d, 0d)] + [InlineData("translateY(10px)", 0d, 10d)] + public void Can_Parse_Translation(string data, double x, double y) + { + var transform = TransformOperations.Parse(data); + + var operations = transform.Operations; + + Assert.Single(operations); + Assert.Equal(TransformOperation.OperationType.Translate, operations[0].Type); + Assert.Equal(x, operations[0].Data.Translate.X); + Assert.Equal(y, operations[0].Data.Translate.Y); + } + + [Theory] + [InlineData("rotate(90deg)", 90d)] + [InlineData("rotate(0.5turn)", 180d)] + [InlineData("rotate(200grad)", 180d)] + [InlineData("rotate(3.14159265rad)", 180d)] + public void Can_Parse_Rotation(string data, double angleDeg) + { + var transform = TransformOperations.Parse(data); + + var operations = transform.Operations; + + Assert.Single(operations); + Assert.Equal(TransformOperation.OperationType.Rotate, operations[0].Type); + Assert.Equal(MathUtilities.Deg2Rad(angleDeg), operations[0].Data.Rotate.Angle, 4); + } + + [Theory] + [InlineData("scale(10)", 10d, 10d)] + [InlineData("scale(10, 10)", 10d, 10d)] + [InlineData("scale(0, 10)", 0d, 10d)] + [InlineData("scale(10, 0)", 10d, 0d)] + [InlineData("scaleX(10)", 10d, 1d)] + [InlineData("scaleY(10)", 1d, 10d)] + public void Can_Parse_Scale(string data, double x, double y) + { + var transform = TransformOperations.Parse(data); + + var operations = transform.Operations; + + Assert.Single(operations); + Assert.Equal(TransformOperation.OperationType.Scale, operations[0].Type); + Assert.Equal(x, operations[0].Data.Scale.X); + Assert.Equal(y, operations[0].Data.Scale.Y); + } + + [Theory] + [InlineData("skew(90deg)", 90d, 0d)] + [InlineData("skew(0.5turn)", 180d, 0d)] + [InlineData("skew(200grad)", 180d, 0d)] + [InlineData("skew(3.14159265rad)", 180d, 0d)] + [InlineData("skewX(90deg)", 90d, 0d)] + [InlineData("skewX(0.5turn)", 180d, 0d)] + [InlineData("skewX(200grad)", 180d, 0d)] + [InlineData("skewX(3.14159265rad)", 180d, 0d)] + [InlineData("skew(0, 90deg)", 0d, 90d)] + [InlineData("skew(0, 0.5turn)", 0d, 180d)] + [InlineData("skew(0, 200grad)", 0d, 180d)] + [InlineData("skew(0, 3.14159265rad)", 0d, 180d)] + [InlineData("skewY(90deg)", 0d, 90d)] + [InlineData("skewY(0.5turn)", 0d, 180d)] + [InlineData("skewY(200grad)", 0d, 180d)] + [InlineData("skewY(3.14159265rad)", 0d, 180d)] + [InlineData("skew(90deg, 90deg)", 90d, 90d)] + [InlineData("skew(0.5turn, 0.5turn)", 180d, 180d)] + [InlineData("skew(200grad, 200grad)", 180d, 180d)] + [InlineData("skew(3.14159265rad, 3.14159265rad)", 180d, 180d)] + public void Can_Parse_Skew(string data, double x, double y) + { + var transform = TransformOperations.Parse(data); + + var operations = transform.Operations; + + Assert.Single(operations); + Assert.Equal(TransformOperation.OperationType.Skew, operations[0].Type); + Assert.Equal(MathUtilities.Deg2Rad(x), operations[0].Data.Skew.X, 4); + Assert.Equal(MathUtilities.Deg2Rad(y), operations[0].Data.Skew.Y, 4); + } + + [Fact] + public void Can_Parse_Compound_Operations() + { + var data = "scale(1,2) translate(3px,4px) rotate(5deg) skew(6deg,7deg)"; + + var transform = TransformOperations.Parse(data); + + var operations = transform.Operations; + + Assert.Equal(TransformOperation.OperationType.Scale, operations[0].Type); + Assert.Equal(1, operations[0].Data.Scale.X); + Assert.Equal(2, operations[0].Data.Scale.Y); + + Assert.Equal(TransformOperation.OperationType.Translate, operations[1].Type); + Assert.Equal(3, operations[1].Data.Translate.X); + Assert.Equal(4, operations[1].Data.Translate.Y); + + Assert.Equal(TransformOperation.OperationType.Rotate, operations[2].Type); + Assert.Equal(MathUtilities.Deg2Rad(5), operations[2].Data.Rotate.Angle); + + Assert.Equal(TransformOperation.OperationType.Skew, operations[3].Type); + Assert.Equal(MathUtilities.Deg2Rad(6), operations[3].Data.Skew.X); + Assert.Equal(MathUtilities.Deg2Rad(7), operations[3].Data.Skew.Y); + } + + [Fact] + public void Can_Parse_Matrix_Operation() + { + var data = "matrix(1,2,3,4,5,6)"; + + var transform = TransformOperations.Parse(data); + + var operations = transform.Operations; + + Assert.Single(operations); + Assert.Equal(TransformOperation.OperationType.Matrix, operations[0].Type); + + var expectedMatrix = new Matrix(1, 2, 3, 4, 5, 6); + + Assert.Equal(expectedMatrix, operations[0].Matrix); + } + + [Theory] + [InlineData(0d, 10d, 0d)] + [InlineData(0.5d, 5d, 10d)] + [InlineData(1d, 0d, 20d)] + public void Can_Interpolate_Translation(double progress, double x, double y) + { + var from = TransformOperations.Parse("translateX(10px)"); + var to = TransformOperations.Parse("translateY(20px)"); + + var interpolated = TransformOperations.Interpolate(from, to, progress); + + var operations = interpolated.Operations; + + Assert.Single(operations); + Assert.Equal(TransformOperation.OperationType.Translate, operations[0].Type); + Assert.Equal(x, operations[0].Data.Translate.X); + Assert.Equal(y, operations[0].Data.Translate.Y); + } + + [Theory] + [InlineData(0d, 10d, 1d)] + [InlineData(0.5d, 5.5d, 10.5d)] + [InlineData(1d, 1d, 20d)] + public void Can_Interpolate_Scale(double progress, double x, double y) + { + var from = TransformOperations.Parse("scaleX(10)"); + var to = TransformOperations.Parse("scaleY(20)"); + + var interpolated = TransformOperations.Interpolate(from, to, progress); + + var operations = interpolated.Operations; + + Assert.Single(operations); + Assert.Equal(TransformOperation.OperationType.Scale, operations[0].Type); + Assert.Equal(x, operations[0].Data.Scale.X); + Assert.Equal(y, operations[0].Data.Scale.Y); + } + + [Theory] + [InlineData(0d, 10d, 0d)] + [InlineData(0.5d, 5d, 10d)] + [InlineData(1d, 0d, 20d)] + public void Can_Interpolate_Skew(double progress, double x, double y) + { + var from = TransformOperations.Parse("skewX(10deg)"); + var to = TransformOperations.Parse("skewY(20deg)"); + + var interpolated = TransformOperations.Interpolate(from, to, progress); + + var operations = interpolated.Operations; + + Assert.Single(operations); + Assert.Equal(TransformOperation.OperationType.Skew, operations[0].Type); + Assert.Equal(MathUtilities.Deg2Rad(x), operations[0].Data.Skew.X); + Assert.Equal(MathUtilities.Deg2Rad(y), operations[0].Data.Skew.Y); + } + + [Theory] + [InlineData(0d, 10d)] + [InlineData(0.5d, 15d)] + [InlineData(1d,20d)] + public void Can_Interpolate_Rotation(double progress, double angle) + { + var from = TransformOperations.Parse("rotate(10deg)"); + var to = TransformOperations.Parse("rotate(20deg)"); + + var interpolated = TransformOperations.Interpolate(from, to, progress); + + var operations = interpolated.Operations; + + Assert.Single(operations); + Assert.Equal(TransformOperation.OperationType.Rotate, operations[0].Type); + Assert.Equal(MathUtilities.Deg2Rad(angle), operations[0].Data.Rotate.Angle); + } + + [Fact] + public void Interpolation_Fallback_To_Matrix() + { + double progress = 0.5d; + + var from = TransformOperations.Parse("rotate(45deg)"); + var to = TransformOperations.Parse("translate(100px, 100px) rotate(1215deg)"); + + var interpolated = TransformOperations.Interpolate(from, to, progress); + + var operations = interpolated.Operations; + + Assert.Single(operations); + Assert.Equal(TransformOperation.OperationType.Matrix, operations[0].Type); + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs index 0e43c76da1..e526172622 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs @@ -9,7 +9,7 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Exception_Should_Be_Thrown_If_FontWeight_LessThanEqualTo_Zero() { - Assert.Throws(() => new Typeface("foo", 0, (FontStyle)12)); + Assert.Throws(() => new Typeface("foo", (FontStyle)12, 0)); } [Fact] diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index 767111b89b..bfcc341eed 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs @@ -557,7 +557,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering } } - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void Should_Create_And_Delete_Layers_For_Controls_With_Animated_Opacity() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -599,7 +599,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering } } - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void Should_Not_Create_Layer_For_Childless_Control_With_Animated_Opacity() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -629,7 +629,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering } } - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void Should_Not_Push_Opacity_For_Transparent_Layer_Root_Control() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -658,7 +658,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering } } - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void Should_Draw_Transparent_Layer_With_Correct_Opacity() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs index 52552f0bee..acee9a50f5 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs @@ -134,7 +134,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering var root = new TestRoot(child); root.Renderer = new ImmediateRenderer(root); - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); root.Measure(new Size(50, 100)); root.Arrange(new Rect(new Size(50, 100))); @@ -171,7 +171,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering var root = new TestRoot(child); root.Renderer = new ImmediateRenderer(root); - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); root.Measure(new Size(300, 100)); root.Arrange(new Rect(new Size(300, 100))); @@ -222,7 +222,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering var root = new TestRoot(rootGrid); root.Renderer = new ImmediateRenderer(root); - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); var rootSize = new Size(RootWidth, RootHeight); root.Measure(rootSize); @@ -277,7 +277,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering var root = new TestRoot(rootGrid); root.Renderer = new ImmediateRenderer(root); - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); var rootSize = new Size(RootWidth, RootHeight); root.Measure(rootSize); diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs index b0f890b484..e219682fa6 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs @@ -653,7 +653,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph }; var layout = tree.LayoutManager; - layout.ExecuteInitialLayoutPass(tree); + layout.ExecuteInitialLayoutPass(); var scene = new Scene(tree); var sceneBuilder = new SceneBuilder(); @@ -696,7 +696,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph }; var layout = tree.LayoutManager; - layout.ExecuteInitialLayoutPass(tree); + layout.ExecuteInitialLayoutPass(); var scene = new Scene(tree); var sceneBuilder = new SceneBuilder(); @@ -744,7 +744,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph }; var layout = tree.LayoutManager; - layout.ExecuteInitialLayoutPass(tree); + layout.ExecuteInitialLayoutPass(); var scene = new Scene(tree); var sceneBuilder = new SceneBuilder(); @@ -810,7 +810,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph }; Assert.Equal(expected, scene.Layers[tree].Dirty.ToArray()); - Assert.Equal(expected, scene.Layers[border].Dirty.ToArray()); + + // Layers are disabled. See #2244 + // Assert.Equal(expected, scene.Layers[border].Dirty.ToArray()); } } diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs index 1bece3ae22..19b4c7e606 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs @@ -14,7 +14,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph { public partial class SceneBuilderTests { - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void Control_With_Animated_Opacity_And_Children_Should_Start_New_Layer() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -40,7 +40,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph }; var layout = tree.LayoutManager; - layout.ExecuteInitialLayoutPass(tree); + layout.ExecuteInitialLayoutPass(); var animation = new BehaviorSubject(0.5); border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); @@ -105,7 +105,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph }; var layout = tree.LayoutManager; - layout.ExecuteInitialLayoutPass(tree); + layout.ExecuteInitialLayoutPass(); var animation = new BehaviorSubject(0.5); border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); @@ -118,7 +118,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } } - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void Removing_Control_With_Animated_Opacity_Should_Remove_Layers() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -147,7 +147,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph }; var layout = tree.LayoutManager; - layout.ExecuteInitialLayoutPass(tree); + layout.ExecuteInitialLayoutPass(); var animation = new BehaviorSubject(0.5); border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); @@ -168,7 +168,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } } - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void Hiding_Transparent_Control_Should_Remove_Layers() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -197,7 +197,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph }; var layout = tree.LayoutManager; - layout.ExecuteInitialLayoutPass(tree); + layout.ExecuteInitialLayoutPass(); var animation = new BehaviorSubject(0.5); border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); @@ -218,7 +218,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } } - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void GeometryClip_Should_Affect_Child_Layers() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -241,7 +241,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph }; var layout = tree.LayoutManager; - layout.ExecuteInitialLayoutPass(tree); + layout.ExecuteInitialLayoutPass(); var animation = new BehaviorSubject(0.5); border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);