diff --git a/.ncrunch/BindingDemo.v3.ncrunchproject b/.ncrunch/BindingDemo.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/BindingDemo.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file 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/.ncrunch/RenderDemo.v3.ncrunchproject b/.ncrunch/RenderDemo.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/RenderDemo.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/VirtualizationDemo.v3.ncrunchproject b/.ncrunch/VirtualizationDemo.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/VirtualizationDemo.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/Avalonia.sln b/Avalonia.sln index 568a16ce0e..3a2c619d5b 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}" @@ -128,6 +126,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\Base.props = build\Base.props build\Binding.props = build\Binding.props build\BuildTargets.targets = build\BuildTargets.targets + build\HarfBuzzSharp.props = build\HarfBuzzSharp.props build\JetBrains.Annotations.props = build\JetBrains.Annotations.props build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props build\Magick.NET-Q16-AnyCPU.props = build\Magick.NET-Q16-AnyCPU.props @@ -139,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 @@ -201,16 +199,22 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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 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 = 4 + 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\RenderHelpers\RenderHelpers.projitems*{7d2d3083-71dd-4cc9-8907-39a0d86fb322}*SharedItemsImports = 4 - tests\Avalonia.RenderTests\Avalonia.RenderTests.projitems*{dabfd304-d6a4-4752-8123-c2ccf7ac7831}*SharedItemsImports = 4 + 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 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -1022,30 +1026,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 @@ -1894,6 +1874,78 @@ Global {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhone.Build.0 = Release|Any CPU {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|iPhone.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|iPhone.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|Any CPU.Build.0 = Release|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhone.ActiveCfg = Release|Any CPU + {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 @@ -1950,6 +2002,8 @@ Global {41B02319-965D-4945-8005-C1A3D1224165} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {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/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..fde2931bf9 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,130 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +steven@avaloniaui.net. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..dcf95ce33c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,69 @@ +# Contributing to Avalonia + +## Before You Start + +Drop into our [gitter chat room](https://gitter.im/AvaloniaUI/Avalonia) and let us know what you're thinking of doing. We might be able to give you guidance or let you know if someone else is already working on the feature. + +## Style + +The codebase uses [.net core](https://github.com/dotnet/runtime/blob/master/docs/coding-guidelines/coding-style.md) coding style. + +Try to keep lines of code around 100 characters in length or less, though this is not a hard limit. +If you're a few characters over then don't worry too much. + +**DO NOT USE #REGIONS** full stop. + +## Pull requests + +A single pull request should be submitted for each change. If you're making more than one change, +please submit separate pull requests for each change for easy review. Rebase your changes to make +sense, so a history that looks like: + +* Add class A +* Feature A didn't set Foo when Bar was set +* Fix spacing +* Add class B +* Sort using statements + +Should be rebased to read: + +* Add class A +* Add class B + +Again, this makes review much easier. + +Please try not to submit pull requests that don't add new features (e.g. moving stuff around) +unless you see something that is obviously wrong or that could be written in a more terse or +idiomatic style. It takes time to review each pull request - time that I'd prefer to spend writing +new features! + +Prefer terseness to verbosity but don't try to be too clever. + +## Tests + +There are two types of tests currently in the codebase; unit tests and render tests. + +Unit tests should be contained in a class name that mirrors the class being tested with the suffix +-Tests, e.g. + + Avalonia.Controls.UnitTests.Presenters.TextPresenterTests + +Where Avalonia.Controls.UnitTests is the name of the project. + +Unit test methods should be named in a sentence style, separated by underscores, that describes in +English what the test is testing, e.g. + +```csharp + void Calling_Foo_Should_Increment_Bar() +``` + +Render tests should describe what the produced image is: + +```csharp + void Rectangle_2px_Stroke_Filled() +``` + +## Code of Conduct + +This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community. +For more information see the [Contributor Covenant Code of Conduct](https://dotnetfoundation.org/code-of-conduct) diff --git a/Documentation/build.md b/Documentation/build.md new file mode 100644 index 0000000000..8c2ef57b54 --- /dev/null +++ b/Documentation/build.md @@ -0,0 +1,79 @@ +# Windows + +Avalonia requires at least Visual Studio 2019 and .NET Core SDK 3.1 to build on Windows. + +### Clone the Avalonia repository + +``` +git clone https://github.com/AvaloniaUI/Avalonia.git +git submodule update --init +``` + +### Open in Visual Studio + +Open the `Avalonia.sln` solution in Visual Studio 2019 or newer. The free Visual Studio Community +edition works fine. Run the `Samples\ControlCatalog.Desktop` project to see the sample application. + +# Linux/macOS + +It's *not* possible to build the *whole* project on Linux/macOS. You can only build the subset targeting .NET Standard and .NET Core (which is, however, sufficient to get UI working on Linux/macOS). If you want to something that involves changing platform-specific APIs you'll need a Windows machine. + +MonoDevelop, Xamarin Studio and Visual Studio for Mac aren't capable of properly opening our solution. You can use Rider (at least 2017.2 EAP) or VSCode instead. They will fail to load most of platform specific projects, but you don't need them to run on .NET Core. + +### Install the latest version of .NET Core + +Go to https://www.microsoft.com/net/core and follow instructions for your OS. You need SDK (not just "runtime") package. + +### Additional requirements for macOS + +The build process needs [Xcode](https://developer.apple.com/xcode/) to build the native library. Following the install instructions at the [Xcode](https://developer.apple.com/xcode/) website to properly install. + +Linux operating systems ship with their own respective package managers however we will use [Homebrew](https://brew.sh/) to manage packages on macOS. To install follow the instructions [here](https://docs.brew.sh/Installation). + +### Install CastXML + +Avalonia requires [CastXML](https://github.com/CastXML/CastXML) for XML processing during the build process. The easiest way to install this is via the operating system's package managers, such as below. + +On macOS: +``` +brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb +``` + +On Debian based Linux (Debian, Ubuntu, Mint, etc): +``` +sudo apt install castxml +``` + +On Red Hat based Linux (Fedora, CentOS, RHEL, etc) using `yum` (`dnf` takes same arguments though): +``` +sudo yum install castxml +``` + + +### Clone the Avalonia repository + +``` +git clone https://github.com/AvaloniaUI/Avalonia.git +cd Avalonia +git submodule update --init --recursive +``` + +### Build native libraries (macOS only) + +On macOS it is necessary to build and manually install the respective native libraries using [Xcode](https://developer.apple.com/xcode/). The steps to get this working correctly are: +- Navigate to the Avalonia/native/Avalonia.Native/src/OSX folder and open the `Avalonia.Native.OSX.xcodeproj` project +- Build the library via the Product->Build menu. This will generate binaries in your local path under ~/Library/Developer/Xcode/DerivedData/Avalonia.Native.OSX-*guid* where "guid" is uniquely generated every time you build. +- Manually install the native library by copying it from the build artifacts folder into the shared dynamic library path: + +``` +cd ~/Library/Developer/Xcode/DerivedData/Avalonia.Native.OSX-[guid]/Build/Products/Debug +cp libAvalonia.Native.OSX.dylib /usr/local/lib/libAvaloniaNative.dylib +``` + +### Build and Run Avalonia + +``` +cd samples/ControlCatalog.NetCore +dotnet restore +dotnet run +``` diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 0000000000..92fd725957 --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,305 @@ +# WPF + +https://github.com/dotnet/wpf + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +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. + +# SharpDX + +https://github.com/sharpdx/SharpDX + +Copyright (c) 2010-2014 SharpDX - Alexandre Mutel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +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. + +# Silverlight Toolkit + +https://github.com/microsoftarchive/SilverlightToolkit + +Microsoft Public License (MS-PL) + +This license governs use of the accompanying software. If you use the software, you +accept this license. If you do not accept the license, do not use the software. + +1. Definitions +The terms "reproduce," "reproduction," "derivative works," and "distribution" have the +same meaning here as under U.S. copyright law. +A "contribution" is the original software, or any additions or changes to the software. +A "contributor" is any person that distributes its contribution under this license. +"Licensed patents" are a contributor's patent claims that read directly on its contribution. + +2. Grant of Rights +(A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. +(B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. + +3. Conditions and Limitations +(A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. +(B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically. +(C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software. +(D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license. +(E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement. + +# wayland-protocols + +https://github.com/wayland-project/wayland-protocols + +Copyright 2008-2013 Kristian Hgsberg +Copyright 2010-2013 Intel Corporation +Copyright 2013 Rafael Antognolli +Copyright 2013 Jasper St. Pierre +Copyright 2014 Jonas dahl +Copyright 2014 Jason Ekstrand +Copyright 2014-2015 Collabora, Ltd. +Copyright 2015 Red Hat Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE 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. + +# Metsys.Bson + +Copyright (c) 2010, Karl Seguin - http://www.openmymind.net/ +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 the 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 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. + +# RichTextKit + +https://github.com/toptensoftware/RichTextKit + +Copyright 2019 Topten Software. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this product except in compliance with the License. You may obtain +a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. + +# Mono + +https://github.com/mono/mono + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE 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. + +# Collections.Pooled + +https://github.com/jtmueller/Collections.Pooled + +The MIT License (MIT) + +Copyright (c) Joel Mueller + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +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. + +# EllipticalArc.java + +http://www.spaceroots.org/documents/ellipse/EllipticalArc.java +http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf + +Copyright (c) 2003-2004, Luc Maisonobe +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 names of spaceroots.org, spaceroots.com + nor the names of their 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. + +# WinUI + +https://github.com/microsoft/microsoft-ui-xaml + +MIT License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +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/azure-pipelines.yml b/azure-pipelines.yml index 92e4afdca8..54645e461e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -14,7 +14,7 @@ jobs: displayName: 'Install Nuke' inputs: script: | - dotnet tool install --global Nuke.GlobalTool --version 0.12.3 + dotnet tool install --global Nuke.GlobalTool --version 0.24.0 - task: CmdLine@2 displayName: 'Run Nuke' inputs: @@ -34,9 +34,17 @@ jobs: pool: vmImage: 'macOS-10.14' steps: - - task: DotNetCoreInstaller@0 + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 3.1.101' inputs: - version: '2.1.403' + packageType: sdk + version: 3.1.101 + + - task: UseDotNet@2 + displayName: 'Use .NET Core Runtime 3.1.1' + inputs: + packageType: runtime + version: 3.1.1 - task: CmdLine@2 displayName: 'Install Mono 5.18' @@ -52,7 +60,7 @@ jobs: sdk: 'macosx10.14' configuration: 'Release' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' - xcodeVersion: 'default' # Options: 8, 9, default, specifyPath + xcodeVersion: '10' # Options: 8, 9, default, specifyPath args: '-derivedDataPath ./' - task: CmdLine@2 @@ -60,13 +68,13 @@ jobs: inputs: script: | brew update - brew install castxml + brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb - task: CmdLine@2 displayName: 'Install Nuke' inputs: script: | - dotnet tool install --global Nuke.GlobalTool --version 0.12.3 + dotnet tool install --global Nuke.GlobalTool --version 0.24.0 - task: CmdLine@2 displayName: 'Run Nuke' @@ -108,7 +116,7 @@ jobs: displayName: 'Install Nuke' inputs: script: | - dotnet tool install --global Nuke.GlobalTool --version 0.12.3 + dotnet tool install --global Nuke.GlobalTool --version 0.24.0 - task: CmdLine@2 displayName: 'Run Nuke' 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/Base.props b/build/Base.props index a60373ebb3..100c9088cd 100644 --- a/build/Base.props +++ b/build/Base.props @@ -1,5 +1,6 @@  + diff --git a/build/BuildTargets.targets b/build/BuildTargets.targets index 08ec039ec7..a5543cd050 100644 --- a/build/BuildTargets.targets +++ b/build/BuildTargets.targets @@ -2,6 +2,7 @@ $(MSBuildThisFileDirectory)\..\src\Avalonia.Build.Tasks\bin\$(Configuration)\netstandard2.0\Avalonia.Build.Tasks.dll true + true diff --git a/build/CoreLibraries.props b/build/CoreLibraries.props index 3923bdeeda..d17eec0135 100644 --- a/build/CoreLibraries.props +++ b/build/CoreLibraries.props @@ -4,14 +4,13 @@ - - + diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props new file mode 100644 index 0000000000..873048ef21 --- /dev/null +++ b/build/HarfBuzzSharp.props @@ -0,0 +1,6 @@ + + + + + + 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/Rx.props b/build/Rx.props index edff0af160..8a15ccd6a9 100644 --- a/build/Rx.props +++ b/build/Rx.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/SharedVersion.props b/build/SharedVersion.props index 44d5c239ef..bd183faab3 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -2,8 +2,8 @@ xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> Avalonia - 0.8.999 - Copyright 2019 © The AvaloniaUI Project + 0.9.999 + Copyright 2020 © The AvaloniaUI Project https://avaloniaui.net https://github.com/AvaloniaUI/Avalonia/ true diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index c03ad0fefd..4def44cbd0 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - - + + 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/global.json b/global.json index 1e599211d4..a3fdca9bed 100644 --- a/global.json +++ b/global.json @@ -1,4 +1,7 @@ { + "sdk": { + "version": "3.1.101" + }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", "MSBuild.Sdk.Extras": "2.0.46", diff --git a/licence.md b/licence.md index d730e936db..d18aef99ad 100644 --- a/licence.md +++ b/licence.md @@ -1,6 +1,7 @@ The MIT License (MIT) -Copyright (c) 2014 Steven Kirk +Copyright (c) .NET Foundation and Contributors +All Rights Reserved Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +19,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 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. \ No newline at end of file +SOFTWARE. diff --git a/native/Avalonia.Native/inc/avalonia-native-guids.h b/native/Avalonia.Native/inc/avalonia-native-guids.h index 439008fd4b..64fec729a2 100644 --- a/native/Avalonia.Native/inc/avalonia-native-guids.h +++ b/native/Avalonia.Native/inc/avalonia-native-guids.h @@ -1,5 +1,2 @@ -// 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. - #define COM_GUIDS_MATERIALIZE #include "avalonia-native.h" diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index f1c7664c3e..1cf3bc75b0 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -1,8 +1,6 @@ -// 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. - #include "com.h" #include "key.h" +#include "stddef.h" #define AVNCOM(name, id) COMINTERFACE(name, 2e2cda0a, 9ae5, 4f1b, 8e, 20, 08, 1a, 04, 27, 9f, id) @@ -22,8 +20,19 @@ struct IAvnGlContext; struct IAvnGlDisplay; struct IAvnGlSurfaceRenderTarget; struct IAvnGlSurfaceRenderingSession; -struct IAvnAppMenu; -struct IAvnAppMenuItem; +struct IAvnMenu; +struct IAvnMenuItem; +struct IAvnStringArray; +struct IAvnDndResultCallback; +struct IAvnGCHandleDeallocatorCallback; +struct IAvnMenuEvents; +struct IAvnNativeControlHost; +struct IAvnNativeControlHostTopLevelAttachment; +enum SystemDecorations { + SystemDecorationsNone = 0, + SystemDecorationsBorderOnly = 1, + SystemDecorationsFull = 2, +}; struct AvnSize { @@ -92,9 +101,17 @@ enum AvnRawMouseEventType RightButtonUp, MiddleButtonDown, MiddleButtonUp, + XButton1Down, + XButton1Up, + XButton2Down, + XButton2Up, Move, Wheel, - NonClientLeftButtonDown + NonClientLeftButtonDown, + TouchBegin, + TouchUpdate, + TouchEnd, + TouchCancel }; enum AvnRawKeyEventType @@ -112,7 +129,25 @@ enum AvnInputModifiers Windows = 8, LeftMouseButton = 16, RightMouseButton = 32, - MiddleMouseButton = 64 + MiddleMouseButton = 64, + XButton1MouseButton = 128, + XButton2MouseButton = 256 +}; + +enum class AvnDragDropEffects +{ + None = 0, + Copy = 1, + Move = 2, + Link = 4, +}; + +enum class AvnDragEventType +{ + Enter, + Over, + Leave, + Drop }; enum AvnWindowState @@ -120,6 +155,7 @@ enum AvnWindowState Normal, Minimized, Maximized, + FullScreen, }; enum AvnStandardCursorType @@ -162,24 +198,31 @@ enum AvnWindowEdge WindowEdgeSouthEast }; +enum AvnMenuItemToggleType +{ + None, + CheckMark, + Radio +}; + AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown { public: - virtual HRESULT Initialize() = 0; + virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator) = 0; virtual IAvnMacOptions* GetMacOptions() = 0; - virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnWindow** ppv) = 0; - virtual HRESULT CreatePopup (IAvnWindowEvents* cb, IAvnPopup** ppv) = 0; + virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv) = 0; + virtual HRESULT CreatePopup (IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv) = 0; virtual HRESULT CreatePlatformThreadingInterface(IAvnPlatformThreadingInterface** ppv) = 0; virtual HRESULT CreateSystemDialogs (IAvnSystemDialogs** ppv) = 0; virtual HRESULT CreateScreens (IAvnScreens** ppv) = 0; virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0; + virtual HRESULT CreateDndClipboard(IAvnClipboard** ppv) = 0; virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0; - virtual HRESULT ObtainGlFeature(IAvnGlFeature** ppv) = 0; - virtual HRESULT ObtainAppMenu(IAvnAppMenu** retOut) = 0; - virtual HRESULT SetAppMenu(IAvnAppMenu* menu) = 0; - virtual HRESULT CreateMenu (IAvnAppMenu** ppv) = 0; - virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) = 0; - virtual HRESULT CreateMenuItemSeperator (IAvnAppMenuItem** ppv) = 0; + virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) = 0; + virtual HRESULT SetAppMenu(IAvnMenu* menu) = 0; + virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) = 0; + virtual HRESULT CreateMenuItem (IAvnMenuItem** ppv) = 0; + virtual HRESULT CreateMenuItemSeperator (IAvnMenuItem** ppv) = 0; }; AVNCOM(IAvnString, 17) : IUnknown @@ -209,11 +252,15 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown virtual HRESULT SetTopMost (bool value) = 0; virtual HRESULT SetCursor(IAvnCursor* cursor) = 0; virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ret) = 0; - virtual HRESULT GetSoftwareFramebuffer(AvnFramebuffer*ret) = 0; - virtual HRESULT SetMainMenu(IAvnAppMenu* menu) = 0; - virtual HRESULT ObtainMainMenu(IAvnAppMenu** retOut) = 0; - virtual bool TryLock() = 0; - virtual void Unlock() = 0; + virtual HRESULT SetMainMenu(IAvnMenu* menu) = 0; + virtual HRESULT ObtainNSWindowHandle(void** retOut) = 0; + 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; }; AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase @@ -223,9 +270,10 @@ AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase { - virtual HRESULT ShowDialog (IAvnWindow* parent) = 0; + virtual HRESULT SetEnabled (bool enable) = 0; + virtual HRESULT SetParent (IAvnWindow* parent) = 0; virtual HRESULT SetCanResize(bool value) = 0; - virtual HRESULT SetHasDecorations(bool value) = 0; + virtual HRESULT SetDecorations(SystemDecorations value) = 0; virtual HRESULT SetTitle (void* utf8Title) = 0; virtual HRESULT SetTitleBarColor (AvnColor color) = 0; virtual HRESULT SetWindowState(AvnWindowState state) = 0; @@ -249,6 +297,10 @@ 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; }; @@ -262,6 +314,8 @@ AVNCOM(IAvnWindowEvents, 06) : IAvnWindowBaseEvents virtual bool Closing () = 0; virtual void WindowStateChanged (AvnWindowState state) = 0; + + virtual void GotInputWhenDisabled () = 0; }; AVNCOM(IAvnMacOptions, 07) : IUnknown @@ -332,8 +386,13 @@ AVNCOM(IAvnScreens, 0e) : IUnknown AVNCOM(IAvnClipboard, 0f) : IUnknown { - virtual HRESULT GetText (IAvnString**ppv) = 0; - virtual HRESULT SetText (void* utf8Text) = 0; + virtual HRESULT GetText (char* type, IAvnString**ppv) = 0; + 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; }; @@ -346,24 +405,21 @@ AVNCOM(IAvnCursorFactory, 11) : IUnknown virtual HRESULT GetCursor (AvnStandardCursorType cursorType, IAvnCursor** retOut) = 0; }; - -AVNCOM(IAvnGlFeature, 12) : IUnknown -{ - virtual HRESULT ObtainDisplay(IAvnGlDisplay**retOut) = 0; - virtual HRESULT ObtainImmediateContext(IAvnGlContext**retOut) = 0; -}; - AVNCOM(IAvnGlDisplay, 13) : IUnknown { - virtual HRESULT GetSampleCount(int* ret) = 0; - virtual HRESULT GetStencilSize(int* ret) = 0; - virtual HRESULT ClearContext() = 0; + virtual HRESULT CreateContext(IAvnGlContext* share, IAvnGlContext**ppv) = 0; + virtual void LegacyClearCurrentContext() = 0; + virtual HRESULT WrapContext(void* native, IAvnGlContext**ppv) = 0; virtual void* GetProcAddress(char* proc) = 0; }; AVNCOM(IAvnGlContext, 14) : IUnknown { - virtual HRESULT MakeCurrent() = 0; + virtual HRESULT MakeCurrent(IUnknown** ppv) = 0; + virtual HRESULT LegacyMakeCurrent() = 0; + virtual int GetSampleCount() = 0; + virtual int GetStencilSize() = 0; + virtual void* GetNativeHandle() = 0; }; AVNCOM(IAvnGlSurfaceRenderTarget, 15) : IUnknown @@ -377,10 +433,10 @@ AVNCOM(IAvnGlSurfaceRenderingSession, 16) : IUnknown virtual HRESULT GetScaling(double* ret) = 0; }; -AVNCOM(IAvnAppMenu, 17) : IUnknown +AVNCOM(IAvnMenu, 17) : IUnknown { - virtual HRESULT AddItem (IAvnAppMenuItem* item) = 0; - virtual HRESULT RemoveItem (IAvnAppMenuItem* item) = 0; + virtual HRESULT InsertItem (int index, IAvnMenuItem* item) = 0; + virtual HRESULT RemoveItem (IAvnMenuItem* item) = 0; virtual HRESULT SetTitle (void* utf8String) = 0; virtual HRESULT Clear () = 0; }; @@ -390,12 +446,57 @@ AVNCOM(IAvnPredicateCallback, 18) : IUnknown virtual bool Evaluate() = 0; }; -AVNCOM(IAvnAppMenuItem, 19) : IUnknown +AVNCOM(IAvnMenuItem, 19) : IUnknown { - virtual HRESULT SetSubMenu (IAvnAppMenu* menu) = 0; + virtual HRESULT SetSubMenu (IAvnMenu* menu) = 0; virtual HRESULT SetTitle (void* utf8String) = 0; virtual HRESULT SetGesture (void* utf8String, AvnInputModifiers modifiers) = 0; virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) = 0; + virtual HRESULT SetIsChecked (bool isChecked) = 0; + virtual HRESULT SetToggleType (AvnMenuItemToggleType toggleType) = 0; + virtual HRESULT SetIcon (void* data, size_t length) = 0; +}; + +AVNCOM(IAvnMenuEvents, 1A) : IUnknown +{ + /** + * NeedsUpdate + */ + virtual void NeedsUpdate () = 0; +}; + +AVNCOM(IAvnStringArray, 20) : IUnknown +{ + virtual unsigned int GetCount() = 0; + virtual HRESULT Get(unsigned int index, IAvnString**ppv) = 0; }; +AVNCOM(IAvnDndResultCallback, 21) : IUnknown +{ + virtual void OnDragAndDropComplete(AvnDragDropEffects effecct) = 0; +}; + +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 MoveTo(float x, float y, float width, float height) = 0; + virtual void Hide() = 0; + virtual void ReleaseChild() = 0; +}; + + extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative(); diff --git a/native/Avalonia.Native/inc/com.h b/native/Avalonia.Native/inc/com.h index 22fb4a11a3..df251514ef 100644 --- a/native/Avalonia.Native/inc/com.h +++ b/native/Avalonia.Native/inc/com.h @@ -1,6 +1,3 @@ -// 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. - #pragma clang diagnostic push #pragma ide diagnostic ignored "OCUnusedGlobalDeclarationInspection" #ifndef COM_H_INCLUDED diff --git a/native/Avalonia.Native/inc/comimpl.h b/native/Avalonia.Native/inc/comimpl.h index cf1aa4c735..0ff64b7215 100644 --- a/native/Avalonia.Native/inc/comimpl.h +++ b/native/Avalonia.Native/inc/comimpl.h @@ -162,6 +162,19 @@ public: return _obj; } + TInterface* getRetainedReference() + { + if(_obj == NULL) + return NULL; + _obj->AddRef(); + return _obj; + } + + TInterface** getPPV() + { + return &_obj; + } + operator TInterface*() const { return _obj; diff --git a/native/Avalonia.Native/inc/key.h b/native/Avalonia.Native/inc/key.h index cdc9658e29..12d283cc17 100644 --- a/native/Avalonia.Native/inc/key.h +++ b/native/Avalonia.Native/inc/key.h @@ -1,6 +1,3 @@ -// 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. - #ifndef _KEY_H_ #define _KEY_H_ diff --git a/native/Avalonia.Native/inc/rendertarget.h b/native/Avalonia.Native/inc/rendertarget.h new file mode 100644 index 0000000000..2b0338d099 --- /dev/null +++ b/native/Avalonia.Native/inc/rendertarget.h @@ -0,0 +1,12 @@ + +@protocol IRenderTarget +-(void) setNewLayer: (CALayer*) layer; +-(HRESULT) setSwFrame: (AvnFramebuffer*) fb; +-(void) resize: (AvnPixelSize) size withScale: (float) scale; +-(AvnPixelSize) pixelSize; +-(IAvnGlSurfaceRenderTarget*) createSurfaceRenderTarget; +@end + +@interface IOSurfaceRenderTarget : NSObject +-(IOSurfaceRenderTarget*) initWithOpenGlContext: (IAvnGlContext*) context; +@end 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 c0a49382a7..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 @@ -8,6 +8,13 @@ /* Begin PBXBuildFile section */ 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 */; }; 37155CE4233C00EB0034DCE9 /* menu.h in Headers */ = {isa = PBXBuildFile; fileRef = 37155CE3233C00EB0034DCE9 /* menu.h */; }; 37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; }; 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; }; @@ -18,7 +25,6 @@ 5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; }; AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; }; AB1E522C217613570091CD71 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB1E522B217613570091CD71 /* OpenGL.framework */; }; - AB573DC4217605E400D389A2 /* gl.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB573DC3217605E400D389A2 /* gl.mm */; }; AB661C1E2148230F00291242 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB661C1D2148230F00291242 /* AppKit.framework */; }; AB661C202148286E00291242 /* window.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB661C1F2148286E00291242 /* window.mm */; }; AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */; }; @@ -26,6 +32,13 @@ /* Begin PBXFileReference section */ 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 = ""; }; 37155CE3233C00EB0034DCE9 /* menu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = menu.h; sourceTree = ""; }; 379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = ""; }; 37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = ""; }; @@ -41,7 +54,6 @@ 5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = ""; }; AB00E4F62147CA920032A60A /* main.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = ""; }; AB1E522B217613570091CD71 /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = System/Library/Frameworks/OpenGL.framework; sourceTree = SDKROOT; }; - AB573DC3217605E400D389A2 /* gl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = gl.mm; sourceTree = ""; }; AB661C1D2148230F00291242 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; AB661C1F2148286E00291242 /* window.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = window.mm; sourceTree = ""; }; AB661C212148288600291242 /* common.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = common.h; sourceTree = ""; }; @@ -54,6 +66,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */, + 1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */, AB1E522C217613570091CD71 /* OpenGL.framework in Frameworks */, AB661C1E2148230F00291242 /* AppKit.framework in Frameworks */, ); @@ -65,6 +79,8 @@ AB661C1C2148230E00291242 /* Frameworks */ = { isa = PBXGroup; children = ( + 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */, + 1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */, AB1E522B217613570091CD71 /* OpenGL.framework */, AB661C1D2148230F00291242 /* AppKit.framework */, ); @@ -74,14 +90,17 @@ AB7A61E62147C814003C5833 = { isa = PBXGroup; children = ( + 1A1852DB23E05814008F0DED /* deadlock.mm */, 1A002B9D232135EE00021753 /* app.mm */, 37DDA9B121933371002E132B /* AvnString.h */, 37DDA9AF219330F8002E132B /* AvnString.mm */, 37A4E71A2178846A00EACBCD /* headers */, - AB573DC3217605E400D389A2 /* gl.mm */, + 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */, + 1AFD334023E03C4F0042899B /* controlhost.mm */, 5BF943652167AD1D009CAE35 /* cursor.h */, 5B21A981216530F500CEE36E /* cursor.mm */, 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */, + 1A465D0F246AB61600C5858B /* dnd.mm */, AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */, AB661C212148288600291242 /* common.h */, 379860FE214DA0C000CD0246 /* KeyTransform.h */, @@ -91,6 +110,7 @@ AB00E4F62147CA920032A60A /* main.mm */, 37155CE3233C00EB0034DCE9 /* menu.h */, 520624B222973F4100C4DCEF /* menu.mm */, + 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */, 37A517B22159597E00FBA241 /* Screens.mm */, 37C09D8721580FE4006A6758 /* SystemDialogs.mm */, AB7A61F02147C815003C5833 /* Products */, @@ -177,15 +197,19 @@ 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 */, + 1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */, + 1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */, 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 */, - AB573DC4217605E400D389A2 /* gl.mm in Sources */, AB661C202148286E00291242 /* window.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme index 1a665d3ea5..5d20a135b9 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme @@ -29,8 +29,6 @@ shouldUseLaunchSchemeArgsEnv = "YES"> - - - - * 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 b62fe8a968..00b748ef63 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -7,6 +7,7 @@ // #include "common.h" +#include class AvnStringImpl : public virtual ComSingleObject { @@ -28,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); @@ -61,7 +69,60 @@ public: } }; +class AvnStringArrayImpl : public virtual ComSingleObject +{ +private: + std::vector> _list; +public: + FORWARD_IUNKNOWN() + AvnStringArrayImpl(NSArray* array) + { + for(int c = 0; c < [array count]; c++) + { + ComPtr s; + *s.getPPV() = new AvnStringImpl([array objectAtIndex:c]); + _list.push_back(s); + } + } + + AvnStringArrayImpl(NSString* string) + { + ComPtr s; + *s.getPPV() = new AvnStringImpl(string); + _list.push_back(s); + } + + virtual unsigned int GetCount() override + { + return (unsigned int)_list.size(); + } + + virtual HRESULT Get(unsigned int index, IAvnString**ppv) override + { + if(_list.size() <= index) + return E_INVALIDARG; + *ppv = _list[index].getRetainedReference(); + return S_OK; + } +}; + IAvnString* CreateAvnString(NSString* string) { return new AvnStringImpl(string); } + + +IAvnStringArray* CreateAvnStringArray(NSArray * array) +{ + return new AvnStringArrayImpl(array); +} + +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/KeyTransform.h b/native/Avalonia.Native/src/OSX/KeyTransform.h index c4466020c3..ea4fbecd5c 100644 --- a/native/Avalonia.Native/src/OSX/KeyTransform.h +++ b/native/Avalonia.Native/src/OSX/KeyTransform.h @@ -1,6 +1,3 @@ -// 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. - #ifndef keytransform_h #define keytransform_h #include "common.h" diff --git a/native/Avalonia.Native/src/OSX/KeyTransform.mm b/native/Avalonia.Native/src/OSX/KeyTransform.mm index 971bcf24f8..ff1bf6b1af 100644 --- a/native/Avalonia.Native/src/OSX/KeyTransform.mm +++ b/native/Avalonia.Native/src/OSX/KeyTransform.mm @@ -1,6 +1,3 @@ -// 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. - #include "KeyTransform.h" const int kVK_ANSI_A = 0x00; diff --git a/native/Avalonia.Native/src/OSX/Screens.mm b/native/Avalonia.Native/src/OSX/Screens.mm index e7f009787a..278daf9a18 100644 --- a/native/Avalonia.Native/src/OSX/Screens.mm +++ b/native/Avalonia.Native/src/OSX/Screens.mm @@ -1,6 +1,3 @@ -// 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. - #include "common.h" class Screens : public ComSingleObject diff --git a/native/Avalonia.Native/src/OSX/SystemDialogs.mm b/native/Avalonia.Native/src/OSX/SystemDialogs.mm index 567dd7f747..a47221056b 100644 --- a/native/Avalonia.Native/src/OSX/SystemDialogs.mm +++ b/native/Avalonia.Native/src/OSX/SystemDialogs.mm @@ -1,6 +1,3 @@ -// 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. - #include "common.h" #include "window.h" @@ -23,6 +20,7 @@ public: if(title != nullptr) { + panel.message = [NSString stringWithUTF8String:title]; panel.title = [NSString stringWithUTF8String:title]; } @@ -97,6 +95,7 @@ public: if(title != nullptr) { + panel.message = [NSString stringWithUTF8String:title]; panel.title = [NSString stringWithUTF8String:title]; } @@ -185,6 +184,7 @@ public: if(title != nullptr) { + panel.message = [NSString stringWithUTF8String:title]; panel.title = [NSString stringWithUTF8String:title]; } diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 81855995b7..814b91cb62 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -1,24 +1,71 @@ #include "common.h" @interface AvnAppDelegate : NSObject @end -extern NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular; + +NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular; + @implementation AvnAppDelegate - (void)applicationWillFinishLaunching:(NSNotification *)notification { - [[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy]; + if([[NSApplication sharedApplication] activationPolicy] != AvnDesiredActivationPolicy) + { + for (NSRunningApplication * app in [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.dock"]) { + [app activateWithOptions:NSApplicationActivateIgnoringOtherApps]; + break; + } + + [[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy]; + + [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"NSFullScreenMenuItemEverywhere"]; + + [[NSApplication sharedApplication] setHelpMenu: [[NSMenu new] initWithTitle:@""]]; + } } - (void)applicationDidFinishLaunching:(NSNotification *)notification { - [NSApp activateIgnoringOtherApps:true]; + [[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps]; +} + +@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/cgl.mm b/native/Avalonia.Native/src/OSX/cgl.mm new file mode 100644 index 0000000000..a9d94cdf04 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/cgl.mm @@ -0,0 +1,166 @@ +#include "common.h" +#include + +static CGLContextObj CreateCglContext(CGLContextObj share) +{ + int attributes[] = { + kCGLPFAAccelerated, + kCGLPFAOpenGLProfile, (CGLPixelFormatAttribute)kCGLOGLPVersion_3_2_Core, + kCGLPFADepthSize, 8, + kCGLPFAStencilSize, 8, + kCGLPFAColorSize, 32, + 0 + }; + + CGLPixelFormatObj pix; + CGLError errorCode; + GLint num; // stores the number of possible pixel formats + errorCode = CGLChoosePixelFormat( (CGLPixelFormatAttribute*)attributes, &pix, &num ); + if(errorCode != 0) + return nil; + CGLContextObj ctx = nil; + errorCode = CGLCreateContext(pix, share, &ctx ); + CGLDestroyPixelFormat( pix ); + if(errorCode != 0) + return nil; + return ctx; +}; + + + +class AvnGlContext : public virtual ComSingleObject +{ + // Debug + int _usageCount = 0; +public: + CGLContextObj Context; + int SampleCount = 0, StencilBits = 0; + FORWARD_IUNKNOWN() + + class SavedGlContext : public virtual ComUnknownObject + { + CGLContextObj _savedContext; + ComPtr _parent; + public: + SavedGlContext(CGLContextObj saved, AvnGlContext* parent) + { + _savedContext = saved; + _parent = parent; + _parent->_usageCount++; + } + + ~SavedGlContext() + { + if(_parent->Context == CGLGetCurrentContext()) + CGLSetCurrentContext(_savedContext); + _parent->_usageCount--; + CGLUnlockContext(_parent->Context); + } + }; + + AvnGlContext(CGLContextObj context) + { + Context = context; + CGLPixelFormatObj fmt = CGLGetPixelFormat(context); + CGLDescribePixelFormat(fmt, 0, kCGLPFASamples, &SampleCount); + CGLDescribePixelFormat(fmt, 0, kCGLPFAStencilSize, &StencilBits); + + } + + virtual HRESULT LegacyMakeCurrent() override + { + if(CGLSetCurrentContext(Context) != 0) + return E_FAIL; + return S_OK; + } + + virtual HRESULT MakeCurrent(IUnknown** ppv) override + { + CGLContextObj saved = CGLGetCurrentContext(); + CGLLockContext(Context); + if(CGLSetCurrentContext(Context) != 0) + { + CGLUnlockContext(Context); + return E_FAIL; + } + *ppv = new SavedGlContext(saved, this); + + return S_OK; + } + + virtual int GetSampleCount() override + { + return SampleCount; + } + + virtual int GetStencilSize() override + { + return StencilBits; + } + + virtual void* GetNativeHandle() override + { + return Context; + } + + ~AvnGlContext() + { + CGLReleaseContext(Context); + } +}; + +class AvnGlDisplay : public virtual ComSingleObject +{ + void* _libgl; + +public: + FORWARD_IUNKNOWN() + + AvnGlDisplay() + { + _libgl = dlopen("/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib", RTLD_LAZY); + } + + virtual void* GetProcAddress(char* proc) override + { + return dlsym(_libgl, proc); + } + + virtual HRESULT CreateContext(IAvnGlContext* share, IAvnGlContext**ppv) override + { + CGLContextObj shareContext = nil; + if(share != nil) + { + AvnGlContext* shareCtx = dynamic_cast(share); + if(shareCtx != nil) + shareContext = shareCtx->Context; + } + CGLContextObj ctx = ::CreateCglContext(shareContext); + if(ctx == nil) + return E_FAIL; + *ppv = new AvnGlContext(ctx); + return S_OK; + } + + virtual HRESULT WrapContext(void* native, IAvnGlContext**ppv) override + { + if(native == nil) + return E_INVALIDARG; + *ppv = new AvnGlContext((CGLContextObj) native); + return S_OK; + } + + virtual void LegacyClearCurrentContext() override + { + CGLSetCurrentContext(nil); + } +}; + +static IAvnGlDisplay* GlDisplay = new AvnGlDisplay(); + + +extern IAvnGlDisplay* GetGlDisplay() +{ + return GlDisplay; +}; + diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm index 6e4d3ce668..116a08670e 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/native/Avalonia.Native/src/OSX/clipboard.mm @@ -1,21 +1,29 @@ -// 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. - #include "common.h" #include "AvnString.h" class Clipboard : public ComSingleObject { +private: + NSPasteboard* _pb; + NSPasteboardItem* _item; public: FORWARD_IUNKNOWN() - Clipboard() + Clipboard(NSPasteboard* pasteboard, NSPasteboardItem* item) { - NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; - [pasteBoard stringForType:NSPasteboardTypeString]; + if(pasteboard == nil && item == nil) + pasteboard = [NSPasteboard generalPasteboard]; + + _pb = pasteboard; + _item = item; } - virtual HRESULT GetText (IAvnString**ppv) override + NSPasteboardItem* TryGetItem() + { + return _item; + } + + virtual HRESULT GetText (char* type, IAvnString**ppv) override { @autoreleasepool { @@ -23,39 +31,124 @@ public: { return E_POINTER; } + NSString* typeString = [NSString stringWithUTF8String:(const char*)type]; + NSString* string = _item == nil ? [_pb stringForType:typeString] : [_item stringForType:typeString]; - *ppv = CreateAvnString([[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]); + *ppv = CreateAvnString(string); return S_OK; } } - virtual HRESULT SetText (void* utf8String) override + virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) override { @autoreleasepool { - NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; - [pasteBoard clearContents]; - [pasteBoard setString:[NSString stringWithUTF8String:(const char*)utf8String] forType:NSPasteboardTypeString]; + *ppv= nil; + NSString* typeString = [NSString stringWithUTF8String:(const char*)type]; + NSObject* data = _item == nil ? [_pb propertyListForType: typeString] : [_item propertyListForType: typeString]; + if(data == nil) + return S_OK; + + if([data isKindOfClass: [NSString class]]) + { + *ppv = CreateAvnStringArray((NSString*) data); + return S_OK; + } + + NSArray* arr = (NSArray*)data; + + for(int c = 0; c < [arr count]; c++) + if(![[arr objectAtIndex:c] isKindOfClass:[NSString class]]) + return E_INVALIDARG; + + *ppv = CreateAvnStringArray(arr); + return S_OK; + } + } + + virtual HRESULT SetText (char* type, void* utf8String) override + { + Clear(); + @autoreleasepool + { + auto string = [NSString stringWithUTF8String:(const char*)utf8String]; + auto typeString = [NSString stringWithUTF8String:(const char*)type]; + if(_item == nil) + [_pb setString: string forType: typeString]; + else + [_item setString: string forType:typeString]; } 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 { @autoreleasepool { - NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; - [pasteBoard clearContents]; - [pasteBoard setString:@"" forType:NSPasteboardTypeString]; + if(_item != nil) + _item = [NSPasteboardItem new]; + else + { + [_pb clearContents]; + [_pb setString:@"" forType:NSPasteboardTypeString]; + } } return S_OK; } + + virtual HRESULT ObtainFormats(IAvnStringArray** ppv) override + { + *ppv = CreateAvnStringArray(_item == nil ? [_pb types] : [_item types]); + return S_OK; + } }; -extern IAvnClipboard* CreateClipboard() +extern IAvnClipboard* CreateClipboard(NSPasteboard* pb, NSPasteboardItem* item) +{ + return new Clipboard(pb, item); +} + +extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*cb) { - return new Clipboard(); + auto clipboard = dynamic_cast(cb); + if(clipboard == nil) + return nil; + return clipboard->TryGetItem(); } diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 10534dea26..871bca086d 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -1,6 +1,3 @@ -// 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. - #ifndef common_h #define common_h #include "comimpl.h" @@ -11,19 +8,25 @@ #include extern IAvnPlatformThreadingInterface* CreatePlatformThreading(); -extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events); -extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events); +extern void FreeAvnGCHandle(void* handle); +extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl); +extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl); extern IAvnSystemDialogs* CreateSystemDialogs(); extern IAvnScreens* CreateScreens(); -extern IAvnClipboard* CreateClipboard(); +extern IAvnClipboard* CreateClipboard(NSPasteboard*, NSPasteboardItem*); +extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*); +extern NSObject* CreateDraggingSource(NSDragOperation op, IAvnDndResultCallback* cb, void* handle); +extern void* GetAvnDataObjectHandleFromDraggingInfo(NSObject* info); +extern NSString* GetAvnCustomDataType(); +extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop); extern IAvnCursorFactory* CreateCursorFactory(); -extern IAvnGlFeature* GetGlFeature(); -extern IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(NSWindow* window, NSView* view); -extern IAvnAppMenu* CreateAppMenu(); -extern IAvnAppMenuItem* CreateAppMenuItem(); -extern IAvnAppMenuItem* CreateAppMenuItemSeperator(); -extern void SetAppMenu (NSString* appName, IAvnAppMenu* appMenu); -extern IAvnAppMenu* GetAppMenu (); +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 (); extern void InitializeAvnApp(); @@ -52,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..315ec2f310 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/controlhost.mm @@ -0,0 +1,145 @@ +#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 MoveTo(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->MoveTo(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 Hide() override + { + [_holder setHidden: true]; + } + + 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/cursor.h b/native/Avalonia.Native/src/OSX/cursor.h index cfe91955d8..75a9c3d2ad 100644 --- a/native/Avalonia.Native/src/OSX/cursor.h +++ b/native/Avalonia.Native/src/OSX/cursor.h @@ -1,6 +1,3 @@ -// 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. - #ifndef cursor_h #define cursor_h diff --git a/native/Avalonia.Native/src/OSX/cursor.mm b/native/Avalonia.Native/src/OSX/cursor.mm index 799fa9e8e6..b6f9ed5071 100644 --- a/native/Avalonia.Native/src/OSX/cursor.mm +++ b/native/Avalonia.Native/src/OSX/cursor.mm @@ -1,6 +1,3 @@ -// 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. - #include "common.h" #include "cursor.h" #include 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/dnd.mm b/native/Avalonia.Native/src/OSX/dnd.mm new file mode 100644 index 0000000000..294b8ee8ea --- /dev/null +++ b/native/Avalonia.Native/src/OSX/dnd.mm @@ -0,0 +1,89 @@ +#include "common.h" + +extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop) +{ + int effects = 0; + if((nsop & NSDragOperationCopy) != 0) + effects |= (int)AvnDragDropEffects::Copy; + if((nsop & NSDragOperationMove) != 0) + effects |= (int)AvnDragDropEffects::Move; + if((nsop & NSDragOperationLink) != 0) + effects |= (int)AvnDragDropEffects::Link; + return (AvnDragDropEffects)effects; +}; + +extern NSString* GetAvnCustomDataType() +{ + char buffer[256]; + sprintf(buffer, "net.avaloniaui.inproc.uti.n%in", getpid()); + return [NSString stringWithUTF8String:buffer]; +} + +@interface AvnDndSource : NSObject + +@end + +@implementation AvnDndSource +{ + NSDragOperation _operation; + ComPtr _cb; + void* _sourceHandle; +}; + +- (NSDragOperation)draggingSession:(nonnull NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context +{ + return NSDragOperationCopy; +} + +- (AvnDndSource*) initWithOperation: (NSDragOperation)operation + andCallback: (IAvnDndResultCallback*) cb + andSourceHandle: (void*) handle +{ + self = [super init]; + _operation = operation; + _cb = cb; + _sourceHandle = handle; + return self; +} + +- (void)draggingSession:(NSDraggingSession *)session + endedAtPoint:(NSPoint)screenPoint + operation:(NSDragOperation)operation +{ + if(_cb != nil) + { + auto cb = _cb; + _cb = nil; + cb->OnDragAndDropComplete(ConvertDragDropEffects(operation)); + } + if(_sourceHandle != nil) + { + FreeAvnGCHandle(_sourceHandle); + _sourceHandle = nil; + } +} + +- (void*) gcHandle +{ + return _sourceHandle; +} + +@end + +extern NSObject* CreateDraggingSource(NSDragOperation op, IAvnDndResultCallback* cb, void* handle) +{ + return [[AvnDndSource alloc] initWithOperation:op andCallback:cb andSourceHandle:handle]; +}; + +extern void* GetAvnDataObjectHandleFromDraggingInfo(NSObject* info) +{ + id obj = [info draggingSource]; + if(obj == nil) + return nil; + if([obj isKindOfClass: [AvnDndSource class]]) + { + auto src = (AvnDndSource*)obj; + return [src gcHandle]; + } + return nil; +} diff --git a/native/Avalonia.Native/src/OSX/gl.mm b/native/Avalonia.Native/src/OSX/gl.mm deleted file mode 100644 index 083adc927d..0000000000 --- a/native/Avalonia.Native/src/OSX/gl.mm +++ /dev/null @@ -1,253 +0,0 @@ -#include "common.h" -#include -#include - -template char (&ArrayCounter(T (&a)[N]))[N]; -#define ARRAY_COUNT(a) (sizeof(ArrayCounter(a))) - -NSOpenGLPixelFormat* CreateFormat() -{ - NSOpenGLPixelFormatAttribute attribs[] = - { - NSOpenGLPFADoubleBuffer, - NSOpenGLPFAColorSize, 32, - NSOpenGLPFAStencilSize, 8, - NSOpenGLPFADepthSize, 8, - 0 - }; - return [[NSOpenGLPixelFormat alloc] initWithAttributes:attribs]; -} - -class AvnGlContext : public virtual ComSingleObject -{ -public: - FORWARD_IUNKNOWN() - NSOpenGLContext* GlContext; - GLuint Framebuffer, RenderBuffer, StencilBuffer; - AvnGlContext(NSOpenGLContext* gl, bool offscreen) - { - Framebuffer = 0; - RenderBuffer = 0; - StencilBuffer = 0; - GlContext = gl; - if(offscreen) - { - [GlContext makeCurrentContext]; - - glGenFramebuffersEXT(1, &Framebuffer); - glBindFramebufferEXT(GL_FRAMEBUFFER, Framebuffer); - glGenRenderbuffersEXT(1, &RenderBuffer); - glGenRenderbuffersEXT(1, &StencilBuffer); - - glBindRenderbufferEXT(GL_RENDERBUFFER, StencilBuffer); - glFramebufferRenderbufferEXT(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, StencilBuffer); - glBindRenderbufferEXT(GL_RENDERBUFFER, RenderBuffer); - glFramebufferRenderbufferEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, RenderBuffer); - } - - } - - - virtual HRESULT MakeCurrent() override - { - [GlContext makeCurrentContext];/* - glBindFramebufferEXT(GL_FRAMEBUFFER, Framebuffer); - glBindRenderbufferEXT(GL_RENDERBUFFER, RenderBuffer); - glFramebufferRenderbufferEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, RenderBuffer); - glBindRenderbufferEXT(GL_RENDERBUFFER, StencilBuffer); - glFramebufferRenderbufferEXT(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, StencilBuffer);*/ - return S_OK; - } -}; - -class AvnGlDisplay : public virtual ComSingleObject -{ - int _sampleCount, _stencilSize; - void* _libgl; - -public: - FORWARD_IUNKNOWN() - - AvnGlDisplay(int sampleCount, int stencilSize) - { - _sampleCount = sampleCount; - _stencilSize = stencilSize; - _libgl = dlopen("/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib", RTLD_LAZY); - } - - virtual HRESULT GetSampleCount(int* ret) override - { - *ret = _sampleCount; - return S_OK; - } - virtual HRESULT GetStencilSize(int* ret) override - { - *ret = _stencilSize; - return S_OK; - } - - virtual HRESULT ClearContext() override - { - [NSOpenGLContext clearCurrentContext]; - return S_OK; - } - - virtual void* GetProcAddress(char* proc) override - { - return dlsym(_libgl, proc); - } -}; - - -class GlFeature : public virtual ComSingleObject -{ - IAvnGlDisplay* _display; - AvnGlContext *_immediate; - NSOpenGLContext* _shared; -public: - FORWARD_IUNKNOWN() - NSOpenGLPixelFormat* _format; - GlFeature(IAvnGlDisplay* display, AvnGlContext* immediate, NSOpenGLPixelFormat* format) - { - _display = display; - _immediate = immediate; - _format = format; - _shared = [[NSOpenGLContext alloc] initWithFormat:_format shareContext:_immediate->GlContext]; - } - - NSOpenGLContext* CreateContext() - { - return _shared; - //return [[NSOpenGLContext alloc] initWithFormat:_format shareContext:nil]; - } - - virtual HRESULT ObtainDisplay(IAvnGlDisplay**retOut) override - { - *retOut = _display; - _display->AddRef(); - return S_OK; - } - - virtual HRESULT ObtainImmediateContext(IAvnGlContext**retOut) override - { - *retOut = _immediate; - _immediate->AddRef(); - return S_OK; - } -}; - -static GlFeature* Feature; - -GlFeature* CreateGlFeature() -{ - auto format = CreateFormat(); - if(format == nil) - { - NSLog(@"Unable to choose pixel format"); - return NULL; - } - - auto immediateContext = [[NSOpenGLContext alloc] initWithFormat:format shareContext:nil]; - if(immediateContext == nil) - { - NSLog(@"Unable to create NSOpenGLContext"); - return NULL; - } - - int stencilBits = 0, sampleCount = 0; - - auto fmt = CGLGetPixelFormat([immediateContext CGLContextObj]); - CGLDescribePixelFormat(fmt, 0, kCGLPFASamples, &sampleCount); - CGLDescribePixelFormat(fmt, 0, kCGLPFAStencilSize, &stencilBits); - - auto offscreen = new AvnGlContext(immediateContext, true); - auto display = new AvnGlDisplay(sampleCount, stencilBits); - - return new GlFeature(display, offscreen, format); -} - - -static GlFeature* GetFeature() -{ - if(Feature == nil) - Feature = CreateGlFeature(); - return Feature; -} - -extern IAvnGlFeature* GetGlFeature() -{ - return GetFeature(); -} - -class AvnGlRenderingSession : public ComSingleObject -{ - NSView* _view; - NSWindow* _window; - NSOpenGLContext* _context; -public: - FORWARD_IUNKNOWN() - AvnGlRenderingSession(NSWindow*window, NSView* view, NSOpenGLContext* context) - { - _context = context; - _window = window; - _view = view; - } - - virtual HRESULT GetPixelSize(AvnPixelSize* ret) override - { - auto fsize = [_view convertSizeToBacking: [_view frame].size]; - ret->Width = (int)fsize.width; - ret->Height = (int)fsize.height; - return S_OK; - } - virtual HRESULT GetScaling(double* ret) override - { - *ret = [_window backingScaleFactor]; - return S_OK; - } - - virtual ~AvnGlRenderingSession() - { - [_context flushBuffer]; - [NSOpenGLContext clearCurrentContext]; - CGLUnlockContext([_context CGLContextObj]); - [_view unlockFocus]; - } -}; - -class AvnGlRenderTarget : public ComSingleObject -{ - NSView* _view; - NSWindow* _window; - NSOpenGLContext* _context; -public: - FORWARD_IUNKNOWN() - AvnGlRenderTarget(NSWindow* window, NSView*view) - { - _window = window; - _view = view; - _context = GetFeature()->CreateContext(); - } - - virtual HRESULT BeginDrawing(IAvnGlSurfaceRenderingSession** ret) override - { - auto f = GetFeature(); - if(f == NULL) - return E_FAIL; - if(![_view lockFocusIfCanDraw]) - return E_ABORT; - - auto gl = _context; - CGLLockContext([_context CGLContextObj]); - [gl setView: _view]; - [gl update]; - [gl makeCurrentContext]; - *ret = new AvnGlRenderingSession(_window, _view, gl); - return S_OK; - } -}; - -extern IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(NSWindow* window, NSView* view) -{ - return new AvnGlRenderTarget(window, view); -} diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 9418782fd1..e6c4a861fd 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -1,6 +1,3 @@ -// 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. - //This file will contain actual IID structures #define COM_GUIDS_MATERIALIZE #include "common.h" @@ -95,12 +92,11 @@ void SetProcessName(NSString* appTitle) { PrivateLSASN asn = ls_get_current_application_asn_func(); // Constant used by WebKit; what exactly it means is unknown. const int magic_session_constant = -2; - OSErr err = + ls_set_application_information_item_func(magic_session_constant, asn, ls_display_name_key, process_name, NULL /* optional out param */); - //LOG_IF(ERROR, err) << "Call to set process name failed, err " << err; } class MacOptions : public ComSingleObject @@ -154,14 +150,15 @@ public: } @end - +static ComPtr _deallocator; class AvaloniaNative : public ComSingleObject { public: FORWARD_IUNKNOWN() - virtual HRESULT Initialize() override + virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator) override { + _deallocator = deallocator; @autoreleasepool{ [[ThreadingInitializer new] do]; } @@ -174,20 +171,20 @@ public: return (IAvnMacOptions*)new MacOptions(); } - virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnWindow** ppv) override + virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv) override { if(cb == nullptr || ppv == nullptr) return E_POINTER; - *ppv = CreateAvnWindow(cb); + *ppv = CreateAvnWindow(cb, gl); return S_OK; }; - virtual HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnPopup** ppv) override + virtual HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv) override { if(cb == nullptr || ppv == nullptr) return E_POINTER; - *ppv = CreateAvnPopup(cb); + *ppv = CreateAvnPopup(cb, gl); return S_OK; } @@ -211,7 +208,13 @@ public: virtual HRESULT CreateClipboard(IAvnClipboard** ppv) override { - *ppv = ::CreateClipboard (); + *ppv = ::CreateClipboard (nil, nil); + return S_OK; + } + + virtual HRESULT CreateDndClipboard(IAvnClipboard** ppv) override + { + *ppv = ::CreateClipboard (nil, [NSPasteboardItem new]); return S_OK; } @@ -221,9 +224,9 @@ public: return S_OK; } - virtual HRESULT ObtainGlFeature(IAvnGlFeature** ppv) override + virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) override { - auto rv = ::GetGlFeature(); + auto rv = ::GetGlDisplay(); if(rv == NULL) return E_FAIL; rv->AddRef(); @@ -231,41 +234,29 @@ public: return S_OK; } - virtual HRESULT CreateMenu (IAvnAppMenu** ppv) override + virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) override { - *ppv = ::CreateAppMenu(); + *ppv = ::CreateAppMenu(cb); return S_OK; } - virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) override + virtual HRESULT CreateMenuItem (IAvnMenuItem** ppv) override { *ppv = ::CreateAppMenuItem(); return S_OK; } - virtual HRESULT CreateMenuItemSeperator (IAvnAppMenuItem** ppv) override + virtual HRESULT CreateMenuItemSeperator (IAvnMenuItem** ppv) override { *ppv = ::CreateAppMenuItemSeperator(); return S_OK; } - virtual HRESULT SetAppMenu (IAvnAppMenu* appMenu) override + virtual HRESULT SetAppMenu (IAvnMenu* appMenu) override { ::SetAppMenu(s_appTitle, appMenu); return S_OK; } - - virtual HRESULT ObtainAppMenu(IAvnAppMenu** retOut) override - { - if(retOut == nullptr) - { - return E_POINTER; - } - - *retOut = ::GetAppMenu(); - - return S_OK; - } }; extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative() @@ -273,6 +264,12 @@ extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative() return new AvaloniaNative(); }; +extern void FreeAvnGCHandle(void* handle) +{ + if(_deallocator != nil) + _deallocator->FreeGCHandle(handle); +} + NSSize ToNSSize (AvnSize s) { NSSize result; diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index befbe6a7e0..bfbc6801f8 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -14,8 +14,10 @@ class AvnAppMenuItem; class AvnAppMenu; -@interface AvnMenu : NSMenu // for some reason it doesnt detect nsmenu here but compiler doesnt complain -- (void)setMenu:(NSMenu*) menu; +@interface AvnMenu : NSMenu +- (id) initWithDelegate: (NSObject*) del; +- (void) setHasGlobalMenuItem: (bool) value; +- (bool) hasGlobalMenuItem; @end @interface AvnMenuItem : NSMenuItem @@ -23,13 +25,14 @@ class AvnAppMenu; - (void)didSelectItem:(id)sender; @end -class AvnAppMenuItem : public ComSingleObject +class AvnAppMenuItem : public ComSingleObject { private: NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem IAvnActionCallback* _callback; IAvnPredicateCallback* _predicate; bool _isSeperator; + bool _isCheckable; public: FORWARD_IUNKNOWN() @@ -38,7 +41,7 @@ public: NSMenuItem* GetNative(); - virtual HRESULT SetSubMenu (IAvnAppMenu* menu) override; + virtual HRESULT SetSubMenu (IAvnMenu* menu) override; virtual HRESULT SetTitle (void* utf8String) override; @@ -46,29 +49,36 @@ public: virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) override; + virtual HRESULT SetIsChecked (bool isChecked) override; + + virtual HRESULT SetToggleType (AvnMenuItemToggleType toggleType) override; + + virtual HRESULT SetIcon (void* data, size_t length) override; + bool EvaluateItemEnabled(); void RaiseOnClicked(); }; -class AvnAppMenu : public ComSingleObject +class AvnAppMenu : public ComSingleObject { private: AvnMenu* _native; + ComPtr _baseEvents; public: FORWARD_IUNKNOWN() - AvnAppMenu(); - - AvnAppMenu(AvnMenu* native); - + AvnAppMenu(IAvnMenuEvents* events); + AvnMenu* GetNative(); - virtual HRESULT AddItem (IAvnAppMenuItem* item) override; + void RaiseNeedsUpdate (); + + virtual HRESULT InsertItem (int index, IAvnMenuItem* item) override; - virtual HRESULT RemoveItem (IAvnAppMenuItem* item) override; + virtual HRESULT RemoveItem (IAvnMenuItem* item) override; virtual HRESULT SetTitle (void* utf8String) override; @@ -76,5 +86,9 @@ public: }; +@interface AvnMenuDelegate : NSObject +- (id) initWithParent: (AvnAppMenu*) parent; +@end + #endif diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index d9dfe36444..dc1245cd23 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -1,8 +1,33 @@ #include "common.h" #include "menu.h" +#include "window.h" @implementation AvnMenu +{ + bool _isReparented; + NSObject* _wtf; +} + +- (id) initWithDelegate: (NSObject*)del +{ + self = [super init]; + self.delegate = del; + _wtf = del; + _isReparented = false; + return self; +} + +- (bool)hasGlobalMenuItem +{ + return _isReparented; +} + +- (void)setHasGlobalMenuItem:(bool)value +{ + _isReparented = value; +} + @end @implementation AvnMenuItem @@ -45,6 +70,7 @@ AvnAppMenuItem::AvnAppMenuItem(bool isSeperator) { + _isCheckable = false; _isSeperator = isSeperator; if(isSeperator) @@ -64,49 +90,134 @@ NSMenuItem* AvnAppMenuItem::GetNative() return _native; } -HRESULT AvnAppMenuItem::SetSubMenu (IAvnAppMenu* menu) +HRESULT AvnAppMenuItem::SetSubMenu (IAvnMenu* menu) { - auto nsMenu = dynamic_cast(menu)->GetNative(); - - [_native setSubmenu: nsMenu]; - - return S_OK; + @autoreleasepool + { + if(menu != nullptr) + { + auto nsMenu = dynamic_cast(menu)->GetNative(); + + [_native setSubmenu: nsMenu]; + } + else + { + [_native setSubmenu: nullptr]; + } + + return S_OK; + } } HRESULT AvnAppMenuItem::SetTitle (void* utf8String) { - if (utf8String != nullptr) + @autoreleasepool { - [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + if (utf8String != nullptr) + { + [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + } + + return S_OK; } - - return S_OK; } HRESULT AvnAppMenuItem::SetGesture (void* key, AvnInputModifiers modifiers) { - NSEventModifierFlags flags = 0; - - if (modifiers & Control) - flags |= NSEventModifierFlagControl; - if (modifiers & Shift) - flags |= NSEventModifierFlagShift; - if (modifiers & Alt) - flags |= NSEventModifierFlagOption; - if (modifiers & Windows) - flags |= NSEventModifierFlagCommand; - - [_native setKeyEquivalent:[NSString stringWithUTF8String:(const char*)key]]; - [_native setKeyEquivalentModifierMask:flags]; - - return S_OK; + @autoreleasepool + { + NSEventModifierFlags flags = 0; + + if (modifiers & Control) + flags |= NSEventModifierFlagControl; + if (modifiers & Shift) + flags |= NSEventModifierFlagShift; + if (modifiers & Alt) + flags |= NSEventModifierFlagOption; + if (modifiers & Windows) + flags |= NSEventModifierFlagCommand; + + [_native setKeyEquivalent:[NSString stringWithUTF8String:(const char*)key]]; + [_native setKeyEquivalentModifierMask:flags]; + + return S_OK; + } } HRESULT AvnAppMenuItem::SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) { - _predicate = predicate; - _callback = callback; - return S_OK; + @autoreleasepool + { + _predicate = predicate; + _callback = callback; + return S_OK; + } +} + +HRESULT AvnAppMenuItem::SetIsChecked (bool isChecked) +{ + @autoreleasepool + { + [_native setState:(isChecked && _isCheckable ? NSOnState : NSOffState)]; + return S_OK; + } +} + +HRESULT AvnAppMenuItem::SetToggleType(AvnMenuItemToggleType toggleType) +{ + @autoreleasepool + { + switch(toggleType) + { + case AvnMenuItemToggleType::None: + [_native setOnStateImage: [NSImage imageNamed:@"NSMenuCheckmark"]]; + + _isCheckable = false; + break; + + case AvnMenuItemToggleType::CheckMark: + [_native setOnStateImage: [NSImage imageNamed:@"NSMenuCheckmark"]]; + + _isCheckable = true; + break; + + case AvnMenuItemToggleType::Radio: + [_native setOnStateImage: [NSImage imageNamed:@"NSMenuItemBullet"]]; + + _isCheckable = true; + break; + } + + return S_OK; + } +} + +HRESULT AvnAppMenuItem::SetIcon(void *data, size_t length) +{ + @autoreleasepool + { + if(data != nullptr) + { + NSData *imageData = [NSData dataWithBytes:data length:length]; + NSImage *image = [[NSImage alloc] initWithData:imageData]; + + NSSize originalSize = [image size]; + + NSSize size; + size.height = [[NSFont menuFontOfSize:0] pointSize] * 1.333333; + + auto scaleFactor = size.height / originalSize.height; + size.width = originalSize.width * scaleFactor; + + [image setSize: size]; + [_native setImage:image]; + } + else + { + [_native setImage:nullptr]; + } + return S_OK; + } } bool AvnAppMenuItem::EvaluateItemEnabled() @@ -129,71 +240,123 @@ void AvnAppMenuItem::RaiseOnClicked() } } -AvnAppMenu::AvnAppMenu() +AvnAppMenu::AvnAppMenu(IAvnMenuEvents* events) { - _native = [AvnMenu new]; + _baseEvents = events; + id del = [[AvnMenuDelegate alloc] initWithParent: this]; + _native = [[AvnMenu alloc] initWithDelegate: del]; } -AvnAppMenu::AvnAppMenu(AvnMenu* native) -{ - _native = native; -} AvnMenu* AvnAppMenu::GetNative() { return _native; } -HRESULT AvnAppMenu::AddItem (IAvnAppMenuItem* item) +void AvnAppMenu::RaiseNeedsUpdate() { - auto avnMenuItem = dynamic_cast(item); - - if(avnMenuItem != nullptr) + if(_baseEvents != nullptr) { - [_native addItem: avnMenuItem->GetNative()]; + _baseEvents->NeedsUpdate(); } - - return S_OK; } -HRESULT AvnAppMenu::RemoveItem (IAvnAppMenuItem* item) +HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item) { - auto avnMenuItem = dynamic_cast(item); - - if(avnMenuItem != nullptr) + @autoreleasepool { - [_native removeItem:avnMenuItem->GetNative()]; + if([_native hasGlobalMenuItem]) + { + index++; + } + + auto avnMenuItem = dynamic_cast(item); + + if(avnMenuItem != nullptr) + { + [_native insertItem: avnMenuItem->GetNative() atIndex:index]; + } + + return S_OK; + } +} + +HRESULT AvnAppMenu::RemoveItem (IAvnMenuItem* item) +{ + @autoreleasepool + { + auto avnMenuItem = dynamic_cast(item); + + if(avnMenuItem != nullptr) + { + [_native removeItem:avnMenuItem->GetNative()]; + } + + return S_OK; } - - return S_OK; } HRESULT AvnAppMenu::SetTitle (void* utf8String) { - if (utf8String != nullptr) + @autoreleasepool { - [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + if (utf8String != nullptr) + { + [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + } + + return S_OK; } - - return S_OK; } HRESULT AvnAppMenu::Clear() { - [_native removeAllItems]; - return S_OK; + @autoreleasepool + { + [_native removeAllItems]; + return S_OK; + } +} + +@implementation AvnMenuDelegate +{ + ComPtr _parent; } +- (id) initWithParent:(AvnAppMenu *)parent +{ + self = [super init]; + _parent = parent; + return self; +} +- (BOOL)menu:(NSMenu *)menu updateItem:(NSMenuItem *)item atIndex:(NSInteger)index shouldCancel:(BOOL)shouldCancel +{ + if(shouldCancel) + return NO; + return YES; +} + +- (NSInteger)numberOfItemsInMenu:(NSMenu *)menu +{ + return [menu numberOfItems]; +} + +- (void)menuNeedsUpdate:(NSMenu *)menu +{ + _parent->RaiseNeedsUpdate(); +} + + +@end -extern IAvnAppMenu* CreateAppMenu() +extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* cb) { @autoreleasepool { - id menuBar = [NSMenu new]; - return new AvnAppMenu(menuBar); + return new AvnAppMenu(cb); } } -extern IAvnAppMenuItem* CreateAppMenuItem() +extern IAvnMenuItem* CreateAppMenuItem() { @autoreleasepool { @@ -201,7 +364,7 @@ extern IAvnAppMenuItem* CreateAppMenuItem() } } -extern IAvnAppMenuItem* CreateAppMenuItemSeperator() +extern IAvnMenuItem* CreateAppMenuItemSeperator() { @autoreleasepool { @@ -209,10 +372,10 @@ extern IAvnAppMenuItem* CreateAppMenuItemSeperator() } } -static IAvnAppMenu* s_appMenu = nullptr; +static IAvnMenu* s_appMenu = nullptr; static NSMenuItem* s_appMenuItem = nullptr; -extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu) +extern void SetAppMenu (NSString* appName, IAvnMenu* menu) { s_appMenu = menu; @@ -283,7 +446,8 @@ extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu) auto quitItem = [[NSMenuItem alloc] init]; quitItem.title = [@"Quit " stringByAppendingString:appName]; quitItem.keyEquivalent = @"q"; - quitItem.action = @selector(terminate:); + quitItem.target = [AvnWindow class]; + quitItem.action = @selector(closeAll); [appMenu addItem:quitItem]; } else @@ -292,7 +456,7 @@ extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu) } } -extern IAvnAppMenu* GetAppMenu () +extern IAvnMenu* GetAppMenu () { return s_appMenu; } diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm index e7abedae51..f93436d157 100644 --- a/native/Avalonia.Native/src/OSX/platformthreading.mm +++ b/native/Avalonia.Native/src/OSX/platformthreading.mm @@ -1,6 +1,3 @@ -// 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. - #include "common.h" class PlatformThreadingInterface; @@ -57,9 +54,11 @@ private: { public: FORWARD_IUNKNOWN() + bool Running = false; bool Cancelled = false; - virtual void Cancel() + + virtual void Cancel() override { Cancelled = true; if(Running) @@ -157,11 +156,14 @@ NSArray* _modes; -(void) perform { + ComPtr cb; @synchronized (self) { _signaled = false; - if(_parent != NULL && _parent->SignaledCallback != NULL) - _parent->SignaledCallback->Signaled(0, false); + if(_parent != NULL) + cb = _parent->SignaledCallback; } + if(cb != nullptr) + cb->Signaled(0, false); } -(void) setParent:(PlatformThreadingInterface *)parent diff --git a/native/Avalonia.Native/src/OSX/rendertarget.mm b/native/Avalonia.Native/src/OSX/rendertarget.mm new file mode 100644 index 0000000000..1565417c1a --- /dev/null +++ b/native/Avalonia.Native/src/OSX/rendertarget.mm @@ -0,0 +1,284 @@ +#include "common.h" +#include "rendertarget.h" +#import +#import + +#include +#include +#include +#include +#include + +@interface IOSurfaceHolder : NSObject +@end + +@implementation IOSurfaceHolder +{ + @public IOSurfaceRef surface; + @public AvnPixelSize size; + @public float scale; + ComPtr _context; + GLuint _framebuffer, _texture, _renderbuffer; +} + +- (IOSurfaceHolder*) initWithSize: (AvnPixelSize) size + withScale: (float)scale + withOpenGlContext: (IAvnGlContext*) context +{ + long bytesPerRow = IOSurfaceAlignProperty(kIOSurfaceBytesPerRow, size.Width * 4); + long allocSize = IOSurfaceAlignProperty(kIOSurfaceAllocSize, size.Height * bytesPerRow); + NSDictionary* options = @{ + (id)kIOSurfaceWidth: @(size.Width), + (id)kIOSurfaceHeight: @(size.Height), + (id)kIOSurfacePixelFormat: @((uint)'BGRA'), + (id)kIOSurfaceBytesPerElement: @(4), + (id)kIOSurfaceBytesPerRow: @(bytesPerRow), + (id)kIOSurfaceAllocSize: @(allocSize), + + //(id)kIOSurfaceCacheMode: @(kIOMapWriteCombineCache), + (id)kIOSurfaceElementWidth: @(1), + (id)kIOSurfaceElementHeight: @(1) + }; + + surface = IOSurfaceCreate((CFDictionaryRef)options); + self->scale = scale; + self->size = size; + self->_context = context; + return self; +} + +-(HRESULT) prepareForGlRender +{ + if(_context == nil) + return E_FAIL; + if(CGLGetCurrentContext() != _context->GetNativeHandle()) + return E_FAIL; + if(_framebuffer == 0) + glGenFramebuffersEXT(1, &_framebuffer); + + + glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, _framebuffer); + if(_texture == 0) + { + glGenTextures(1, &_texture); + + glBindTexture(GL_TEXTURE_RECTANGLE_EXT, _texture); + CGLError res = CGLTexImageIOSurface2D((CGLContextObj)_context->GetNativeHandle(), + GL_TEXTURE_RECTANGLE_EXT, GL_RGBA8, + size.Width, size.Height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, surface, 0); + glBindTexture(GL_TEXTURE_RECTANGLE_EXT, 0); + + if(res != 0) + { + glDeleteTextures(1, &_texture); + _texture = 0; + glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); + return E_FAIL; + } + glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_RECTANGLE_EXT, _texture, 0); + } + + if(_renderbuffer == 0) + { + glGenRenderbuffers(1, &_renderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, _renderbuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, size.Width, size.Height); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _renderbuffer); + } + + return S_OK; +} + +-(void) finishDraw +{ + ComPtr release; + _context->MakeCurrent(release.getPPV()); + glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); + glFlush(); +} + +-(void) dealloc +{ + + if(_framebuffer != 0) + { + ComPtr release; + _context->MakeCurrent(release.getPPV()); + glDeleteFramebuffers(1, &_framebuffer); + if(_texture != 0) + glDeleteTextures(1, &_texture); + if(_renderbuffer != 0) + glDeleteRenderbuffers(1, &_renderbuffer); + } + IOSurfaceDecrementUseCount(surface); +} +@end + +static IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(IOSurfaceRenderTarget* target); + +@implementation IOSurfaceRenderTarget +{ + CALayer* _layer; + @public IOSurfaceHolder* surface; + @public NSObject* lock; + ComPtr _glContext; +} + +- (IOSurfaceRenderTarget*) initWithOpenGlContext: (IAvnGlContext*) context; +{ + self = [super init]; + _glContext = context; + lock = [NSObject new]; + surface = nil; + [self resize:{1,1} withScale: 1]; + + return self; +} + +- (AvnPixelSize) pixelSize { + return {1, 1}; +} + +- (CALayer *)layer { + return _layer; +} + +- (void)resize:(AvnPixelSize)size withScale: (float) scale;{ + @synchronized (lock) { + if(surface == nil + || surface->size.Width != size.Width + || surface->size.Height != size.Height + || surface->scale != scale) + surface = [[IOSurfaceHolder alloc] initWithSize:size withScale:scale withOpenGlContext:_glContext.getRaw()]; + } +} + +- (void)updateLayer { + if ([NSThread isMainThread]) + { + @synchronized (lock) { + if(_layer == nil) + return; + [_layer setContents: nil]; + if(surface != nil) + { + [_layer setContentsScale: surface->scale]; + [_layer setContents: (__bridge IOSurface*) surface->surface]; + } + } + } + else + dispatch_async(dispatch_get_main_queue(), ^{ + [self updateLayer]; + }); +} + +- (void) setNewLayer:(CALayer *)layer { + _layer = layer; + [self updateLayer]; +} + +- (HRESULT)setSwFrame:(AvnFramebuffer *)fb { + @synchronized (lock) { + if(fb->PixelFormat == AvnPixelFormat::kAvnRgb565) + return E_INVALIDARG; + if(surface == nil) + return E_FAIL; + IOSurfaceRef surf = surface->surface; + if(IOSurfaceLock(surf, 0, nil)) + return E_FAIL; + size_t w = MIN(fb->Width, IOSurfaceGetWidth(surf)); + size_t h = MIN(fb->Height, IOSurfaceGetHeight(surf)); + size_t wbytes = w*4; + size_t sstride = IOSurfaceGetBytesPerRow(surf); + size_t fstride = fb->Stride; + char*pSurface = (char*)IOSurfaceGetBaseAddress(surf); + char*pFb = (char*)fb->Data; + for(size_t y = 0; y < h; y++) + { + memcpy(pSurface + y*sstride, pFb + y*fstride, wbytes); + } + IOSurfaceUnlock(surf, 0, nil); + [self updateLayer]; + return S_OK; + } +} + +-(IAvnGlSurfaceRenderTarget*) createSurfaceRenderTarget +{ + return CreateGlRenderTarget(self); +} + +@end + +class AvnGlRenderingSession : public ComSingleObject +{ + ComPtr _releaseContext; + IOSurfaceRenderTarget* _target; + IOSurfaceHolder* _surface; +public: + FORWARD_IUNKNOWN() + AvnGlRenderingSession(IOSurfaceRenderTarget* target, ComPtr releaseContext) + { + _target = target; + // This happens in a synchronized block set up by AvnRenderTarget, so we take the current surface for this + // particular render session + _surface = _target->surface; + _releaseContext = releaseContext; + } + + virtual HRESULT GetPixelSize(AvnPixelSize* ret) override + { + if(!_surface) + return E_FAIL; + *ret = _surface->size; + return S_OK; + } + + virtual HRESULT GetScaling(double* ret) override + { + if(!_surface) + return E_FAIL; + *ret = _surface->scale; + return S_OK; + } + + virtual ~AvnGlRenderingSession() + { + [_surface finishDraw]; + [_target updateLayer]; + _releaseContext = nil; + } +}; + +class AvnGlRenderTarget : public ComSingleObject +{ + IOSurfaceRenderTarget* _target; +public: + FORWARD_IUNKNOWN() + AvnGlRenderTarget(IOSurfaceRenderTarget* target) + { + _target = target; + } + + virtual HRESULT BeginDrawing(IAvnGlSurfaceRenderingSession** ret) override + { + ComPtr releaseContext; + @synchronized (_target->lock) { + if(_target->surface == nil) + return E_FAIL; + _target->_glContext->MakeCurrent(releaseContext.getPPV()); + HRESULT res = [_target->surface prepareForGlRender]; + if(res) + return res; + *ret = new AvnGlRenderingSession(_target, releaseContext); + return S_OK; + } + } +}; + + +static IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(IOSurfaceRenderTarget* target) +{ + return new AvnGlRenderTarget(target); +} diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 557e19e7a8..bdf3007a28 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -1,26 +1,32 @@ -// 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. - #ifndef window_h #define window_h class WindowBaseImpl; -@interface AvnView : NSView +@interface AutoFitContentVisualEffectView : NSVisualEffectView +@end + +@interface AvnView : NSView -(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; -(NSEvent* _Nonnull) lastMouseDownEvent; -(AvnPoint) translateLocalPoint:(AvnPoint)pt; -(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose; -(void) onClosed; +-(AvnPixelSize) getPixelSize; @end @interface AvnWindow : NSWindow ++(void) closeAll; -(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; -(void) setCanBecomeKeyAndMain; -(void) pollModalSession: (NSModalSession _Nonnull) session; -(void) restoreParentWindow; -(bool) shouldTryToHandleEvents; --(void) applyMenu:(NSMenu *)menu; +-(void) setEnabled: (bool) enable; +-(void) showAppMenuOnly; +-(void) showWindowMenuWithAppMenu; +-(void) applyMenu:(NSMenu* _Nullable)menu; +-(double) getScaling; @end struct INSWindowHolder @@ -31,6 +37,10 @@ struct INSWindowHolder struct IWindowStateChanged { virtual void WindowStateChanged () = 0; + virtual void StartStateTransition () = 0; + virtual void EndStateTransition () = 0; + virtual SystemDecorations Decorations () = 0; + virtual AvnWindowState WindowState () = 0; }; #endif /* window_h */ diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index dbb437243a..86b3584681 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1,50 +1,12 @@ -// 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. - #include "common.h" #include "window.h" #include "KeyTransform.h" #include "cursor.h" #include "menu.h" #include +#include "rendertarget.h" + -class SoftwareDrawingOperation -{ -public: - void* Data = 0; - AvnFramebuffer Desc; - void Alloc(NSView* view) - { - auto logicalSize = [view frame].size; - auto pixelSize = [view convertSizeToBacking:logicalSize]; - int w = pixelSize.width; - int h = pixelSize.height; - int stride = w * 4; - Data = malloc(h * stride); - Desc = { - .Data = Data, - .Stride = stride, - .Width = w, - .Height = h, - .PixelFormat = kAvnRgba8888, - .Dpi = AvnVector { .X = w / logicalSize.width * 96, .Y = h / logicalSize.height * 96} - }; - } - - void Dealloc() - { - if(Data != NULL) - { - free(Data); - Data = NULL; - } - } - - ~SoftwareDrawingOperation() - { - Dealloc(); - } -}; class WindowBaseImpl : public virtual ComSingleObject, public INSWindowHolder { @@ -58,18 +20,24 @@ public: View = NULL; Window = NULL; } + NSVisualEffectView* VisualEffect; AvnView* View; AvnWindow* Window; ComPtr BaseEvents; - SoftwareDrawingOperation CurrentSwDrawingOperation; + ComPtr _glContext; + NSObject* renderTarget; AvnPoint lastPositionSet; NSString* _lastTitle; - IAvnAppMenu* _mainMenu; + IAvnMenu* _mainMenu; + bool _shown; - WindowBaseImpl(IAvnWindowBaseEvents* events) + WindowBaseImpl(IAvnWindowBaseEvents* events, IAvnGlContext* gl) { + _shown = false; _mainMenu = nullptr; BaseEvents = events; + _glContext = gl; + renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext: gl]; View = [[AvnView alloc] initWithParent:this]; Window = [[AvnWindow alloc] initWithParent:this]; @@ -80,9 +48,63 @@ public: [Window setStyleMask:NSWindowStyleMaskBorderless]; [Window setBackingType:NSBackingStoreBuffered]; + + VisualEffect = [AutoFitContentVisualEffectView new]; + [VisualEffect setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; + [VisualEffect setMaterial:NSVisualEffectMaterialLight]; + [VisualEffect setAutoresizesSubviews:true]; + [Window setContentView: View]; } + virtual HRESULT ObtainNSWindowHandle(void** ret) override + { + if (ret == nullptr) + { + return E_POINTER; + } + + *ret = (__bridge void*)Window; + + return S_OK; + } + + virtual HRESULT ObtainNSWindowHandleRetained(void** ret) override + { + if (ret == nullptr) + { + return E_POINTER; + } + + *ret = (__bridge_retained void*)Window; + + return S_OK; + } + + virtual HRESULT ObtainNSViewHandle(void** ret) override + { + if (ret == nullptr) + { + return E_POINTER; + } + + *ret = (__bridge void*)View; + + return S_OK; + } + + virtual HRESULT ObtainNSViewHandleRetained(void** ret) override + { + if (ret == nullptr) + { + return E_POINTER; + } + + *ret = (__bridge_retained void*)View; + + return S_OK; + } + virtual AvnWindow* GetNSWindow() override { return Window; @@ -99,7 +121,8 @@ public: [NSApp activateIgnoringOtherApps:YES]; [Window setTitle:_lastTitle]; - [Window setTitleVisibility:NSWindowTitleVisible]; + + _shown = true; return S_OK; } @@ -147,7 +170,11 @@ public: { @autoreleasepool { - [Window close]; + if (Window != nullptr) + { + [Window close]; + } + return S_OK; } } @@ -214,7 +241,7 @@ public: } } - virtual HRESULT SetMainMenu(IAvnAppMenu* menu) override + virtual HRESULT SetMainMenu(IAvnMenu* menu) override { _mainMenu = menu; @@ -224,37 +251,14 @@ public: [Window applyMenu:nsmenu]; - return S_OK; - } - - virtual HRESULT ObtainMainMenu(IAvnAppMenu** ret) override - { - if(ret == nullptr) + if ([Window isKeyWindow]) { - return E_POINTER; + [Window showWindowMenuWithAppMenu]; } - *ret = _mainMenu; - return S_OK; } - virtual bool TryLock() override - { - @autoreleasepool - { - return [View lockFocusIfCanDraw] == YES; - } - } - - virtual void Unlock() override - { - @autoreleasepool - { - [View unlockFocus]; - } - } - virtual HRESULT BeginMoveDrag () override { @autoreleasepool @@ -349,16 +353,6 @@ public: return S_OK; } - virtual HRESULT GetSoftwareFramebuffer(AvnFramebuffer*ret) override - { - if(![[NSThread currentThread] isMainThread]) - return E_FAIL; - if(CurrentSwDrawingOperation.Data == NULL) - CurrentSwDrawingOperation.Alloc(View); - *ret = CurrentSwDrawingOperation.Desc; - return S_OK; - } - virtual HRESULT SetCursor(IAvnCursor* cursor) override { @autoreleasepool @@ -392,7 +386,71 @@ public: { if(View == NULL) return E_FAIL; - *ppv = ::CreateGlRenderTarget(Window, View); + *ppv = [renderTarget createSurfaceRenderTarget]; + 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]; + } + + return S_OK; + } + + virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, + IAvnClipboard* clipboard, IAvnDndResultCallback* cb, + void* sourceHandle) override + { + auto item = TryGetPasteboardItem(clipboard); + [item setString:@"" forType:GetAvnCustomDataType()]; + if(item == nil) + return E_INVALIDARG; + if(View == NULL) + return E_FAIL; + + auto nsevent = [NSApp currentEvent]; + auto nseventType = [nsevent type]; + + // If current event isn't a mouse one (probably due to malfunctioning user app) + // attempt to forge a new one + if(!((nseventType >= NSEventTypeLeftMouseDown && nseventType <= NSEventTypeMouseExited) + || (nseventType >= NSEventTypeOtherMouseDown && nseventType <= NSEventTypeOtherMouseDragged))) + { + auto nspoint = [Window convertBaseToScreen: ToNSPoint(point)]; + CGPoint cgpoint = NSPointToCGPoint(nspoint); + auto cgevent = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, cgpoint, kCGMouseButtonLeft); + nsevent = [NSEvent eventWithCGEvent: cgevent]; + CFRelease(cgevent); + } + + auto dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter: item]; + + auto dragItemImage = [NSImage imageNamed:NSImageNameMultipleDocuments]; + NSRect dragItemRect = {(float)point.X, (float)point.Y, [dragItemImage size].width, [dragItemImage size].height}; + [dragItem setDraggingFrame: dragItemRect contents: dragItemImage]; + + int op = 0; int ieffects = (int)effects; + if((ieffects & (int)AvnDragDropEffects::Copy) != 0) + op |= NSDragOperationCopy; + if((ieffects & (int)AvnDragDropEffects::Link) != 0) + op |= NSDragOperationLink; + if((ieffects & (int)AvnDragDropEffects::Move) != 0) + op |= NSDragOperationMove; + [View beginDraggingSessionWithItems: @[dragItem] event: nsevent + source: CreateDraggingSource((NSDragOperation) op, cb, sourceHandle)]; return S_OK; } @@ -404,9 +462,10 @@ protected: void UpdateStyle() { - [Window setStyleMask:GetStyle()]; + [Window setStyleMask: GetStyle()]; } +public: virtual void OnResized () { @@ -416,10 +475,13 @@ protected: class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, public IWindowStateChanged { private: - bool _canResize = true; - bool _hasDecorations = true; - CGRect _lastUndecoratedFrame; + bool _canResize; + bool _fullScreenActive; + SystemDecorations _decorations; AvnWindowState _lastWindowState; + bool _inSetWindowState; + NSRect _preZoomSize; + bool _transitioningWindowState; FORWARD_IUNKNOWN() BEGIN_INTERFACE_MAP() @@ -431,26 +493,56 @@ private: } ComPtr WindowEvents; - WindowImpl(IAvnWindowEvents* events) : WindowBaseImpl(events) - { + WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) + { + _fullScreenActive = false; + _canResize = true; + _decorations = SystemDecorationsFull; + _transitioningWindowState = false; + _inSetWindowState = false; + _lastWindowState = Normal; WindowEvents = events; [Window setCanBecomeKeyAndMain]; [Window disableCursorRects]; + [Window setTabbingMode:NSWindowTabbingModeDisallowed]; + } + + void HideOrShowTrafficLights () + { + for (id subview in Window.contentView.superview.subviews) { + 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)]; + } + } + } + } } virtual HRESULT Show () override { @autoreleasepool - { - if([Window parentWindow] != nil) - [[Window parentWindow] removeChildWindow:Window]; + { WindowBaseImpl::Show(); - return SetWindowState(Normal); + HideOrShowTrafficLights(); + + return SetWindowState(_lastWindowState); } } - virtual HRESULT ShowDialog (IAvnWindow* parent) override + virtual HRESULT SetEnabled (bool enable) override + { + @autoreleasepool + { + [Window setEnabled:enable]; + return S_OK; + } + } + + virtual HRESULT SetParent (IAvnWindow* parent) override { @autoreleasepool { @@ -462,43 +554,73 @@ private: return E_INVALIDARG; [cparent->Window addChildWindow:Window ordered:NSWindowAbove]; - WindowBaseImpl::Show(); + + UpdateStyle(); return S_OK; } } + void StartStateTransition () override + { + _transitioningWindowState = true; + } + + void EndStateTransition () override + { + _transitioningWindowState = false; + } + + SystemDecorations Decorations () override + { + return _decorations; + } + + AvnWindowState WindowState () override + { + return _lastWindowState; + } + void WindowStateChanged () override { - AvnWindowState state; - GetWindowState(&state); - WindowEvents->WindowStateChanged(state); + if(!_inSetWindowState && !_transitioningWindowState) + { + AvnWindowState state; + GetWindowState(&state); + + if(_lastWindowState != state) + { + _lastWindowState = state; + WindowEvents->WindowStateChanged(state); + } + } } bool UndecoratedIsMaximized () { - return CGRectEqualToRect([Window frame], [Window screen].visibleFrame); + auto windowSize = [Window frame]; + auto available = [Window screen].visibleFrame; + return CGRectEqualToRect(windowSize, available); } bool IsZoomed () { - return _hasDecorations ? [Window isZoomed] : UndecoratedIsMaximized(); + return _decorations == SystemDecorationsFull ? [Window isZoomed] : UndecoratedIsMaximized(); } void DoZoom() { - if (_hasDecorations) - { - [Window performZoom:Window]; - } - else + switch (_decorations) { - if (!UndecoratedIsMaximized()) - { - _lastUndecoratedFrame = [Window frame]; - } + case SystemDecorationsNone: + case SystemDecorationsBorderOnly: + [Window setFrame:[Window screen].visibleFrame display:true]; + break; + - [Window zoom:Window]; + case SystemDecorationsFull: + [Window performZoom:Window]; + break; } } @@ -512,13 +634,69 @@ private: } } - virtual HRESULT SetHasDecorations(bool value) override + virtual HRESULT SetDecorations(SystemDecorations value) override { @autoreleasepool { - _hasDecorations = value; + auto currentWindowState = _lastWindowState; + _decorations = value; + + if(_fullScreenActive) + { + return S_OK; + } + + auto currentFrame = [Window frame]; + UpdateStyle(); + HideOrShowTrafficLights(); + + switch (_decorations) + { + case SystemDecorationsNone: + [Window setHasShadow:NO]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + + if(currentWindowState == Maximized) + { + if(!UndecoratedIsMaximized()) + { + DoZoom(); + } + } + break; + + case SystemDecorationsBorderOnly: + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + + if(currentWindowState == Maximized) + { + if(!UndecoratedIsMaximized()) + { + DoZoom(); + } + } + break; + + case SystemDecorationsFull: + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleVisible]; + [Window setTitlebarAppearsTransparent:NO]; + [Window setTitle:_lastTitle]; + + if(currentWindowState == Maximized) + { + auto newFrame = [Window contentRectForFrameRect:[Window frame]].size; + + [View setFrameSize:newFrame]; + } + break; + } + return S_OK; } } @@ -529,7 +707,6 @@ private: { _lastTitle = [NSString stringWithUTF8String:(const char*)utf8title]; [Window setTitle:_lastTitle]; - [Window setTitleVisibility:NSWindowTitleVisible]; return S_OK; } @@ -573,13 +750,19 @@ private: return E_POINTER; } + if(([Window styleMask] & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen) + { + *ret = FullScreen; + return S_OK; + } + if([Window isMiniaturized]) { *ret = Minimized; return S_OK; } - if([Window isZoomed]) + if(IsZoomed()) { *ret = Maximized; return S_OK; @@ -591,76 +774,180 @@ private: } } + void EnterFullScreenMode () + { + _fullScreenActive = true; + + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleVisible]; + [Window setTitlebarAppearsTransparent:NO]; + [Window setTitle:_lastTitle]; + + [Window setStyleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskResizable]; + + [Window toggleFullScreen:nullptr]; + } + + void ExitFullScreenMode () + { + [Window toggleFullScreen:nullptr]; + + _fullScreenActive = false; + + SetDecorations(_decorations); + } + virtual HRESULT SetWindowState (AvnWindowState state) override { @autoreleasepool { + if(_lastWindowState == state) + { + return S_OK; + } + + _inSetWindowState = true; + + auto currentState = _lastWindowState; _lastWindowState = state; - switch (state) { - case Maximized: - lastPositionSet.X = 0; - lastPositionSet.Y = 0; - - if([Window isMiniaturized]) - { - [Window deminiaturize:Window]; - } - - if(!IsZoomed()) - { - DoZoom(); - } - break; - - case Minimized: - [Window miniaturize:Window]; - break; - - default: - if([Window isMiniaturized]) - { - [Window deminiaturize:Window]; - } - - if(IsZoomed()) - { - DoZoom(); - } - break; + if(currentState == Normal) + { + _preZoomSize = [Window frame]; + } + + if(_shown) + { + switch (state) { + case Maximized: + if(currentState == FullScreen) + { + ExitFullScreenMode(); + } + + lastPositionSet.X = 0; + lastPositionSet.Y = 0; + + if([Window isMiniaturized]) + { + [Window deminiaturize:Window]; + } + + if(!IsZoomed()) + { + DoZoom(); + } + break; + + case Minimized: + if(currentState == FullScreen) + { + ExitFullScreenMode(); + } + else + { + [Window miniaturize:Window]; + } + break; + + case FullScreen: + if([Window isMiniaturized]) + { + [Window deminiaturize:Window]; + } + + EnterFullScreenMode(); + break; + + case Normal: + if([Window isMiniaturized]) + { + [Window deminiaturize:Window]; + } + + if(currentState == FullScreen) + { + ExitFullScreenMode(); + } + + if(IsZoomed()) + { + if(_decorations == SystemDecorationsFull) + { + DoZoom(); + } + else + { + [Window setFrame:_preZoomSize display:true]; + auto newFrame = [Window contentRectForFrameRect:[Window frame]].size; + + [View setFrameSize:newFrame]; + } + + } + break; + } } + _inSetWindowState = false; + return S_OK; } } - -protected: + virtual void OnResized () override { - auto windowState = [Window isMiniaturized] ? Minimized - : (IsZoomed() ? Maximized : Normal); - - if (windowState != _lastWindowState) + if(_shown && !_inSetWindowState && !_transitioningWindowState) { - _lastWindowState = windowState; - - WindowEvents->WindowStateChanged(windowState); + WindowStateChanged(); } } +protected: virtual NSWindowStyleMask GetStyle() override { unsigned long s = NSWindowStyleMaskBorderless; - if(_hasDecorations) - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable; - if(_canResize) - s = s | NSWindowStyleMaskResizable; + + switch (_decorations) + { + case SystemDecorationsNone: + s = s | NSWindowStyleMaskFullSizeContentView; + break; + + case SystemDecorationsBorderOnly: + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; + break; + + case SystemDecorationsFull: + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskBorderless; + + if(_canResize) + { + s = s | NSWindowStyleMaskResizable; + } + break; + } + + if([Window parentWindow] == nullptr) + { + s |= NSWindowStyleMaskMiniaturizable; + } return s; } }; NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode, NSRunLoopCommonModes, NSConnectionReplyMode, nil]; +@implementation AutoFitContentVisualEffectView +-(void)setFrameSize:(NSSize)newSize +{ + [super setFrameSize:newSize]; + if([[self subviews] count] == 0) + return; + [[self subviews][0] setFrameSize: newSize]; +} +@end + @implementation AvnView { ComPtr _parent; @@ -668,18 +955,24 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent AvnFramebuffer _swRenderedFrameBuffer; bool _queuedDisplayFromThread; NSTrackingArea* _area; - bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isMouseOver; + bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed, _isMouseOver; NSEvent* _lastMouseDownEvent; bool _lastKeyHandled; + AvnPixelSize _lastPixelSize; + NSObject* _renderTarget; } -- (void)dealloc +- (void)onClosed { + @synchronized (self) + { + _parent = nullptr; + } } -- (void)onClosed +-(AvnPixelSize) getPixelSize { - _parent = NULL; + return _lastPixelSize; } - (NSEvent*) lastMouseDownEvent @@ -687,15 +980,43 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return _lastMouseDownEvent; } +- (void) updateRenderTarget +{ + [_renderTarget resize:_lastPixelSize withScale: [[self window] backingScaleFactor]]; + [self setNeedsDisplayInRect:[self frame]]; +} + -(AvnView*) initWithParent: (WindowBaseImpl*) parent { self = [super init]; - [self setWantsBestResolutionOpenGLSurface:true]; + _renderTarget = parent->renderTarget; + [self setWantsLayer:YES]; + [self setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize]; + _parent = parent; _area = nullptr; + _lastPixelSize.Height = 100; + _lastPixelSize.Width = 100; + [self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]]; return self; } +- (BOOL)isFlipped +{ + return YES; +} + +- (BOOL)wantsUpdateLayer +{ + return YES; +} + +- (void)setLayer:(CALayer *)layer +{ + [_renderTarget setNewLayer: layer]; + [super setLayer: layer]; +} + - (BOOL)isOpaque { return YES; @@ -725,7 +1046,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [self removeTrackingArea:_area]; _area = nullptr; } - + + if (_parent == nullptr) + { + return; + } + NSRect rect = NSZeroRect; rect.size = newSize; @@ -734,81 +1060,36 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [self addTrackingArea:_area]; _parent->UpdateCursor(); - - _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}); -} - -- (void) drawFb: (AvnFramebuffer*) fb -{ - auto colorSpace = CGColorSpaceCreateDeviceRGB(); - auto dataProvider = CGDataProviderCreateWithData(NULL, fb->Data, fb->Height*fb->Stride, NULL); - - - auto image = CGImageCreate(fb->Width, fb->Height, 8, 32, fb->Stride, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast, - dataProvider, nullptr, false, kCGRenderingIntentDefault); - - auto ctx = [NSGraphicsContext currentContext]; - [ctx saveGraphicsState]; - auto cgc = [ctx CGContext]; - - CGContextDrawImage(cgc, CGRect{0,0, fb->Width/(fb->Dpi.X/96), fb->Height/(fb->Dpi.Y/96)}, image); - CGImageRelease(image); - CGColorSpaceRelease(colorSpace); - CGDataProviderRelease(dataProvider); - - [ctx restoreGraphicsState]; - + auto fsize = [self convertSizeToBacking: [self frame].size]; + _lastPixelSize.Width = (int)fsize.width; + _lastPixelSize.Height = (int)fsize.height; + [self updateRenderTarget]; + _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}); } -- (void)drawRect:(NSRect)dirtyRect +- (void)updateLayer { - _parent->BaseEvents->RunRenderPriorityJobs(); - @synchronized (self) { - if(_swRenderedFrame != NULL) - { - [self drawFb: &_swRenderedFrameBuffer]; - return; - } + AvnInsidePotentialDeadlock deadlock; + if (_parent == nullptr) + { + return; } - auto swOp = &_parent->CurrentSwDrawingOperation; + _parent->BaseEvents->RunRenderPriorityJobs(); _parent->BaseEvents->Paint(); - if(swOp->Data != NULL) - [self drawFb: &swOp->Desc]; - - swOp->Dealloc(); - return; } --(void) redrawSelf +- (void)drawRect:(NSRect)dirtyRect { - @autoreleasepool - { - @synchronized(self) - { - if(!_queuedDisplayFromThread) - return; - _queuedDisplayFromThread = false; - } - [self setNeedsDisplayInRect:[self frame]]; - [self display]; - - } + return; } -(void) setSwRenderedFrame: (AvnFramebuffer*) fb dispose: (IUnknown*) dispose { @autoreleasepool { - @synchronized (self) { - _swRenderedFrame = dispose; - _swRenderedFrameBuffer = *fb; - if(!_queuedDisplayFromThread) - { - _queuedDisplayFromThread = true; - [self performSelector:@selector(redrawSelf) onThread:[NSThread mainThread] withObject:NULL waitUntilDone:false modes: AllLoopModes]; - } - } + [_renderTarget setSwFrame:fb]; + dispose->Release(); } } @@ -830,24 +1111,45 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void) viewDidChangeBackingProperties { - _parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]); + auto fsize = [self convertSizeToBacking: [self frame].size]; + _lastPixelSize.Width = (int)fsize.width; + _lastPixelSize.Height = (int)fsize.height; + [self updateRenderTarget]; + + if(_parent != nullptr) + { + _parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]); + } + [super viewDidChangeBackingProperties]; } - (bool) ignoreUserInput { auto parentWindow = objc_cast([self window]); + if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents]) + { + auto window = dynamic_cast(_parent.getRaw()); + + if(window != nullptr) + { + window->WindowEvents->GotInputWhenDisabled(); + } + return TRUE; + } + return FALSE; } - (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type { if([self ignoreUserInput]) + { return; + } - [self becomeFirstResponder]; auto localPoint = [self convertPoint:[event locationInWindow] toView:self]; auto avnPoint = [self toAvnPoint:localPoint]; auto point = [self translateLocalPoint:avnPoint]; @@ -855,8 +1157,15 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent if(type == Wheel) { - delta.X = [event scrollingDeltaX] / 5; - delta.Y = [event scrollingDeltaY] / 5; + auto speed = 5; + + if([event hasPreciseScrollingDeltas]) + { + speed = 50; + } + + delta.X = [event scrollingDeltaX] / speed; + delta.Y = [event scrollingDeltaY] / speed; if(delta.X == 0 && delta.Y == 0) { @@ -867,11 +1176,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]; @@ -886,9 +1215,23 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void)otherMouseDown:(NSEvent *)event { - _isMiddlePressed = true; _lastMouseDownEvent = event; - [self mouseEvent:event withType:MiddleButtonDown]; + + switch(event.buttonNumber) + { + case 3: + _isMiddlePressed = true; + [self mouseEvent:event withType:MiddleButtonDown]; + break; + case 4: + _isXButton1Pressed = true; + [self mouseEvent:event withType:XButton1Down]; + break; + case 5: + _isXButton2Pressed = true; + [self mouseEvent:event withType:XButton2Down]; + break; + } } - (void)rightMouseDown:(NSEvent *)event @@ -906,8 +1249,21 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void)otherMouseUp:(NSEvent *)event { - _isMiddlePressed = false; - [self mouseEvent:event withType:MiddleButtonUp]; + switch(event.buttonNumber) + { + case 3: + _isMiddlePressed = false; + [self mouseEvent:event withType:MiddleButtonUp]; + break; + case 4: + _isXButton1Pressed = false; + [self mouseEvent:event withType:XButton1Up]; + break; + case 5: + _isXButton2Pressed = false; + [self mouseEvent:event withType:XButton2Up]; + break; + } } - (void)rightMouseUp:(NSEvent *)event @@ -956,13 +1312,19 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type { if([self ignoreUserInput]) + { return; + } + auto key = s_KeyMap[[event keyCode]]; 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 @@ -1006,6 +1368,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent rv |= MiddleMouseButton; if (_isRightPressed) rv |= RightMouseButton; + if (_isXButton1Pressed) + rv |= XButton1MouseButton; + if (_isXButton2Pressed) + rv |= XButton2MouseButton; return (AvnInputModifiers)rv; } @@ -1049,7 +1415,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]); + } } } @@ -1064,6 +1433,68 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return result; } + +- (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id )info +{ + auto localPoint = [self convertPoint:[info draggingLocation] toView:self]; + auto avnPoint = [self toAvnPoint:localPoint]; + auto point = [self translateLocalPoint:avnPoint]; + auto modifiers = [self getModifiers:[[NSApp currentEvent] modifierFlags]]; + NSDragOperation nsop = [info draggingSourceOperationMask]; + + auto effects = ConvertDragDropEffects(nsop); + int reffects = (int)_parent->BaseEvents + ->DragEvent(type, point, modifiers, effects, + CreateClipboard([info draggingPasteboard], nil), + GetAvnDataObjectHandleFromDraggingInfo(info)); + + NSDragOperation ret = 0; + + // Ensure that the managed part didn't add any new effects + reffects = (int)effects & (int)reffects; + + // OSX requires exactly one operation + if((reffects & (int)AvnDragDropEffects::Copy) != 0) + ret = NSDragOperationCopy; + else if((reffects & (int)AvnDragDropEffects::Move) != 0) + ret = NSDragOperationMove; + else if((reffects & (int)AvnDragDropEffects::Link) != 0) + ret = NSDragOperationLink; + if(ret == 0) + ret = NSDragOperationNone; + return ret; +} + +- (NSDragOperation)draggingEntered:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Enter info:sender]; +} + +- (NSDragOperation)draggingUpdated:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender]; +} + +- (void)draggingExited:(id )sender +{ + [self triggerAvnDragEvent: AvnDragEventType::Leave info:sender]; +} + +- (BOOL)prepareForDragOperation:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender] != NSDragOperationNone; +} + +- (BOOL)performDragOperation:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Drop info:sender] != NSDragOperationNone; +} + +- (void)concludeDragOperation:(nullable id )sender +{ + +} + @end @@ -1072,12 +1503,39 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent ComPtr _parent; bool _canBecomeKeyAndMain; bool _closed; - NSMenu* _menu; - bool _isAppMenuApplied; + bool _isEnabled; + AvnMenu* _menu; + double _lastScaling; +} + +-(double) getScaling +{ + return _lastScaling; } -- (void)dealloc ++(void)closeAll { + NSArray* windows = [NSArray arrayWithArray:[NSApp windows]]; + auto numWindows = [windows count]; + + for(int i = 0; i < numWindows; i++) + { + [[windows objectAtIndex:i] performClose:nil]; + } +} + +- (void)performClose:(id)sender +{ + if([[self delegate] respondsToSelector:@selector(windowShouldClose:)]) + { + if(![[self delegate] windowShouldClose:self]) return; + } + else if([self respondsToSelector:@selector(windowShouldClose:)]) + { + if(![self windowShouldClose:self]) return; + } + + [self close]; } - (void)pollModalSession:(nonnull NSModalSession)session @@ -1097,32 +1555,64 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } } --(void) applyMenu:(NSMenu *)menu +-(void) showWindowMenuWithAppMenu { - if(menu == nullptr) + if(_menu != nullptr) { - menu = [NSMenu new]; + auto appMenuItem = ::GetAppMenuItem(); + + if(appMenuItem != nullptr) + { + auto appMenu = [appMenuItem menu]; + + [appMenu removeItem:appMenuItem]; + + [_menu insertItem:appMenuItem atIndex:0]; + + [_menu setHasGlobalMenuItem:true]; + } + + [NSApp setMenu:_menu]; } +} + +-(void) showAppMenuOnly +{ + auto appMenuItem = ::GetAppMenuItem(); - _menu = menu; - - if ([self isKeyWindow]) + if(appMenuItem != nullptr) { - auto appMenu = ::GetAppMenuItem(); + auto appMenu = ::GetAppMenu(); + + auto nativeAppMenu = dynamic_cast(appMenu); + + [[appMenuItem menu] removeItem:appMenuItem]; - if(appMenu != nullptr) + if(_menu != nullptr) { - [[appMenu menu] removeItem:appMenu]; - - [_menu insertItem:appMenu atIndex:0]; - - _isAppMenuApplied = true; + [_menu setHasGlobalMenuItem:false]; } - [NSApp setMenu:menu]; + [nativeAppMenu->GetNative() addItem:appMenuItem]; + + [NSApp setMenu:nativeAppMenu->GetNative()]; + } + else + { + [NSApp setMenu:nullptr]; } } +-(void) applyMenu:(AvnMenu *)menu +{ + if(menu == nullptr) + { + menu = [AvnMenu new]; + } + + _menu = menu; +} + -(void) setCanBecomeKeyAndMain { _canBecomeKeyAndMain = true; @@ -1134,6 +1624,13 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [self setReleasedWhenClosed:false]; _parent = parent; [self setDelegate:self]; + _closed = false; + _isEnabled = true; + + _lastScaling = [self backingScaleFactor]; + [self setOpaque:NO]; + [self setBackgroundColor: [NSColor clearColor]]; + [self invalidateShadow]; return self; } @@ -1149,6 +1646,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return true; } +- (void)windowDidChangeBackingProperties:(NSNotification *)notification +{ + _lastScaling = [self backingScaleFactor]; +} + - (void)windowWillClose:(NSNotification *)notification { _closed = true; @@ -1159,9 +1661,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [self restoreParentWindow]; parent->BaseEvents->Closed(); [parent->View onClosed]; - dispatch_async(dispatch_get_main_queue(), ^{ - [self setContentView: nil]; - }); } } @@ -1193,14 +1692,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent -(bool)shouldTryToHandleEvents { - for(NSWindow* uch in [self childWindows]) - { - auto ch = objc_cast(uch); - if(ch == nil) - continue; - return FALSE; - } - return TRUE; + return _isEnabled; +} + +-(void) setEnabled:(bool)enable +{ + _isEnabled = enable; } -(void)makeKeyWindow @@ -1215,25 +1712,13 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { if([self activateAppropriateChild: true]) { - if(_menu == nullptr) - { - _menu = [NSMenu new]; - } + [self showWindowMenuWithAppMenu]; - auto appMenu = ::GetAppMenuItem(); - - if(appMenu != nullptr) + if(_parent != nullptr) { - [[appMenu menu] removeItem:appMenu]; - - [_menu insertItem:appMenu atIndex:0]; - - _isAppMenuApplied = true; + _parent->BaseEvents->Activated(); } - [NSApp setMenu:_menu]; - - _parent->BaseEvents->Activated(); [super becomeKeyWindow]; } } @@ -1268,58 +1753,95 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } } -- (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame +- (void)windowDidResize:(NSNotification *)notification { - return true; + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->WindowStateChanged(); + } } --(void)resignKeyWindow +- (void)windowWillExitFullScreen:(NSNotification *)notification { - if(_parent) - _parent->BaseEvents->Deactivated(); + auto parent = dynamic_cast(_parent.operator->()); - auto appMenuItem = ::GetAppMenuItem(); + if(parent != nullptr) + { + parent->StartStateTransition(); + } +} + +- (void)windowDidExitFullScreen:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); - if(appMenuItem != nullptr) + if(parent != nullptr) { - auto appMenu = ::GetAppMenu(); + parent->EndStateTransition(); - auto nativeAppMenu = dynamic_cast(appMenu); - - [[appMenuItem menu] removeItem:appMenuItem]; + if(parent->Decorations() != SystemDecorationsFull && parent->WindowState() == Maximized) + { + NSRect screenRect = [[self screen] visibleFrame]; + [self setFrame:screenRect display:YES]; + } - [nativeAppMenu->GetNative() addItem:appMenuItem]; + if(parent->WindowState() == Minimized) + { + [self miniaturize:nullptr]; + } - [NSApp setMenu:nativeAppMenu->GetNative()]; + parent->WindowStateChanged(); } - else +} + +- (void)windowWillEnterFullScreen:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) { - [NSApp setMenu:nullptr]; + parent->StartStateTransition(); } +} + +- (void)windowDidEnterFullScreen:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); - // remove window menu items from appmenu? - - [super resignKeyWindow]; + if(parent != nullptr) + { + parent->EndStateTransition(); + parent->WindowStateChanged(); + } } -- (void)windowDidMove:(NSNotification *)notification +- (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame { - AvnPoint position; - _parent->GetPosition(&position); - _parent->BaseEvents->PositionChanged(position); + return true; } -// TODO this breaks resizing. -/*- (void)windowDidResize:(NSNotification *)notification +-(void)resignKeyWindow { + if(_parent) + _parent->BaseEvents->Deactivated(); - auto parent = dynamic_cast(_parent.operator->()); + [self showAppMenuOnly]; - if(parent != nullptr) + [super resignKeyWindow]; +} + +- (void)windowDidMove:(NSNotification *)notification +{ + AvnPoint position; + + if(_parent != nullptr) { - parent->WindowStateChanged(); + _parent->GetPosition(&position); + _parent->BaseEvents->PositionChanged(position); } -}*/ +} @end class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup @@ -1331,7 +1853,7 @@ private: END_INTERFACE_MAP() virtual ~PopupImpl(){} ComPtr WindowEvents; - PopupImpl(IAvnWindowEvents* events) : WindowBaseImpl(events) + PopupImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) { WindowEvents = events; [Window setLevel:NSPopUpMenuWindowLevel]; @@ -1350,25 +1872,26 @@ protected: [Window setContentSize:NSSize{x, y}]; [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))]; + return S_OK; } } }; -extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events) +extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl) { @autoreleasepool { - IAvnPopup* ptr = dynamic_cast(new PopupImpl(events)); + IAvnPopup* ptr = dynamic_cast(new PopupImpl(events, gl)); return ptr; } } -extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events) +extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl) { @autoreleasepool { - IAvnWindow* ptr = (IAvnWindow*)new WindowImpl(events); + IAvnWindow* ptr = (IAvnWindow*)new WindowImpl(events, gl); return ptr; } } diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index dd2f27116d..890967ae4f 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -12,7 +12,9 @@ using Nuke.Common.ProjectModel; using Nuke.Common.Tooling; using Nuke.Common.Tools.DotNet; using Nuke.Common.Tools.MSBuild; +using Nuke.Common.Tools.Npm; using Nuke.Common.Utilities; +using Nuke.Common.Utilities.Collections; using static Nuke.Common.EnvironmentInfo; using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.IO.PathConstruction; @@ -26,11 +28,13 @@ using static Nuke.Common.Tools.VSWhere.VSWhereTasks; running and debugging a particular target (optionally without deps) would be way easier ReSharper/Rider - https://plugins.jetbrains.com/plugin/10803-nuke-support VSCode - https://marketplace.visualstudio.com/items?itemName=nuke.support - + */ partial class Build : NukeBuild { + [Solution("Avalonia.sln")] readonly Solution Solution; + static Lazy MsBuildExe = new Lazy(() => { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -54,7 +58,7 @@ partial class Build : NukeBuild protected override void OnBuildInitialized() { Parameters = new BuildParameters(this); - Information("Building version {0} of Avalonia ({1}) using version {2} of Nuke.", + Information("Building version {0} of Avalonia ({1}) using version {2} of Nuke.", Parameters.Version, Parameters.Configuration, typeof(NukeBuild).Assembly.GetName().Version.ToString()); @@ -93,29 +97,24 @@ partial class Build : NukeBuild string projectFile, Configure configurator = null) { - return MSBuild(projectFile, c => - { + return MSBuild(c => c + .SetProjectFile(projectFile) // This is required for VS2019 image on Azure Pipelines - if (Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure) - { - var javaSdk = Environment.GetEnvironmentVariable("JAVA_HOME_8_X64"); - if (javaSdk != null) - c = c.AddProperty("JavaSdkDirectory", javaSdk); - } - - c = c.AddProperty("PackageVersion", Parameters.Version) - .AddProperty("iOSRoslynPathHackRequired", "true") - .SetToolPath(MsBuildExe.Value) - .SetConfiguration(Parameters.Configuration) - .SetVerbosity(MSBuildVerbosity.Minimal); - c = configurator?.Invoke(c) ?? c; - return c; - }); + .When(Parameters.IsRunningOnWindows && + Parameters.IsRunningOnAzure, _ => _ + .AddProperty("JavaSdkDirectory", GetVariable("JAVA_HOME_8_X64"))) + .AddProperty("PackageVersion", Parameters.Version) + .AddProperty("iOSRoslynPathHackRequired", true) + .SetToolPath(MsBuildExe.Value) + .SetConfiguration(Parameters.Configuration) + .SetVerbosity(MSBuildVerbosity.Minimal) + .Apply(configurator)); } + Target Clean => _ => _.Executes(() => { - DeleteDirectories(Parameters.BuildDirs); - EnsureCleanDirectories(Parameters.BuildDirs); + Parameters.BuildDirs.ForEach(DeleteDirectory); + Parameters.BuildDirs.ForEach(EnsureCleanDirectory); EnsureCleanDirectory(Parameters.ArtifactsDir); EnsureCleanDirectory(Parameters.NugetIntermediateRoot); EnsureCleanDirectory(Parameters.NugetRoot); @@ -123,8 +122,21 @@ partial class Build : NukeBuild EnsureCleanDirectory(Parameters.TestResultsRoot); }); + Target CompileHtmlPreviewer => _ => _ + .DependsOn(Clean) + .Executes(() => + { + var webappDir = RootDirectory / "src" / "Avalonia.DesignerSupport" / "Remote" / "HtmlTransport" / "webapp"; + + NpmTasks.NpmInstall(c => c.SetWorkingDirectory(webappDir)); + NpmTasks.NpmRun(c => c + .SetWorkingDirectory(webappDir) + .SetCommand("dist")); + }); + Target Compile => _ => _ .DependsOn(Clean) + .DependsOn(CompileHtmlPreviewer) .Executes(() => { if (Parameters.IsRunningOnWindows) @@ -134,96 +146,84 @@ partial class Build : NukeBuild ); else - DotNetBuild(Parameters.MSBuildSolution, c => c + DotNetBuild(c => c + .SetProjectFile(Parameters.MSBuildSolution) .AddProperty("PackageVersion", Parameters.Version) .SetConfiguration(Parameters.Configuration) ); }); - - void RunCoreTest(string project) + + void RunCoreTest(string projectName) { - if(!project.EndsWith(".csproj")) - project = System.IO.Path.Combine(project, System.IO.Path.GetFileName(project)+".csproj"); - Information("Running tests from " + project); - XDocument xdoc; - using (var s = File.OpenRead(project)) - xdoc = XDocument.Load(s); - - List frameworks = null; - var targets = xdoc.Root.Descendants("TargetFrameworks").FirstOrDefault(); - if (targets != null) - frameworks = targets.Value.Split(';').Where(f => !string.IsNullOrWhiteSpace(f)).ToList(); - else - frameworks = new List {xdoc.Root.Descendants("TargetFramework").First().Value}; - - foreach(var fw in frameworks) + Information($"Running tests from {projectName}"); + var project = Solution.GetProject(projectName).NotNull("project != null"); + + foreach (var fw in project.GetTargetFrameworks()) { if (fw.StartsWith("net4") - && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && Environment.GetEnvironmentVariable("FORCE_LINUX_TESTS") != "1") { - Information($"Skipping {fw} tests on Linux - https://github.com/mono/mono/issues/13969"); + Information($"Skipping {projectName} ({fw}) tests on Linux - https://github.com/mono/mono/issues/13969"); continue; } - Information("Running for " + fw); - DotNetTest(c => - { - c = c - .SetProjectFile(project) - .SetConfiguration(Parameters.Configuration) - .SetFramework(fw) - .EnableNoBuild() - .EnableNoRestore(); - // NOTE: I can see that we could maybe add another extension method "Switch" or "If" to make this more convenient - if (Parameters.PublishTestResults) - c = c.SetLogger("trx").SetResultsDirectory(Parameters.TestResultsRoot); - return c; - }); + Information($"Running for {projectName} ({fw}) ..."); + + DotNetTest(c => c + .SetProjectFile(project) + .SetConfiguration(Parameters.Configuration) + .SetFramework(fw) + .EnableNoBuild() + .EnableNoRestore() + .When(Parameters.PublishTestResults, _ => _ + .SetLogger("trx") + .SetResultsDirectory(Parameters.TestResultsRoot))); } } Target RunCoreLibsTests => _ => _ - .OnlyWhen(() => !Parameters.SkipTests) + .OnlyWhenStatic(() => !Parameters.SkipTests) .DependsOn(Compile) .Executes(() => { - RunCoreTest("./tests/Avalonia.Animation.UnitTests"); - RunCoreTest("./tests/Avalonia.Base.UnitTests"); - RunCoreTest("./tests/Avalonia.Controls.UnitTests"); - RunCoreTest("./tests/Avalonia.Input.UnitTests"); - RunCoreTest("./tests/Avalonia.Interactivity.UnitTests"); - RunCoreTest("./tests/Avalonia.Layout.UnitTests"); - RunCoreTest("./tests/Avalonia.Markup.UnitTests"); - RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests"); - RunCoreTest("./tests/Avalonia.Styling.UnitTests"); - RunCoreTest("./tests/Avalonia.Visuals.UnitTests"); - RunCoreTest("./tests/Avalonia.Skia.UnitTests"); - RunCoreTest("./tests/Avalonia.ReactiveUI.UnitTests"); + RunCoreTest("Avalonia.Animation.UnitTests"); + RunCoreTest("Avalonia.Base.UnitTests"); + RunCoreTest("Avalonia.Controls.UnitTests"); + RunCoreTest("Avalonia.Controls.DataGrid.UnitTests"); + RunCoreTest("Avalonia.Input.UnitTests"); + RunCoreTest("Avalonia.Interactivity.UnitTests"); + RunCoreTest("Avalonia.Layout.UnitTests"); + RunCoreTest("Avalonia.Markup.UnitTests"); + RunCoreTest("Avalonia.Markup.Xaml.UnitTests"); + RunCoreTest("Avalonia.Styling.UnitTests"); + RunCoreTest("Avalonia.Visuals.UnitTests"); + RunCoreTest("Avalonia.Skia.UnitTests"); + RunCoreTest("Avalonia.ReactiveUI.UnitTests"); }); Target RunRenderTests => _ => _ - .OnlyWhen(() => !Parameters.SkipTests) + .OnlyWhenStatic(() => !Parameters.SkipTests) .DependsOn(Compile) .Executes(() => { - RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj"); + RunCoreTest("Avalonia.Skia.RenderTests"); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj"); + RunCoreTest("Avalonia.Direct2D1.RenderTests"); }); - + Target RunDesignerTests => _ => _ - .OnlyWhen(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) + .OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) .DependsOn(Compile) .Executes(() => { - RunCoreTest("./tests/Avalonia.DesignerSupport.Tests"); + RunCoreTest("Avalonia.DesignerSupport.Tests"); }); [PackageExecutable("JetBrains.dotMemoryUnit", "dotMemoryUnit.exe")] readonly Tool DotMemoryUnit; Target RunLeakTests => _ => _ - .OnlyWhen(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) + .OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) .DependsOn(Compile) .Executes(() => { @@ -234,7 +234,7 @@ partial class Build : NukeBuild }); Target ZipFiles => _ => _ - .After(CreateNugetPackages, Compile, RunCoreLibsTests, Package) + .After(CreateNugetPackages, Compile, RunCoreLibsTests, Package) .Executes(() => { var data = Parameters; @@ -258,9 +258,10 @@ partial class Build : NukeBuild MsBuildCommon(Parameters.MSBuildSolution, c => c .AddTargets("Pack")); else - DotNetPack(Parameters.MSBuildSolution, c => - c.SetConfiguration(Parameters.Configuration) - .AddProperty("PackageVersion", Parameters.Version)); + DotNetPack(c => c + .SetProject(Parameters.MSBuildSolution) + .SetConfiguration(Parameters.Configuration) + .AddProperty("PackageVersion", Parameters.Version)); }); Target CreateNugetPackages => _ => _ @@ -273,32 +274,40 @@ partial class Build : NukeBuild new NumergeNukeLogger())) throw new Exception("Package merge failed"); }); - + Target RunTests => _ => _ .DependsOn(RunCoreLibsTests) .DependsOn(RunRenderTests) .DependsOn(RunDesignerTests) .DependsOn(RunLeakTests); - + Target Package => _ => _ .DependsOn(RunTests) .DependsOn(CreateNugetPackages); - + Target CiAzureLinux => _ => _ .DependsOn(RunTests); - + Target CiAzureOSX => _ => _ .DependsOn(Package) .DependsOn(ZipFiles); - + Target CiAzureWindows => _ => _ .DependsOn(Package) .DependsOn(ZipFiles); - + public static int Main() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Execute(x => x.Package) : Execute(x => x.RunTests); } + +public static class ToolSettingsExtensions +{ + public static T Apply(this T settings, Configure configurator) + { + return configurator != null ? configurator(settings) : settings; + } +} diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs index 65ba5e9756..149716b416 100644 --- a/nukebuild/BuildParameters.cs +++ b/nukebuild/BuildParameters.cs @@ -4,24 +4,21 @@ using System.Linq; using System.Runtime.InteropServices; using System.Xml.Linq; using Nuke.Common; -using Nuke.Common.BuildServers; -using Nuke.Common.Execution; +using Nuke.Common.CI.AzurePipelines; using Nuke.Common.IO; -using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.IO.PathConstruction; -using static Nuke.Common.Tools.MSBuild.MSBuildTasks; public partial class Build { [Parameter("configuration")] public string Configuration { get; set; } - + [Parameter("skip-tests")] public bool SkipTests { get; set; } - + [Parameter("force-nuget-version")] public string ForceNugetVersion { get; set; } - + public class BuildParameters { public string Configuration { get; } @@ -79,15 +76,15 @@ public partial class Build IsRunningOnUnix = Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX; IsRunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - IsRunningOnAzure = Host == HostType.TeamServices || + IsRunningOnAzure = Host == HostType.AzurePipelines || Environment.GetEnvironmentVariable("LOGNAME") == "vsts"; if (IsRunningOnAzure) { - RepositoryName = TeamServices.Instance.RepositoryUri; - RepositoryBranch = TeamServices.Instance.SourceBranch; - IsPullRequest = TeamServices.Instance.PullRequestId.HasValue; - IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, TeamServices.Instance.RepositoryUri); + RepositoryName = AzurePipelines.Instance.RepositoryUri; + RepositoryBranch = AzurePipelines.Instance.SourceBranch; + IsPullRequest = AzurePipelines.Instance.PullRequestId.HasValue; + IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, AzurePipelines.Instance.RepositoryUri); } IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, 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/Shims.cs b/nukebuild/Shims.cs index 461d617643..1ac14bf622 100644 --- a/nukebuild/Shims.cs +++ b/nukebuild/Shims.cs @@ -19,9 +19,9 @@ public partial class Build Logger.Info(info, args); } - private void Zip(PathConstruction.AbsolutePath target, params string[] paths) => Zip(target, paths.AsEnumerable()); + private void Zip(AbsolutePath target, params string[] paths) => Zip(target, paths.AsEnumerable()); - private void Zip(PathConstruction.AbsolutePath target, IEnumerable paths) + private void Zip(AbsolutePath target, IEnumerable paths) { var targetPath = target.ToString(); bool finished = false, atLeastOneFileAdded = false; @@ -38,7 +38,7 @@ public partial class Build fileStream.CopyTo(entryStream); atLeastOneFileAdded = true; } - + foreach (var path in paths) { if (Directory.Exists(path)) @@ -64,7 +64,7 @@ public partial class Build finished = true; } - finally + finally { try { diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 2a736e4653..584c36d033 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.0 + netcoreapp3.1 false False @@ -10,7 +10,7 @@ - + @@ -20,11 +20,11 @@ - + - + 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 a455e2b3fc..84a62bb5c0 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -2,7 +2,22 @@ <_AvaloniaUseExternalMSBuild>$(AvaloniaUseExternalMSBuild) <_AvaloniaUseExternalMSBuild Condition="'$(_AvaloniaForceInternalMSBuild)' == 'true'">false + low + + + + + %(Filename) + Code + + + %(Filename) + Code + + + + + DependsOnTargets="$(BuildAvaloniaResourcesDependsOn)"> + + + + EmbeddedResources="@(EmbeddedResources)" + ReportImportance="$(AvaloniaXamlReportImportance)"/> @@ -53,6 +72,7 @@ $(IntermediateOutputPath)/Avalonia/references $(IntermediateOutputPath)/Avalonia/original.dll + false + 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 ee44a0cc3f..6a04c7e31e 100644 --- a/readme.md +++ b/readme.md @@ -1,61 +1,75 @@ - +[![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 WPF/UWP-inspired 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 and with experimental support for Android and iOS. + -**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. To see the status for some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239). +> **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. -| Control catalog | Desktop platforms | Mobile platforms | -|---|---|---| -| | | | +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! -## Getting Started +## 🚀 Getting Started -Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started. After installing it, open "New Project" dialog in Visual Studio, choose "Avalonia" in "Visual C#" section, select "Avalonia .NET Core Application" and press OK (screenshot). Now you can write code and markup that will work on multiple platforms! +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). -For those without Visual Studio, starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core). +Avalonia is delivered via NuGet package manager. You can find the packages here: https://www.nuget.org/packages/Avalonia/ -Avalonia is delivered via NuGet package manager. You can find the packages here: ([stable(ish)](https://www.nuget.org/packages/Avalonia/), [nightly](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed)) - -Use these commands in Package Manager console to install Avalonia manually: +Use these commands in the Package Manager console to install Avalonia manually: ``` Install-Package Avalonia Install-Package Avalonia.Desktop ``` -## Bleeding Edge Builds +## Showcase -or use nightly build feeds as described here: -https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed +Examples of UIs built with Avalonia +![image](https://user-images.githubusercontent.com/4672627/84707589-5b69a880-af35-11ea-87a6-7ad57a31d314.png) -## Documentation +![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. + +## Bleeding Edge Builds -You can take a look at the [getting started page](http://avaloniaui.net/docs/quickstart/) for an overview of how to get started but probably the best thing to do for now is to already know a little bit about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/AvaloniaUI/Avalonia). +We also have a [nightly build](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed) which tracks the current state of master. Although these packages are less stable than the release on NuGet.org, you'll get all the latest features and bugfixes right away and many of our users actually prefer this feed! -There's also a high-level [architecture document](http://avaloniaui.net/architecture/project-structure) that is currently a little bit out of date, and I've also started writing blog posts on Avalonia at http://grokys.github.io/. +## Documentation -Contributions for our docs are always welcome! +Documentation can be found on our website at http://avaloniaui.net/docs/. We also have a [tutorial](http://avaloniaui.net/docs/tutorial/) over there for newcomers. ## Building and Using -See the [build instructions here](http://avaloniaui.net/contributing/build). +See the [build instructions here](Documentation/build.md). ## Contributing -Please read the [contribution guidelines](http://avaloniaui.net/contributing/contributing) before submitting a pull request. +Please read the [contribution guidelines](CONTRIBUTING.md) before submitting a pull request. -### Contributors +## Code of Conduct -This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing/contributing)]. - +This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community. +For more information see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct). + +## Licence +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)]. + ### Backers @@ -63,7 +77,6 @@ Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com - ### Sponsors Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/Avalonia#sponsor)] @@ -78,3 +91,7 @@ Support this project by becoming a sponsor. Your logo will show up here with a l + +## .NET Foundation + +This project is supported by the [.NET Foundation](https://dotnetfoundation.org). 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 3005fdbc51..817023fd71 100644 --- a/samples/BindingDemo/BindingDemo.csproj +++ b/samples/BindingDemo/BindingDemo.csproj @@ -1,15 +1,15 @@  Exe - netcoreapp2.0;net461 + netcoreapp3.1 + - diff --git a/samples/BindingDemo/MainWindow.xaml b/samples/BindingDemo/MainWindow.xaml index 735e2d7102..14c371efef 100644 --- a/samples/BindingDemo/MainWindow.xaml +++ b/samples/BindingDemo/MainWindow.xaml @@ -75,11 +75,11 @@ - + - + diff --git a/samples/BindingDemo/ViewModels/DataAnnotationsErrorViewModel.cs b/samples/BindingDemo/ViewModels/DataAnnotationsErrorViewModel.cs index e274f9180e..391d1791d3 100644 --- a/samples/BindingDemo/ViewModels/DataAnnotationsErrorViewModel.cs +++ b/samples/BindingDemo/ViewModels/DataAnnotationsErrorViewModel.cs @@ -1,7 +1,4 @@ -// 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 System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace BindingDemo.ViewModels { diff --git a/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs b/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs index df80931367..0fe12a8ef7 100644 --- a/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs +++ b/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs @@ -1,7 +1,4 @@ -// 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 ReactiveUI; +using ReactiveUI; using System; namespace BindingDemo.ViewModels diff --git a/samples/BindingDemo/ViewModels/IndeiErrorViewModel.cs b/samples/BindingDemo/ViewModels/IndeiErrorViewModel.cs index bb3b4d64e9..caf75c846c 100644 --- a/samples/BindingDemo/ViewModels/IndeiErrorViewModel.cs +++ b/samples/BindingDemo/ViewModels/IndeiErrorViewModel.cs @@ -1,7 +1,4 @@ -// 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 ReactiveUI; +using ReactiveUI; using System; using System.ComponentModel; using System.Collections; diff --git a/samples/BindingDemo/ViewModels/MainWindowViewModel.cs b/samples/BindingDemo/ViewModels/MainWindowViewModel.cs index 300c696c34..66800a606c 100644 --- a/samples/BindingDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/BindingDemo/ViewModels/MainWindowViewModel.cs @@ -6,6 +6,7 @@ using System.Reactive.Linq; using System.Threading.Tasks; using System.Threading; using ReactiveUI; +using Avalonia.Controls; namespace BindingDemo.ViewModels { @@ -27,7 +28,7 @@ namespace BindingDemo.ViewModels Detail = "Item " + x + " details", })); - SelectedItems = new ObservableCollection(); + Selection = new SelectionModel(); ShuffleItems = ReactiveCommand.Create(() => { @@ -56,7 +57,7 @@ namespace BindingDemo.ViewModels } public ObservableCollection Items { get; } - public ObservableCollection SelectedItems { get; } + public SelectionModel Selection { get; } public ReactiveCommand ShuffleItems { get; } public string BooleanString diff --git a/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj b/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj index 054de2a05f..1a112d0d7d 100644 --- a/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj +++ b/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj @@ -10,6 +10,5 @@ - diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs index 2a8d288614..b2df1953f5 100644 --- a/samples/ControlCatalog.Desktop/Program.cs +++ b/samples/ControlCatalog.Desktop/Program.cs @@ -2,10 +2,8 @@ using System; using System.Linq; using Avalonia; using Avalonia.Controls; -using Avalonia.Logging.Serilog; using Avalonia.Platform; using Avalonia.ReactiveUI; -using Serilog; namespace ControlCatalog { diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index 7919c3ac5a..772af9eacd 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.0 + netcoreapp3.1 true diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 854cae484c..5df8c1be64 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -4,8 +4,8 @@ using System.Globalization; using System.Linq; using System.Threading; using Avalonia; -using Avalonia.ReactiveUI; using Avalonia.Dialogs; +using Avalonia.ReactiveUI; namespace ControlCatalog.NetCore { @@ -67,7 +67,8 @@ namespace ControlCatalog.NetCore }) .UseSkia() .UseReactiveUI() - .UseManagedSystemDialogs(); + .UseManagedSystemDialogs() + .LogToDebug(); static void SilenceConsole() { diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 335c460b40..c7a75f5a70 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -1,9 +1,7 @@ - - - + - - - - - - - - - - - - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 4fc63ea054..a6754143a2 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -1,27 +1,67 @@ using System; using Avalonia; -using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Styling; +using Avalonia.Styling; namespace ControlCatalog { public class App : Application { - private NativeMenu _recentMenu; + public static Styles FluentDark = new Styles + { + new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) + { + Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseDark.xaml?assembly=Avalonia.Themes.Default") + }, + new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) + { + Source = new Uri("resm:Avalonia.Themes.Fluent.Accents.FluentDark.xaml?assembly=Avalonia.Themes.Fluent") + }, + }; - public override void Initialize() + public static Styles FluentLight = new Styles { - AvaloniaXamlLoader.Load(this); + new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) + { + Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default") + }, + new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) + { + Source = new Uri("resm:Avalonia.Themes.Fluent.Accents.FluentLight.xaml?assembly=Avalonia.Themes.Fluent") + }, + }; - Name = "Avalonia"; + public static Styles DefaultLight = new Styles + { + new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) + { + Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default") + }, + new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) + { + Source = new Uri("resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default") + }, + }; - _recentMenu = (NativeMenu.GetMenu(this).Items[1] as NativeMenuItem).Menu; - } + public static Styles DefaultDark = new Styles + { + new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) + { + Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseDark.xaml?assembly=Avalonia.Themes.Default") + }, + new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) + { + Source = new Uri("resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default") + }, + }; - public void OnOpenClicked(object sender, EventArgs args) + public override void Initialize() { - _recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1))); + AvaloniaXamlLoader.Load(this); + + Styles.Insert(0, FluentDark); } public override void OnFrameworkInitializationCompleted() @@ -30,7 +70,7 @@ namespace ControlCatalog desktopLifetime.MainWindow = new MainWindow(); else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) singleViewLifetime.MainView = new MainView(); - + base.OnFrameworkInitializationCompleted(); } } diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 4c2769bcc4..8a88b89b48 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -1,6 +1,7 @@  - netstandard2.0 + netstandard2.0 + true @@ -17,14 +18,15 @@ + + - diff --git a/samples/ControlCatalog/DecoratedWindow.xaml.cs b/samples/ControlCatalog/DecoratedWindow.xaml.cs index 2e7218b956..bdf5b8fbee 100644 --- a/samples/ControlCatalog/DecoratedWindow.xaml.cs +++ b/samples/ControlCatalog/DecoratedWindow.xaml.cs @@ -18,18 +18,18 @@ namespace ControlCatalog { var ctl = this.FindControl(name); ctl.Cursor = new Cursor(cursor); - ctl.PointerPressed += delegate + ctl.PointerPressed += (i, e) => { - PlatformImpl?.BeginResizeDrag(edge); + PlatformImpl?.BeginResizeDrag(edge, e); }; } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); - this.FindControl("TitleBar").PointerPressed += delegate + this.FindControl("TitleBar").PointerPressed += (i, e) => { - PlatformImpl?.BeginMoveDrag(); + PlatformImpl?.BeginMoveDrag(e); }; SetupSide("Left", StandardCursorType.LeftSide, WindowEdge.West); SetupSide("Right", StandardCursorType.RightSide, WindowEdge.East); diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 874560a294..488062f5b6 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -2,7 +2,7 @@ xmlns:pages="clr-namespace:ControlCatalog.Pages" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.MainView" - Background="{DynamicResource ThemeBackgroundBrush}" + Background="Transparent" Foreground="{DynamicResource ThemeForegroundBrush}" FontSize="{DynamicResource FontSizeNormal}"> @@ -29,10 +29,15 @@ ScrollViewer.HorizontalScrollBarVisibility="Disabled"> - + + - + + + @@ -43,21 +48,41 @@ + + + + - - Light - Dark - + + + No Decorations + Border Only + Full Decorations + + + Fluent - Dark + Fluent - Light + Simple - Light + Simple - Dark + + + None + Transparent + Blur + AcrylicBlur + + + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index acb9bc5bc6..d6d4a71ad3 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -32,30 +32,47 @@ 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; } + }; + + var decorations = this.Find("Decorations"); + decorations.SelectionChanged += (sender, e) => + { + 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; }; - Styles.Add(light); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + var decorations = this.Find("Decorations"); + if (VisualRoot is Window window) + decorations.SelectedIndex = (int)window.SystemDecorations; } } } diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 6088f2ec57..76422bc130 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -7,22 +7,23 @@ 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"> - + x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" Background="{DynamicResource SystemControlPageBackgroundAltHighBrush}"> - + - + - + @@ -34,15 +35,41 @@ + + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 7b0ee897c4..d97325ef8d 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -1,13 +1,11 @@ +using System; +using System.Runtime.InteropServices; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Notifications; -using Avalonia.Controls.Primitives; +using Avalonia.Input; using Avalonia.Markup.Xaml; -using Avalonia.Threading; using ControlCatalog.ViewModels; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; namespace ControlCatalog { @@ -31,20 +29,35 @@ namespace ControlCatalog DataContext = new MainWindowViewModel(_notificationArea); _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[2] as NativeMenuItem).Menu; + + var mainMenu = this.FindControl("MainMenu"); + mainMenu.AttachedToVisualTree += MenuAttached; + } + + public static string MenuQuitHeader => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Quit Avalonia" : "E_xit"; + + public static KeyGesture MenuQuitGesture => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? + new KeyGesture(Key.Q, KeyModifiers.Meta) : + new KeyGesture(Key.F4, KeyModifiers.Alt); + + public void MenuAttached(object sender, VisualTreeAttachmentEventArgs e) + { + if (NativeMenu.GetIsNativeMenuExported(this) && sender is Menu mainMenu) + { + mainMenu.IsVisible = false; + } } public void OnOpenClicked(object sender, EventArgs args) { _recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1))); } - + public void OnCloseClicked(object sender, EventArgs args) { Close(); } - - private void InitializeComponent() { // TODO: iOS does not support dynamically loading assemblies 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/BorderPage.xaml b/samples/ControlCatalog/Pages/BorderPage.xaml index c30056d5e5..8133d0e408 100644 --- a/samples/ControlCatalog/Pages/BorderPage.xaml +++ b/samples/ControlCatalog/Pages/BorderPage.xaml @@ -29,6 +29,13 @@ Padding="16"> Rounded Corners + + + + diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml b/samples/ControlCatalog/Pages/ButtonPage.xaml index 39d89590c2..8b697b7948 100644 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml +++ b/samples/ControlCatalog/Pages/ButtonPage.xaml @@ -24,13 +24,18 @@ - + + + + + + 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..486cc55d44 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 diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs index 3eb6d5b595..d50b051d9f 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs @@ -1,5 +1,7 @@ +using System.Linq; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.Media; namespace ControlCatalog.Pages { @@ -14,7 +16,7 @@ namespace ControlCatalog.Pages { AvaloniaXamlLoader.Load(this); var fontComboBox = this.Find("fontComboBox"); - fontComboBox.Items = Avalonia.Media.FontFamily.SystemFontFamilies; + fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x)); fontComboBox.SelectedIndex = 0; } } diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml b/samples/ControlCatalog/Pages/DialogsPage.xaml index 60f8e3656e..a0e82663bf 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml @@ -6,8 +6,12 @@ + + + + diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index d207689223..cf6c771e34 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -1,5 +1,9 @@ +using System; using System.Collections.Generic; +using System.Linq; +using System.Reflection; using Avalonia.Controls; +using Avalonia.Dialogs; using Avalonia.Markup.Xaml; #pragma warning disable 4014 @@ -34,7 +38,9 @@ namespace ControlCatalog.Pages new OpenFileDialog() { Title = "Open file", - Filters = GetFilters() + Filters = GetFilters(), + // Almost guaranteed to exist + InitialFileName = Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName }.ShowAsync(GetWindow()); }; this.FindControl + + + - + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs index decd849adc..cce80a2d3c 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -5,22 +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 diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml b/samples/ControlCatalog/Pages/MenuPage.xaml index 868f0df6ad..de9ea34e80 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml +++ b/samples/ControlCatalog/Pages/MenuPage.xaml @@ -16,13 +16,13 @@ Defined in XAML - + - + diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml index e605a92da0..0d7e5da17f 100644 --- a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml +++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml @@ -50,22 +50,22 @@ Text: - + Minimum: + CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Margin="2" HorizontalAlignment="Center"/> Maximum: + CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Margin="2" HorizontalAlignment="Center"/> Increment: + Margin="2" HorizontalAlignment="Center"/> Value: + Margin="2" HorizontalAlignment="Center"/> @@ -73,7 +73,7 @@ Usage of NumericUpDown: diff --git a/samples/ControlCatalog/Pages/OpenGlPage.xaml b/samples/ControlCatalog/Pages/OpenGlPage.xaml new file mode 100644 index 0000000000..0eb09004ba --- /dev/null +++ b/samples/ControlCatalog/Pages/OpenGlPage.xaml @@ -0,0 +1,29 @@ + + + + + + + + + Yaw + + Pitch + + Roll + + + D + I + S + C + O + + + + + + diff --git a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs new file mode 100644 index 0000000000..6c13a5ac22 --- /dev/null +++ b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs @@ -0,0 +1,401 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using Avalonia; +using Avalonia.Controls; +using Avalonia.OpenGL; +using Avalonia.Platform.Interop; +using Avalonia.Threading; +using static Avalonia.OpenGL.GlConsts; +// ReSharper disable StringLiteralTypo + +namespace ControlCatalog.Pages +{ + public class OpenGlPage : UserControl + { + + } + + public class OpenGlPageControl : OpenGlControlBase + { + private float _yaw; + + public static readonly DirectProperty YawProperty = + AvaloniaProperty.RegisterDirect("Yaw", o => o.Yaw, (o, v) => o.Yaw = v); + + public float Yaw + { + get => _yaw; + set => SetAndRaise(YawProperty, ref _yaw, value); + } + + private float _pitch; + + public static readonly DirectProperty PitchProperty = + AvaloniaProperty.RegisterDirect("Pitch", o => o.Pitch, (o, v) => o.Pitch = v); + + public float Pitch + { + get => _pitch; + set => SetAndRaise(PitchProperty, ref _pitch, value); + } + + + private float _roll; + + public static readonly DirectProperty RollProperty = + AvaloniaProperty.RegisterDirect("Roll", o => o.Roll, (o, v) => o.Roll = v); + + public float Roll + { + get => _roll; + set => SetAndRaise(RollProperty, ref _roll, value); + } + + + private float _disco; + + public static readonly DirectProperty DiscoProperty = + AvaloniaProperty.RegisterDirect("Disco", o => o.Disco, (o, v) => o.Disco = v); + + public float Disco + { + get => _disco; + set => SetAndRaise(DiscoProperty, ref _disco, value); + } + + private string _info; + + public static readonly DirectProperty InfoProperty = + AvaloniaProperty.RegisterDirect("Info", o => o.Info, (o, v) => o.Info = v); + + public string Info + { + get => _info; + private set => SetAndRaise(InfoProperty, ref _info, value); + } + + static OpenGlPageControl() + { + AffectsRender(YawProperty, PitchProperty, RollProperty, DiscoProperty); + } + + private int _vertexShader; + private int _fragmentShader; + private int _shaderProgram; + private int _vertexBufferObject; + private int _indexBufferObject; + private int _vertexArrayObject; + private GlExtrasInterface _glExt; + + private string GetShader(bool fragment, string shader) + { + var version = (GlVersion.Type == GlProfileType.OpenGL ? + RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 150 : 120 : + 100); + var data = "#version " + version + "\n"; + if (GlVersion.Type == GlProfileType.OpenGLES) + data += "precision mediump float;\n"; + if (version >= 150) + { + shader = shader.Replace("attribute", "in"); + if (fragment) + shader = shader + .Replace("varying", "in") + .Replace("//DECLAREGLFRAG", "out vec4 outFragColor;") + .Replace("gl_FragColor", "outFragColor"); + else + shader = shader.Replace("varying", "out"); + } + + data += shader; + + return data; + } + + + private string VertexShaderSource => GetShader(false, @" + attribute vec3 aPos; + attribute vec3 aNormal; + uniform mat4 uModel; + uniform mat4 uProjection; + uniform mat4 uView; + + varying vec3 FragPos; + varying vec3 VecPos; + varying vec3 Normal; + uniform float uTime; + uniform float uDisco; + void main() + { + float discoScale = sin(uTime * 10.0) / 10.0; + float distortionX = 1.0 + uDisco * cos(uTime * 20.0) / 10.0; + + float scale = 1.0 + uDisco * discoScale; + + vec3 scaledPos = aPos; + scaledPos.x = scaledPos.x * distortionX; + + scaledPos *= scale; + gl_Position = uProjection * uView * uModel * vec4(scaledPos, 1.0); + FragPos = vec3(uModel * vec4(aPos, 1.0)); + VecPos = aPos; + Normal = normalize(vec3(uModel * vec4(aNormal, 1.0))); + } +"); + + private string FragmentShaderSource => GetShader(true, @" + varying vec3 FragPos; + varying vec3 VecPos; + varying vec3 Normal; + uniform float uMaxY; + uniform float uMinY; + uniform float uTime; + uniform float uDisco; + //DECLAREGLFRAG + + void main() + { + float y = (VecPos.y - uMinY) / (uMaxY - uMinY); + float c = cos(atan(VecPos.x, VecPos.z) * 20.0 + uTime * 40.0 + y * 50.0); + float s = sin(-atan(VecPos.z, VecPos.x) * 20.0 - uTime * 20.0 - y * 30.0); + + vec3 discoColor = vec3( + 0.5 + abs(0.5 - y) * cos(uTime * 10.0), + 0.25 + (smoothstep(0.3, 0.8, y) * (0.5 - c / 4.0)), + 0.25 + abs((smoothstep(0.1, 0.4, y) * (0.5 - s / 4.0)))); + + vec3 objectColor = vec3((1.0 - y), 0.40 + y / 4.0, y * 0.75 + 0.25); + objectColor = objectColor * (1.0 - uDisco) + discoColor * uDisco; + + float ambientStrength = 0.3; + vec3 lightColor = vec3(1.0, 1.0, 1.0); + vec3 lightPos = vec3(uMaxY * 2.0, uMaxY * 2.0, uMaxY * 2.0); + vec3 ambient = ambientStrength * lightColor; + + + vec3 norm = normalize(Normal); + vec3 lightDir = normalize(lightPos - FragPos); + + float diff = max(dot(norm, lightDir), 0.0); + vec3 diffuse = diff * lightColor; + + vec3 result = (ambient + diffuse) * objectColor; + gl_FragColor = vec4(result, 1.0); + + } +"); + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + private struct Vertex + { + public Vector3 Position; + public Vector3 Normal; + } + + private readonly Vertex[] _points; + private readonly ushort[] _indices; + private readonly float _minY; + private readonly float _maxY; + + + public OpenGlPageControl() + { + var name = typeof(OpenGlPage).Assembly.GetManifestResourceNames().First(x => x.Contains("teapot.bin")); + using (var sr = new BinaryReader(typeof(OpenGlPage).Assembly.GetManifestResourceStream(name))) + { + var buf = new byte[sr.ReadInt32()]; + sr.Read(buf, 0, buf.Length); + var points = new float[buf.Length / 4]; + Buffer.BlockCopy(buf, 0, points, 0, buf.Length); + buf = new byte[sr.ReadInt32()]; + sr.Read(buf, 0, buf.Length); + _indices = new ushort[buf.Length / 2]; + Buffer.BlockCopy(buf, 0, _indices, 0, buf.Length); + _points = new Vertex[points.Length / 3]; + for (var primitive = 0; primitive < points.Length / 3; primitive++) + { + var srci = primitive * 3; + _points[primitive] = new Vertex + { + Position = new Vector3(points[srci], points[srci + 1], points[srci + 2]) + }; + } + + for (int i = 0; i < _indices.Length; i += 3) + { + Vector3 a = _points[_indices[i]].Position; + Vector3 b = _points[_indices[i + 1]].Position; + Vector3 c = _points[_indices[i + 2]].Position; + var normal = Vector3.Normalize(Vector3.Cross(c - b, a - b)); + + _points[_indices[i]].Normal += normal; + _points[_indices[i + 1]].Normal += normal; + _points[_indices[i + 2]].Normal += normal; + } + + for (int i = 0; i < _points.Length; i++) + { + _points[i].Normal = Vector3.Normalize(_points[i].Normal); + _maxY = Math.Max(_maxY, _points[i].Position.Y); + _minY = Math.Min(_minY, _points[i].Position.Y); + } + } + + } + + private void CheckError(GlInterface gl) + { + int err; + while ((err = gl.GetError()) != GL_NO_ERROR) + Console.WriteLine(err); + } + + protected unsafe override void OnOpenGlInit(GlInterface GL, int fb) + { + CheckError(GL); + _glExt = new GlExtrasInterface(GL); + + Info = $"Renderer: {GL.GetString(GL_RENDERER)} Version: {GL.GetString(GL_VERSION)}"; + + // Load the source of the vertex shader and compile it. + _vertexShader = GL.CreateShader(GL_VERTEX_SHADER); + Console.WriteLine(GL.CompileShaderAndGetError(_vertexShader, VertexShaderSource)); + + // Load the source of the fragment shader and compile it. + _fragmentShader = GL.CreateShader(GL_FRAGMENT_SHADER); + Console.WriteLine(GL.CompileShaderAndGetError(_fragmentShader, FragmentShaderSource)); + + // Create the shader program, attach the vertex and fragment shaders and link the program. + _shaderProgram = GL.CreateProgram(); + GL.AttachShader(_shaderProgram, _vertexShader); + GL.AttachShader(_shaderProgram, _fragmentShader); + const int positionLocation = 0; + const int normalLocation = 1; + GL.BindAttribLocationString(_shaderProgram, positionLocation, "aPos"); + GL.BindAttribLocationString(_shaderProgram, normalLocation, "aNormal"); + Console.WriteLine(GL.LinkProgramAndGetError(_shaderProgram)); + CheckError(GL); + + // Create the vertex buffer object (VBO) for the vertex data. + _vertexBufferObject = GL.GenBuffer(); + // Bind the VBO and copy the vertex data into it. + GL.BindBuffer(GL_ARRAY_BUFFER, _vertexBufferObject); + CheckError(GL); + var vertexSize = Marshal.SizeOf(); + fixed (void* pdata = _points) + GL.BufferData(GL_ARRAY_BUFFER, new IntPtr(_points.Length * vertexSize), + new IntPtr(pdata), GL_STATIC_DRAW); + + _indexBufferObject = GL.GenBuffer(); + GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBufferObject); + CheckError(GL); + fixed (void* pdata = _indices) + GL.BufferData(GL_ELEMENT_ARRAY_BUFFER, new IntPtr(_indices.Length * sizeof(ushort)), new IntPtr(pdata), + GL_STATIC_DRAW); + CheckError(GL); + _vertexArrayObject = _glExt.GenVertexArray(); + _glExt.BindVertexArray(_vertexArrayObject); + CheckError(GL); + GL.VertexAttribPointer(positionLocation, 3, GL_FLOAT, + 0, vertexSize, IntPtr.Zero); + GL.VertexAttribPointer(normalLocation, 3, GL_FLOAT, + 0, vertexSize, new IntPtr(12)); + GL.EnableVertexAttribArray(positionLocation); + GL.EnableVertexAttribArray(normalLocation); + CheckError(GL); + + } + + protected override void OnOpenGlDeinit(GlInterface GL, int fb) + { + // Unbind everything + GL.BindBuffer(GL_ARRAY_BUFFER, 0); + GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + _glExt.BindVertexArray(0); + GL.UseProgram(0); + + // Delete all resources. + GL.DeleteBuffers(2, new[] { _vertexBufferObject, _indexBufferObject }); + _glExt.DeleteVertexArrays(1, new[] { _vertexArrayObject }); + GL.DeleteProgram(_shaderProgram); + GL.DeleteShader(_fragmentShader); + GL.DeleteShader(_vertexShader); + } + + static Stopwatch St = Stopwatch.StartNew(); + protected override unsafe void OnOpenGlRender(GlInterface gl, int fb) + { + gl.ClearColor(0, 0, 0, 0); + gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + gl.Enable(GL_DEPTH_TEST); + gl.Viewport(0, 0, (int)Bounds.Width, (int)Bounds.Height); + var GL = gl; + + GL.BindBuffer(GL_ARRAY_BUFFER, _vertexBufferObject); + GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBufferObject); + _glExt.BindVertexArray(_vertexArrayObject); + GL.UseProgram(_shaderProgram); + CheckError(GL); + var projection = + Matrix4x4.CreatePerspectiveFieldOfView((float)(Math.PI / 4), (float)(Bounds.Width / Bounds.Height), + 0.01f, 1000); + + + var view = Matrix4x4.CreateLookAt(new Vector3(25, 25, 25), new Vector3(), new Vector3(0, -1, 0)); + var model = Matrix4x4.CreateFromYawPitchRoll(_yaw, _pitch, _roll); + var modelLoc = GL.GetUniformLocationString(_shaderProgram, "uModel"); + var viewLoc = GL.GetUniformLocationString(_shaderProgram, "uView"); + var projectionLoc = GL.GetUniformLocationString(_shaderProgram, "uProjection"); + var maxYLoc = GL.GetUniformLocationString(_shaderProgram, "uMaxY"); + var minYLoc = GL.GetUniformLocationString(_shaderProgram, "uMinY"); + var timeLoc = GL.GetUniformLocationString(_shaderProgram, "uTime"); + var discoLoc = GL.GetUniformLocationString(_shaderProgram, "uDisco"); + GL.UniformMatrix4fv(modelLoc, 1, false, &model); + GL.UniformMatrix4fv(viewLoc, 1, false, &view); + GL.UniformMatrix4fv(projectionLoc, 1, false, &projection); + GL.Uniform1f(maxYLoc, _maxY); + GL.Uniform1f(minYLoc, _minY); + GL.Uniform1f(timeLoc, (float)St.Elapsed.TotalSeconds); + GL.Uniform1f(discoLoc, _disco); + CheckError(GL); + GL.DrawElements(GL_TRIANGLES, _indices.Length, GL_UNSIGNED_SHORT, IntPtr.Zero); + + CheckError(GL); + if (_disco > 0.01) + Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); + } + + class GlExtrasInterface : GlInterfaceBase + { + public GlExtrasInterface(GlInterface gl) : base(gl.GetProcAddress, gl.ContextInfo) + { + } + + public delegate void GlDeleteVertexArrays(int count, int[] buffers); + [GlMinVersionEntryPoint("glDeleteVertexArrays", 3,0)] + [GlExtensionEntryPoint("glDeleteVertexArraysOES", "GL_OES_vertex_array_object")] + public GlDeleteVertexArrays DeleteVertexArrays { get; } + + public delegate void GlBindVertexArray(int array); + [GlMinVersionEntryPoint("glBindVertexArray", 3,0)] + [GlExtensionEntryPoint("glBindVertexArrayOES", "GL_OES_vertex_array_object")] + public GlBindVertexArray BindVertexArray { get; } + public delegate void GlGenVertexArrays(int n, int[] rv); + + [GlMinVersionEntryPoint("glGenVertexArrays",3,0)] + [GlExtensionEntryPoint("glGenVertexArraysOES", "GL_OES_vertex_array_object")] + public GlGenVertexArrays GenVertexArrays { get; } + + public int GenVertexArray() + { + var rv = new int[1]; + GenVertexArrays(1, rv); + return rv[0]; + } + } + } +} diff --git a/samples/ControlCatalog/Pages/ProgressBarPage.xaml b/samples/ControlCatalog/Pages/ProgressBarPage.xaml index 39bbf391bb..13bae59805 100644 --- a/samples/ControlCatalog/Pages/ProgressBarPage.xaml +++ b/samples/ControlCatalog/Pages/ProgressBarPage.xaml @@ -6,15 +6,19 @@ A progress bar control + - + - + diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml new file mode 100644 index 0000000000..bbefcab9ca --- /dev/null +++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml @@ -0,0 +1,35 @@ + + + ScrollViewer + Allows for horizontal and vertical content scrolling. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs new file mode 100644 index 0000000000..36d3768b13 --- /dev/null +++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Markup.Xaml; +using ReactiveUI; + +namespace ControlCatalog.Pages +{ + public class ScrollViewerPageViewModel : ReactiveObject + { + private bool _allowAutoHide; + private ScrollBarVisibility _horizontalScrollVisibility; + private ScrollBarVisibility _verticalScrollVisibility; + + public ScrollViewerPageViewModel() + { + AvailableVisibility = new List + { + ScrollBarVisibility.Auto, + ScrollBarVisibility.Visible, + ScrollBarVisibility.Hidden, + ScrollBarVisibility.Disabled, + }; + + HorizontalScrollVisibility = ScrollBarVisibility.Auto; + VerticalScrollVisibility = ScrollBarVisibility.Auto; + AllowAutoHide = true; + } + + public bool AllowAutoHide + { + get => _allowAutoHide; + set => this.RaiseAndSetIfChanged(ref _allowAutoHide, value); + } + + public ScrollBarVisibility HorizontalScrollVisibility + { + get => _horizontalScrollVisibility; + set => this.RaiseAndSetIfChanged(ref _horizontalScrollVisibility, value); + } + + public ScrollBarVisibility VerticalScrollVisibility + { + get => _verticalScrollVisibility; + set => this.RaiseAndSetIfChanged(ref _verticalScrollVisibility, value); + } + + public List AvailableVisibility { get; } + } + + public class ScrollViewerPage : UserControl + { + public ScrollViewerPage() + { + InitializeComponent(); + + DataContext = new ScrollViewerPageViewModel(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/Pages/SliderPage.xaml b/samples/ControlCatalog/Pages/SliderPage.xaml index 58f7b881fe..c6f5521e60 100644 --- a/samples/ControlCatalog/Pages/SliderPage.xaml +++ b/samples/ControlCatalog/Pages/SliderPage.xaml @@ -9,12 +9,14 @@ diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml new file mode 100644 index 0000000000..f73ef9b4fb --- /dev/null +++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml @@ -0,0 +1,134 @@ + + + TextBlock + A control that can display text + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml.cs b/samples/ControlCatalog/Pages/TextBlockPage.xaml.cs new file mode 100644 index 0000000000..49fecbe7c5 --- /dev/null +++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class TextBlockPage : UserControl + { + public TextBlockPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml b/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml new file mode 100644 index 0000000000..161ee2ee16 --- /dev/null +++ b/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml.cs b/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml.cs new file mode 100644 index 0000000000..66f7d14c7f --- /dev/null +++ b/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class ToggleSwitchPage : UserControl + { + public ToggleSwitchPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/Pages/ToolTipPage.xaml b/samples/ControlCatalog/Pages/ToolTipPage.xaml index cbe1e3059c..73d83e08f1 100644 --- a/samples/ControlCatalog/Pages/ToolTipPage.xaml +++ b/samples/ControlCatalog/Pages/ToolTipPage.xaml @@ -18,7 +18,7 @@ ToolTip.Tip="This is a ToolTip"> Hover Here - - + @@ -19,8 +19,8 @@ - + Single diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs index 1f35f05f1d..3fb990459f 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs @@ -1,9 +1,6 @@ -using System.Collections.ObjectModel; -using System.Linq; -using System.Reactive; using Avalonia.Controls; using Avalonia.Markup.Xaml; -using ReactiveUI; +using ControlCatalog.ViewModels; namespace ControlCatalog.Pages { @@ -12,104 +9,12 @@ namespace ControlCatalog.Pages public TreeViewPage() { InitializeComponent(); - DataContext = new PageViewModel(); + DataContext = new TreeViewPageViewModel(); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } - - private class PageViewModel : ReactiveObject - { - private SelectionMode _selectionMode; - - public PageViewModel() - { - Node root = new Node(); - Items = root.Children; - SelectedItems = new ObservableCollection(); - - AddItemCommand = ReactiveCommand.Create(() => - { - Node parentItem = SelectedItems.Count > 0 ? SelectedItems[0] : root; - parentItem.AddNewItem(); - }); - - RemoveItemCommand = ReactiveCommand.Create(() => - { - while (SelectedItems.Count > 0) - { - Node lastItem = SelectedItems[0]; - RecursiveRemove(Items, lastItem); - SelectedItems.Remove(lastItem); - } - - bool RecursiveRemove(ObservableCollection items, Node selectedItem) - { - if (items.Remove(selectedItem)) - { - return true; - } - - foreach (Node item in items) - { - if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem)) - { - return true; - } - } - - return false; - } - }); - } - - public ObservableCollection Items { get; } - - public ObservableCollection SelectedItems { get; } - - public ReactiveCommand AddItemCommand { get; } - - public ReactiveCommand RemoveItemCommand { get; } - - public SelectionMode SelectionMode - { - get => _selectionMode; - set - { - SelectedItems.Clear(); - this.RaiseAndSetIfChanged(ref _selectionMode, value); - } - } - } - - private class Node - { - private int _counter; - private ObservableCollection _children; - - public string Header { get; private set; } - - public bool AreChildrenInitialized => _children != null; - - public ObservableCollection Children - { - get - { - if (_children == null) - { - _children = new ObservableCollection(Enumerable.Range(1, 10).Select(i => CreateNewNode())); - } - return _children; - } - } - - public void AddNewItem() => Children.Add(CreateNewNode()); - - public override string ToString() => Header; - - private Node CreateNewNode() => new Node {Header = $"Item {_counter++}"}; - } } } diff --git a/samples/ControlCatalog/Pages/teapot.bin b/samples/ControlCatalog/Pages/teapot.bin new file mode 100644 index 0000000000..589eeb912d Binary files /dev/null and b/samples/ControlCatalog/Pages/teapot.bin differ diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index 26d25a6266..1ee6bf7a29 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -67,6 +67,9 @@ + diff --git a/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs index bc2ce80714..73aaeff994 100644 --- a/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs @@ -1,31 +1,34 @@ using System; using System.Collections.ObjectModel; using System.Linq; +using Avalonia.Media; using ReactiveUI; namespace ControlCatalog.ViewModels { public class ItemsRepeaterPageViewModel : ReactiveObject { - private int newItemIndex = 1; + private int _newItemIndex = 1; + private int _newGenerationIndex = 0; + private ObservableCollection _items; public ItemsRepeaterPageViewModel() { - Items = new ObservableCollection( - Enumerable.Range(1, 100000).Select(i => new Item - { - Text = $"Item {i.ToString()}", - })); + Items = CreateItems(); } - public ObservableCollection Items { get; } + public ObservableCollection Items + { + get => _items; + set => this.RaiseAndSetIfChanged(ref _items, value); + } public Item SelectedItem { get; set; } 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() @@ -38,9 +41,33 @@ namespace ControlCatalog.ViewModels } } + public void ResetItems() + { + Items = CreateItems(); + } + + private ObservableCollection CreateItems() + { + var suffix = _newGenerationIndex == 0 ? string.Empty : $"[{_newGenerationIndex.ToString()}]"; + + _newGenerationIndex++; + + return new ObservableCollection( + Enumerable.Range(1, 100000).Select(i => new Item(i) + { + Text = $"Item {i.ToString()} {suffix}" + })); + } + public class Item : ReactiveObject { private double _height = double.NaN; + private int _index; + + public Item(int index) + { + _index = index; + } public string Text { get; set; } @@ -49,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 adf0345a70..0257b4ce66 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -1,5 +1,8 @@ using System.Reactive; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Notifications; +using Avalonia.Dialogs; using ReactiveUI; namespace ControlCatalog.ViewModels @@ -8,6 +11,10 @@ namespace ControlCatalog.ViewModels { private IManagedNotificationManager _notificationManager; + private bool _isMenuItemChecked = true; + private WindowState _windowState; + private WindowState[] _windowStates; + public MainWindowViewModel(IManagedNotificationManager notificationManager) { _notificationManager = notificationManager; @@ -26,6 +33,47 @@ namespace ControlCatalog.ViewModels { NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Error", "Native Notifications are not quite ready. Coming soon.", NotificationType.Error)); }); + + AboutCommand = ReactiveCommand.CreateFromTask(async () => + { + var dialog = new AboutAvaloniaDialog(); + + var mainWindow = (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow; + + await dialog.ShowDialog(mainWindow); + }); + + ExitCommand = ReactiveCommand.Create(() => + { + (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).Shutdown(); + }); + + ToggleMenuItemCheckedCommand = ReactiveCommand.Create(() => + { + IsMenuItemChecked = !IsMenuItemChecked; + }); + + WindowState = WindowState.Normal; + + WindowStates = new WindowState[] + { + WindowState.Minimized, + WindowState.Normal, + WindowState.Maximized, + WindowState.FullScreen, + }; + } + + public WindowState WindowState + { + get { return _windowState; } + set { this.RaiseAndSetIfChanged(ref _windowState, value); } + } + + public WindowState[] WindowStates + { + get { return _windowStates; } + set { this.RaiseAndSetIfChanged(ref _windowStates, value); } } public IManagedNotificationManager NotificationManager @@ -34,10 +82,22 @@ namespace ControlCatalog.ViewModels set { this.RaiseAndSetIfChanged(ref _notificationManager, value); } } + public bool IsMenuItemChecked + { + get { return _isMenuItemChecked; } + set { this.RaiseAndSetIfChanged(ref _isMenuItemChecked, value); } + } + public ReactiveCommand ShowCustomManagedNotificationCommand { get; } public ReactiveCommand ShowManagedNotificationCommand { get; } public ReactiveCommand ShowNativeNotificationCommand { get; } + + public ReactiveCommand AboutCommand { get; } + + public ReactiveCommand ExitCommand { get; } + + public ReactiveCommand ToggleMenuItemCheckedCommand { get; } } } diff --git a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs new file mode 100644 index 0000000000..5bc23e2fe5 --- /dev/null +++ b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using Avalonia.Controls; +using ReactiveUI; + +namespace ControlCatalog.ViewModels +{ + public class TreeViewPageViewModel : ReactiveObject + { + private readonly Node _root; + private SelectionMode _selectionMode; + + public TreeViewPageViewModel() + { + _root = new Node(); + + Items = _root.Children; + Selection = new SelectionModel(); + Selection.SelectionChanged += SelectionChanged; + + 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 + { + get => _selectionMode; + set + { + Selection.ClearSelection(); + this.RaiseAndSetIfChanged(ref _selectionMode, value); + } + } + + private void AddItem() + { + var parentItem = Selection.SelectedItems.Count > 0 ? (Node)Selection.SelectedItems[0] : _root; + parentItem.AddItem(); + } + + private void RemoveItem() + { + while (Selection.SelectedItems.Count > 0) + { + Node lastItem = (Node)Selection.SelectedItems[0]; + RecursiveRemove(Items, lastItem); + Selection.DeselectAt(Selection.SelectedIndices[0]); + } + + bool RecursiveRemove(ObservableCollection items, Node selectedItem) + { + if (items.Remove(selectedItem)) + { + return true; + } + + foreach (Node item in items) + { + if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem)) + { + return true; + } + } + + return false; + } + } + + private void SelectRandomItem() + { + var random = new Random(); + var depth = random.Next(4); + var indexes = Enumerable.Range(0, 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); + var deselected = string.Join(",", e.DeselectedIndices); + System.Diagnostics.Debug.WriteLine($"Selected '{selected}', Deselected '{deselected}'"); + } + + public class Node + { + private ObservableCollection _children; + private int _childIndex = 10; + + public Node() + { + Header = "Item"; + } + + public Node(Node parent, int index) + { + Parent = parent; + Header = parent.Header + ' ' + index; + } + + public Node Parent { get; } + public string Header { get; } + public bool AreChildrenInitialized => _children != null; + public ObservableCollection Children => _children ??= CreateChildren(); + public void AddItem() => Children.Add(new Node(this, _childIndex++)); + public void RemoveItem(Node child) => Children.Remove(child); + public override string ToString() => Header; + + private ObservableCollection CreateChildren() + { + return new ObservableCollection( + Enumerable.Range(0, 10).Select(i => new Node(this, i))); + } + } + } +} diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props index b9075b957b..8fc91aca14 100644 --- a/samples/Directory.Build.props +++ b/samples/Directory.Build.props @@ -1,6 +1,7 @@ false + $(MSBuildThisFileDirectory)..\src\tools\Avalonia.Designer.HostApp\bin\Debug\netcoreapp2.0\Avalonia.Designer.HostApp.dll diff --git a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj index 2adef5f64e..86d762a5bc 100644 --- a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj +++ b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.0 + netcoreapp3.1 diff --git a/samples/Previewer/Previewer.csproj b/samples/Previewer/Previewer.csproj index 2cdde0c945..cd3daf61e1 100644 --- a/samples/Previewer/Previewer.csproj +++ b/samples/Previewer/Previewer.csproj @@ -1,7 +1,7 @@  Exe - netcoreapp2.0 + netcoreapp3.1 diff --git a/samples/RemoteDemo/RemoteDemo.csproj b/samples/RemoteDemo/RemoteDemo.csproj index 2d7699561a..530cad805f 100644 --- a/samples/RemoteDemo/RemoteDemo.csproj +++ b/samples/RemoteDemo/RemoteDemo.csproj @@ -1,7 +1,7 @@ Exe - netcoreapp2.0 + netcoreapp3.1 diff --git a/samples/RenderDemo/App.xaml.cs b/samples/RenderDemo/App.xaml.cs index d95018520a..233160b025 100644 --- a/samples/RenderDemo/App.xaml.cs +++ b/samples/RenderDemo/App.xaml.cs @@ -1,8 +1,4 @@ -// 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; -using Avalonia.Logging.Serilog; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; @@ -25,6 +21,5 @@ namespace RenderDemo .UsePlatformDetect() .UseReactiveUI() .LogToDebug(); - } } diff --git a/samples/RenderDemo/Controls/LineBoundsDemoControl.cs b/samples/RenderDemo/Controls/LineBoundsDemoControl.cs new file mode 100644 index 0000000000..cc847a594d --- /dev/null +++ b/samples/RenderDemo/Controls/LineBoundsDemoControl.cs @@ -0,0 +1,53 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Threading; + +namespace RenderDemo.Controls +{ + public class LineBoundsDemoControl : Control + { + static LineBoundsDemoControl() + { + AffectsRender(AngleProperty); + } + + public LineBoundsDemoControl() + { + var timer = new DispatcherTimer(); + timer.Interval = TimeSpan.FromSeconds(1 / 60.0); + timer.Tick += (sender, e) => Angle += Math.PI / 360; + timer.Start(); + } + + public static readonly StyledProperty AngleProperty = + AvaloniaProperty.Register(nameof(Angle)); + + public double Angle + { + get => GetValue(AngleProperty); + set => SetValue(AngleProperty, value); + } + + public override void Render(DrawingContext drawingContext) + { + var lineLength = Math.Sqrt((100 * 100) + (100 * 100)); + + var diffX = LineBoundsHelper.CalculateAdjSide(Angle, lineLength); + var diffY = LineBoundsHelper.CalculateOppSide(Angle, lineLength); + + + var p1 = new Point(200, 200); + var p2 = new Point(p1.X + diffX, p1.Y + diffY); + + var pen = new Pen(Brushes.Green, 20, lineCap: PenLineCap.Square); + var boundPen = new Pen(Brushes.Black); + + drawingContext.DrawLine(pen, p1, p2); + + drawingContext.DrawRectangle(boundPen, LineBoundsHelper.CalculateBounds(p1, p2, pen)); + } + } +} diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index 7f63e7725f..14ccc82043 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -29,6 +29,9 @@ + + + @@ -41,6 +44,12 @@ + + + + + + diff --git a/samples/RenderDemo/MainWindow.xaml.cs b/samples/RenderDemo/MainWindow.xaml.cs index f1f974f7a1..b45a605e04 100644 --- a/samples/RenderDemo/MainWindow.xaml.cs +++ b/samples/RenderDemo/MainWindow.xaml.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia; using Avalonia.Controls; diff --git a/samples/RenderDemo/Pages/AnimationsPage.xaml b/samples/RenderDemo/Pages/AnimationsPage.xaml index c5ad1fbfc8..12fb31ea59 100644 --- a/samples/RenderDemo/Pages/AnimationsPage.xaml +++ b/samples/RenderDemo/Pages/AnimationsPage.xaml @@ -134,6 +134,32 @@ + @@ -152,6 +178,8 @@ + + diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml b/samples/RenderDemo/Pages/GlyphRunPage.xaml new file mode 100644 index 0000000000..fb3e318a0e --- /dev/null +++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml @@ -0,0 +1,14 @@ + + + + + + diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs new file mode 100644 index 0000000000..7f15845596 --- /dev/null +++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs @@ -0,0 +1,80 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using Avalonia.Threading; + +namespace RenderDemo.Pages +{ + public class GlyphRunPage : UserControl + { + private DrawingPresenter _drawingPresenter; + private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface; + private readonly Random _rand = new Random(); + private ushort[] _glyphIndices = new ushort[1]; + private float _fontSize = 20; + private int _direction = 10; + + public GlyphRunPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + + _drawingPresenter = this.FindControl("drawingPresenter"); + + DispatcherTimer.Run(() => + { + UpdateGlyphRun(); + + return true; + }, TimeSpan.FromSeconds(1)); + } + + private void UpdateGlyphRun() + { + var c = (uint)_rand.Next(65, 90); + + if (_fontSize + _direction > 200) + { + _direction = -10; + } + + if (_fontSize + _direction < 20) + { + _direction = 10; + } + + _fontSize += _direction; + + _glyphIndices[0] = _glyphTypeface.GetGlyph(c); + + var scale = (double)_fontSize / _glyphTypeface.DesignEmHeight; + + var drawingGroup = new DrawingGroup(); + + var glyphRunDrawing = new GlyphRunDrawing + { + Foreground = Brushes.Black, + GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _glyphIndices), + BaselineOrigin = new Point(0, -_glyphTypeface.Ascent * scale) + }; + + drawingGroup.Children.Add(glyphRunDrawing); + + var geometryDrawing = new GeometryDrawing + { + Pen = new Pen(Brushes.Black), + Geometry = new RectangleGeometry { Rect = glyphRunDrawing.GlyphRun.Bounds } + }; + + drawingGroup.Children.Add(geometryDrawing); + + _drawingPresenter.Drawing = drawingGroup; + } + } +} diff --git a/samples/RenderDemo/Pages/LineBoundsPage.xaml b/samples/RenderDemo/Pages/LineBoundsPage.xaml new file mode 100644 index 0000000000..07d658630a --- /dev/null +++ b/samples/RenderDemo/Pages/LineBoundsPage.xaml @@ -0,0 +1,9 @@ + + + diff --git a/samples/RenderDemo/Pages/LineBoundsPage.xaml.cs b/samples/RenderDemo/Pages/LineBoundsPage.xaml.cs new file mode 100644 index 0000000000..28ddedd4bc --- /dev/null +++ b/samples/RenderDemo/Pages/LineBoundsPage.xaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace RenderDemo.Pages +{ + public class LineBoundsPage : UserControl + { + public LineBoundsPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs index 3eb2276c48..f263786ab7 100644 --- a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs +++ b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs @@ -39,7 +39,7 @@ namespace RenderDemo.Pages ctx.FillRectangle(Brushes.Fuchsia, new Rect(50, 50, 100, 100)); } - context.DrawImage(_bitmap, 1, + context.DrawImage(_bitmap, new Rect(0, 0, 200, 200), new Rect(0, 0, 200, 200)); Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); 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. + + + + + + + + + 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..c623ae68b5 --- /dev/null +++ b/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj @@ -0,0 +1,30 @@ + + + + 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/scripts/ReplaceNugetCache.ps1 b/scripts/ReplaceNugetCache.ps1 index 70f5eaa40b..2543e58249 100644 --- a/scripts/ReplaceNugetCache.ps1 +++ b/scripts/ReplaceNugetCache.ps1 @@ -1,5 +1,5 @@ -copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\ -copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ -copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ -copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ -copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\ +copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\ +copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ +copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ +copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ +copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\ diff --git a/scripts/ReplaceNugetCache.sh b/scripts/ReplaceNugetCache.sh index e1c0487d60..7ac601571d 100755 --- a/scripts/ReplaceNugetCache.sh +++ b/scripts/ReplaceNugetCache.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash - cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netcoreapp2.0/ - cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netstandard2.0/ - cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.skia/$1/lib/netstandard2.0/ - cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.native/$1/lib/netstandard2.0/ + cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp3.1/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netcoreapp3.1/ + cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp3.1/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netstandard2.0/ + cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp3.1/Avalonia**.dll ~/.nuget/packages/avalonia.skia/$1/lib/netstandard2.0/ + cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp3.1/Avalonia**.dll ~/.nuget/packages/avalonia.native/$1/lib/netstandard2.0/ diff --git a/scripts/ReplaceNugetCacheRelease.ps1 b/scripts/ReplaceNugetCacheRelease.ps1 index 1c19e00400..96678819a4 100644 --- a/scripts/ReplaceNugetCacheRelease.ps1 +++ b/scripts/ReplaceNugetCacheRelease.ps1 @@ -1,5 +1,5 @@ -copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\ -copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ -copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.gtk3\$args\lib\netstandard2.0\ -copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ -copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ \ No newline at end of file +copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\ +copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ +copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ +copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ +copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\ \ No newline at end of file diff --git a/scripts/avalonia-rename.ps1 b/scripts/avalonia-rename.ps1 deleted file mode 100644 index c77dffb55d..0000000000 --- a/scripts/avalonia-rename.ps1 +++ /dev/null @@ -1,64 +0,0 @@ -function Get-NewDirectoryName { - param ([System.IO.DirectoryInfo]$item) - - $name = $item.Name.Replace("perspex", "avalonia") - $name = $name.Replace("Perspex", "Avalonia") - Join-Path $item.Parent.FullName $name -} - -function Get-NewFileName { - param ([System.IO.FileInfo]$item) - - $name = $item.Name.Replace("perspex", "avalonia") - $name = $name.Replace("Perspex", "Avalonia") - Join-Path $item.DirectoryName $name -} - -function Rename-Contents { - param ([System.IO.FileInfo] $file) - - $extensions = @(".cs",".xaml",".csproj",".sln",".md",".json",".yml",".partial",".ps1",".nuspec",".htm",".html",".gitmodules".".xml",".plist",".targets",".projitems",".shproj",".xib") - - if ($extensions.Contains($file.Extension)) { - $text = [IO.File]::ReadAllText($file.FullName) - $text = $text.Replace("github.com/perspex", "github.com/avaloniaui") - $text = $text.Replace("github.com/Perspex", "github.com/AvaloniaUI") - $text = $text.Replace("perspex", "avalonia") - $text = $text.Replace("Perspex", "Avalonia") - $text = $text.Replace("PERSPEX", "AVALONIA") - [IO.File]::WriteAllText($file.FullName, $text) - } -} - -function Process-Files { - param ([System.IO.DirectoryInfo] $item) - - $dirs = Get-ChildItem -Path $item.FullName -Directory - $files = Get-ChildItem -Path $item.FullName -File - - foreach ($dir in $dirs) { - Process-Files $dir.FullName - } - - foreach ($file in $files) { - Rename-Contents $file - - $renamed = Get-NewFileName $file - - if ($file.FullName -ne $renamed) { - Write-Host git mv $file.FullName $renamed - & git mv $file.FullName $renamed - } - } - - $renamed = Get-NewDirectoryName $item - - if ($item.FullName -ne $renamed) { - Write-Host git mv $item.FullName $renamed - & git mv $item.FullName $renamed - } -} - -& git submodule deinit . -& git clean -xdf -Process-Files . 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 ca45fb8c4d..067d9f462f 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -1,13 +1,10 @@ -// 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 System; +using System.Collections; using System.Collections.Generic; -using System.Linq; -using System.Reactive.Linq; -using Avalonia.Collections; +using System.Collections.Specialized; using Avalonia.Data; -using Avalonia.Animation.Animators; + +#nullable enable namespace Avalonia.Animation { @@ -16,9 +13,24 @@ namespace Avalonia.Animation /// public class Animatable : AvaloniaObject { + /// + /// Defines the property. + /// public static readonly StyledProperty ClockProperty = AvaloniaProperty.Register(nameof(Clock), inherits: true); + /// + /// Defines the property. + /// + public static readonly StyledProperty TransitionsProperty = + AvaloniaProperty.Register(nameof(Transitions)); + + private bool _transitionsEnabled = true; + private Dictionary? _transitionState; + + /// + /// Gets or sets the clock which controls the animations on the control. + /// public IClock Clock { get => GetValue(ClockProperty); @@ -26,68 +38,196 @@ namespace Avalonia.Animation } /// - /// Defines the property. + /// Gets or sets the property transitions for the control. /// - public static readonly DirectProperty TransitionsProperty = - AvaloniaProperty.RegisterDirect( - nameof(Transitions), - o => o.Transitions, - (o, v) => o.Transitions = v); + public Transitions? Transitions + { + get => GetValue(TransitionsProperty); + set => SetValue(TransitionsProperty, value); + } - private Transitions _transitions; + /// + /// Enables transitions for the control. + /// + /// + /// This method should not be called from user code, it will be called automatically by the framework + /// when a control is added to the visual tree. + /// + protected void EnableTransitions() + { + if (!_transitionsEnabled) + { + _transitionsEnabled = true; - private Dictionary _previousTransitions; + if (Transitions is object) + { + AddTransitions(Transitions); + } + } + } /// - /// Gets or sets the property transitions for the control. + /// Disables transitions for the control. /// - public Transitions Transitions + /// + /// This method should not be called from user code, it will be called automatically by the framework + /// when a control is added to the visual tree. + /// + protected void DisableTransitions() + { + if (_transitionsEnabled) + { + _transitionsEnabled = false; + + if (Transitions is object) + { + RemoveTransitions(Transitions); + } + } + } + + protected sealed override void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) { - get + if (change.Property == TransitionsProperty && change.IsEffectiveValueChange) { - if (_transitions is null) - _transitions = new Transitions(); + var oldTransitions = change.OldValue.GetValueOrDefault(); + var newTransitions = change.NewValue.GetValueOrDefault(); - if (_previousTransitions is null) - _previousTransitions = new Dictionary(); + if (newTransitions is object) + { + newTransitions.CollectionChanged += TransitionsCollectionChanged; + AddTransitions(newTransitions); + } - return _transitions; + if (oldTransitions is object) + { + oldTransitions.CollectionChanged -= TransitionsCollectionChanged; + RemoveTransitions(oldTransitions); + } } - set + else if (_transitionsEnabled && + Transitions is object && + _transitionState is object && + !change.Property.IsDirect && + change.Priority > BindingPriority.Animation) { - if (value is null) - return; + for (var i = Transitions.Count -1; i >= 0; --i) + { + var transition = Transitions[i]; - if (_previousTransitions is null) - _previousTransitions = new Dictionary(); + if (transition.Property == change.Property) + { + var state = _transitionState[transition]; + var oldValue = state.BaseValue; + var newValue = GetAnimationBaseValue(transition.Property); - SetAndRaise(TransitionsProperty, ref _transitions, value); + if (!Equals(oldValue, newValue)) + { + state.BaseValue = newValue; + + // We need to transition from the current animated value if present, + // instead of the old base value. + var animatedValue = GetValue(transition.Property); + + if (!Equals(newValue, animatedValue)) + { + oldValue = animatedValue; + } + + state.Instance?.Dispose(); + state.Instance = transition.Apply( + this, + Clock ?? AvaloniaLocator.Current.GetService(), + oldValue, + newValue); + return; + } + } + } } + + base.OnPropertyChangedCore(change); } - /// - /// Reacts to a change in a value in - /// order to animate the change if a is set for the property. - /// - /// The event args. - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + private void TransitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { - if (_transitions is null || _previousTransitions is null || e.Priority == BindingPriority.Animation) return; + if (!_transitionsEnabled) + { + return; + } - // PERF-SENSITIVE: Called on every property change. Don't use LINQ here (too many allocations). - foreach (var transition in _transitions) + switch (e.Action) { - if (transition.Property == e.Property) + case NotifyCollectionChangedAction.Add: + AddTransitions(e.NewItems); + break; + case NotifyCollectionChangedAction.Remove: + RemoveTransitions(e.OldItems); + break; + case NotifyCollectionChangedAction.Replace: + RemoveTransitions(e.OldItems); + AddTransitions(e.NewItems); + break; + case NotifyCollectionChangedAction.Reset: + throw new NotSupportedException("Transitions collection cannot be reset."); + } + } + + private void AddTransitions(IList items) + { + if (!_transitionsEnabled) + { + return; + } + + _transitionState ??= new Dictionary(); + + for (var i = 0; i < items.Count; ++i) + { + var t = (ITransition)items[i]; + + _transitionState.Add(t, new TransitionState { - if (_previousTransitions.TryGetValue(e.Property, out var dispose)) - dispose.Dispose(); + BaseValue = GetAnimationBaseValue(t.Property), + }); + } + } - var instance = transition.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue); + private void RemoveTransitions(IList items) + { + if (_transitionState is null) + { + return; + } - _previousTransitions[e.Property] = instance; - return; + for (var i = 0; i < items.Count; ++i) + { + var t = (ITransition)items[i]; + + if (_transitionState.TryGetValue(t, out var state)) + { + state.Instance?.Dispose(); + _transitionState.Remove(t); } } } + + private object GetAnimationBaseValue(AvaloniaProperty property) + { + var value = this.GetBaseValue(property, BindingPriority.LocalValue); + + if (value == AvaloniaProperty.UnsetValue) + { + value = GetValue(property); + } + + return value; + } + + private class TransitionState + { + public IDisposable? Instance { get; set; } + public object? BaseValue { get; set; } + } } } diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index ae6deb585c..ca1d97290e 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Linq; @@ -254,10 +251,10 @@ namespace Avalonia.Animation if (keyframe.TimingMode == KeyFrameTimingMode.TimeSpan) { - cue = new Cue(keyframe.KeyTime.Ticks / Duration.Ticks); + cue = new Cue(keyframe.KeyTime.TotalSeconds / Duration.TotalSeconds); } - var newKF = new AnimatorKeyFrame(handler, cue); + var newKF = new AnimatorKeyFrame(handler, cue, keyframe.KeySpline); subscriptions.Add(newKF.BindSetter(setter, control)); diff --git a/src/Avalonia.Animation/AnimatorKeyFrame.cs b/src/Avalonia.Animation/AnimatorKeyFrame.cs index 36d15e518e..f6a0c12be4 100644 --- a/src/Avalonia.Animation/AnimatorKeyFrame.cs +++ b/src/Avalonia.Animation/AnimatorKeyFrame.cs @@ -24,11 +24,20 @@ namespace Avalonia.Animation { AnimatorType = animatorType; Cue = cue; + KeySpline = null; + } + + public AnimatorKeyFrame(Type animatorType, Cue cue, KeySpline keySpline) + { + AnimatorType = animatorType; + Cue = cue; + KeySpline = keySpline; } internal bool isNeutral; public Type AnimatorType { get; } public Cue Cue { get; } + public KeySpline KeySpline { get; } public AvaloniaProperty Property { get; private set; } private object _value; diff --git a/src/Avalonia.Animation/Animators/Animator`1.cs b/src/Avalonia.Animation/Animators/Animator`1.cs index e42489d6a6..0660440e30 100644 --- a/src/Avalonia.Animation/Animators/Animator`1.cs +++ b/src/Avalonia.Animation/Animators/Animator`1.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; @@ -92,6 +89,9 @@ namespace Avalonia.Animation.Animators else newValue = (T)lastKeyframe.Value; + if (lastKeyframe.KeySpline != null) + progress = lastKeyframe.KeySpline.GetSplineProgress(progress); + return Interpolate(progress, oldValue, newValue); } diff --git a/src/Avalonia.Animation/Animators/BoolAnimator.cs b/src/Avalonia.Animation/Animators/BoolAnimator.cs index 63ff4933d6..f02fd8dfaa 100644 --- a/src/Avalonia.Animation/Animators/BoolAnimator.cs +++ b/src/Avalonia.Animation/Animators/BoolAnimator.cs @@ -1,7 +1,4 @@ -// 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. - -namespace Avalonia.Animation.Animators +namespace Avalonia.Animation.Animators { /// /// Animator that handles properties. diff --git a/src/Avalonia.Animation/Animators/ByteAnimator.cs b/src/Avalonia.Animation/Animators/ByteAnimator.cs index 0fb8f7fdc1..9fce07895c 100644 --- a/src/Avalonia.Animation/Animators/ByteAnimator.cs +++ b/src/Avalonia.Animation/Animators/ByteAnimator.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; namespace Avalonia.Animation.Animators { diff --git a/src/Avalonia.Animation/Animators/DecimalAnimator.cs b/src/Avalonia.Animation/Animators/DecimalAnimator.cs index b5fae7990c..5aed5e5f3d 100644 --- a/src/Avalonia.Animation/Animators/DecimalAnimator.cs +++ b/src/Avalonia.Animation/Animators/DecimalAnimator.cs @@ -1,7 +1,4 @@ -// 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. - -namespace Avalonia.Animation.Animators +namespace Avalonia.Animation.Animators { /// /// Animator that handles properties. diff --git a/src/Avalonia.Animation/Animators/DoubleAnimator.cs b/src/Avalonia.Animation/Animators/DoubleAnimator.cs index 68975be9d0..1ca3fabc47 100644 --- a/src/Avalonia.Animation/Animators/DoubleAnimator.cs +++ b/src/Avalonia.Animation/Animators/DoubleAnimator.cs @@ -1,7 +1,4 @@ -// 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. - -namespace Avalonia.Animation.Animators +namespace Avalonia.Animation.Animators { /// /// Animator that handles properties. diff --git a/src/Avalonia.Animation/Animators/FloatAnimator.cs b/src/Avalonia.Animation/Animators/FloatAnimator.cs index 0fd3bf8729..62f48a15a3 100644 --- a/src/Avalonia.Animation/Animators/FloatAnimator.cs +++ b/src/Avalonia.Animation/Animators/FloatAnimator.cs @@ -1,7 +1,4 @@ -// 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. - -namespace Avalonia.Animation.Animators +namespace Avalonia.Animation.Animators { /// /// Animator that handles properties. diff --git a/src/Avalonia.Animation/Animators/Int16Animator.cs b/src/Avalonia.Animation/Animators/Int16Animator.cs index d7e7da5d38..5235082fc4 100644 --- a/src/Avalonia.Animation/Animators/Int16Animator.cs +++ b/src/Avalonia.Animation/Animators/Int16Animator.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; namespace Avalonia.Animation.Animators { diff --git a/src/Avalonia.Animation/Animators/Int32Animator.cs b/src/Avalonia.Animation/Animators/Int32Animator.cs index 792b810652..2596f13e84 100644 --- a/src/Avalonia.Animation/Animators/Int32Animator.cs +++ b/src/Avalonia.Animation/Animators/Int32Animator.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; namespace Avalonia.Animation.Animators { diff --git a/src/Avalonia.Animation/Animators/Int64Animator.cs b/src/Avalonia.Animation/Animators/Int64Animator.cs index ca5817924e..d05750d35c 100644 --- a/src/Avalonia.Animation/Animators/Int64Animator.cs +++ b/src/Avalonia.Animation/Animators/Int64Animator.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; namespace Avalonia.Animation.Animators { diff --git a/src/Avalonia.Animation/Animators/UInt16Animator.cs b/src/Avalonia.Animation/Animators/UInt16Animator.cs index 4b5463dade..6c9588a966 100644 --- a/src/Avalonia.Animation/Animators/UInt16Animator.cs +++ b/src/Avalonia.Animation/Animators/UInt16Animator.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; namespace Avalonia.Animation.Animators { diff --git a/src/Avalonia.Animation/Animators/UInt32Animator.cs b/src/Avalonia.Animation/Animators/UInt32Animator.cs index c1f09e3518..e2af533566 100644 --- a/src/Avalonia.Animation/Animators/UInt32Animator.cs +++ b/src/Avalonia.Animation/Animators/UInt32Animator.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; namespace Avalonia.Animation.Animators { diff --git a/src/Avalonia.Animation/Animators/UInt64Animator.cs b/src/Avalonia.Animation/Animators/UInt64Animator.cs index 0fd9fcb30a..a709a77100 100644 --- a/src/Avalonia.Animation/Animators/UInt64Animator.cs +++ b/src/Avalonia.Animation/Animators/UInt64Animator.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; namespace Avalonia.Animation.Animators { diff --git a/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs b/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs index 0c15524362..696f43d006 100644 --- a/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs +++ b/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Animation.Animators; diff --git a/src/Avalonia.Animation/Easing/BackEaseIn.cs b/src/Avalonia.Animation/Easing/BackEaseIn.cs index a8d723d433..aab0b27294 100644 --- a/src/Avalonia.Animation/Easing/BackEaseIn.cs +++ b/src/Avalonia.Animation/Easing/BackEaseIn.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Animation.Easings diff --git a/src/Avalonia.Animation/Easing/BackEaseInOut.cs b/src/Avalonia.Animation/Easing/BackEaseInOut.cs index 4844c2a0d1..aba0524209 100644 --- a/src/Avalonia.Animation/Easing/BackEaseInOut.cs +++ b/src/Avalonia.Animation/Easing/BackEaseInOut.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Animation.Easings diff --git a/src/Avalonia.Animation/Easing/BackEaseOut.cs b/src/Avalonia.Animation/Easing/BackEaseOut.cs index 291c7c2541..c6a059cf66 100644 --- a/src/Avalonia.Animation/Easing/BackEaseOut.cs +++ b/src/Avalonia.Animation/Easing/BackEaseOut.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Animation.Easings diff --git a/src/Avalonia.Animation/Easing/BounceEaseIn.cs b/src/Avalonia.Animation/Easing/BounceEaseIn.cs index 3fc879b3cf..343609a89a 100644 --- a/src/Avalonia.Animation/Easing/BounceEaseIn.cs +++ b/src/Avalonia.Animation/Easing/BounceEaseIn.cs @@ -1,6 +1,3 @@ -// 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.Animation.Utils; namespace Avalonia.Animation.Easings diff --git a/src/Avalonia.Animation/Easing/BounceEaseInOut.cs b/src/Avalonia.Animation/Easing/BounceEaseInOut.cs index b393a4e7c7..b0c0391177 100644 --- a/src/Avalonia.Animation/Easing/BounceEaseInOut.cs +++ b/src/Avalonia.Animation/Easing/BounceEaseInOut.cs @@ -1,6 +1,3 @@ -// 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.Animation.Utils; namespace Avalonia.Animation.Easings diff --git a/src/Avalonia.Animation/Easing/BounceEaseOut.cs b/src/Avalonia.Animation/Easing/BounceEaseOut.cs index 555c8d9c79..f2759c4568 100644 --- a/src/Avalonia.Animation/Easing/BounceEaseOut.cs +++ b/src/Avalonia.Animation/Easing/BounceEaseOut.cs @@ -1,5 +1,3 @@ -// 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.Animation.Utils; namespace Avalonia.Animation.Easings diff --git a/src/Avalonia.Animation/Easing/CircularEaseIn.cs b/src/Avalonia.Animation/Easing/CircularEaseIn.cs index 1b22258099..cf6f0f84a8 100644 --- a/src/Avalonia.Animation/Easing/CircularEaseIn.cs +++ b/src/Avalonia.Animation/Easing/CircularEaseIn.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Animation.Easings diff --git a/src/Avalonia.Animation/Easing/CircularEaseInOut.cs b/src/Avalonia.Animation/Easing/CircularEaseInOut.cs index 156b634d3e..d01c7ea76b 100644 --- a/src/Avalonia.Animation/Easing/CircularEaseInOut.cs +++ b/src/Avalonia.Animation/Easing/CircularEaseInOut.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Animation.Easings diff --git a/src/Avalonia.Animation/Easing/CircularEaseOut.cs b/src/Avalonia.Animation/Easing/CircularEaseOut.cs index 2aa6161783..b5fcaa300c 100644 --- a/src/Avalonia.Animation/Easing/CircularEaseOut.cs +++ b/src/Avalonia.Animation/Easing/CircularEaseOut.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Animation.Easings diff --git a/src/Avalonia.Animation/Easing/CubicEaseIn.cs b/src/Avalonia.Animation/Easing/CubicEaseIn.cs index 1997493fc0..294443c0b7 100644 --- a/src/Avalonia.Animation/Easing/CubicEaseIn.cs +++ b/src/Avalonia.Animation/Easing/CubicEaseIn.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Animation.Easings { /// diff --git a/src/Avalonia.Animation/Easing/CubicEaseInOut.cs b/src/Avalonia.Animation/Easing/CubicEaseInOut.cs index be4d46d180..5ff816e38d 100644 --- a/src/Avalonia.Animation/Easing/CubicEaseInOut.cs +++ b/src/Avalonia.Animation/Easing/CubicEaseInOut.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Animation.Easings { /// diff --git a/src/Avalonia.Animation/Easing/CubicEaseOut.cs b/src/Avalonia.Animation/Easing/CubicEaseOut.cs index cfa42afeb9..d51fb633a1 100644 --- a/src/Avalonia.Animation/Easing/CubicEaseOut.cs +++ b/src/Avalonia.Animation/Easing/CubicEaseOut.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Animation.Easings { /// diff --git a/src/Avalonia.Animation/Easing/EasingTypeConverter.cs b/src/Avalonia.Animation/Easing/EasingTypeConverter.cs index ee15761eff..6613f6d393 100644 --- a/src/Avalonia.Animation/Easing/EasingTypeConverter.cs +++ b/src/Avalonia.Animation/Easing/EasingTypeConverter.cs @@ -1,6 +1,3 @@ -// 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 System; using System.ComponentModel; using System.Globalization; diff --git a/src/Avalonia.Animation/Easing/ElasticEaseIn.cs b/src/Avalonia.Animation/Easing/ElasticEaseIn.cs index 654ac6991b..d678bebd0c 100644 --- a/src/Avalonia.Animation/Easing/ElasticEaseIn.cs +++ b/src/Avalonia.Animation/Easing/ElasticEaseIn.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Animation.Utils; diff --git a/src/Avalonia.Animation/Easing/ElasticEaseInOut.cs b/src/Avalonia.Animation/Easing/ElasticEaseInOut.cs index b170cb2d6a..92331d49b6 100644 --- a/src/Avalonia.Animation/Easing/ElasticEaseInOut.cs +++ b/src/Avalonia.Animation/Easing/ElasticEaseInOut.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Animation.Utils; diff --git a/src/Avalonia.Animation/Easing/ElasticEaseOut.cs b/src/Avalonia.Animation/Easing/ElasticEaseOut.cs index 1cc38a9dea..fdda568699 100644 --- a/src/Avalonia.Animation/Easing/ElasticEaseOut.cs +++ b/src/Avalonia.Animation/Easing/ElasticEaseOut.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Animation.Utils; diff --git a/src/Avalonia.Animation/Easing/ExponentialEaseIn.cs b/src/Avalonia.Animation/Easing/ExponentialEaseIn.cs index 14977f6783..546e255348 100644 --- a/src/Avalonia.Animation/Easing/ExponentialEaseIn.cs +++ b/src/Avalonia.Animation/Easing/ExponentialEaseIn.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Animation.Easings diff --git a/src/Avalonia.Animation/Easing/ExponentialEaseInOut.cs b/src/Avalonia.Animation/Easing/ExponentialEaseInOut.cs index 64eb51e116..de3cbf0202 100644 --- a/src/Avalonia.Animation/Easing/ExponentialEaseInOut.cs +++ b/src/Avalonia.Animation/Easing/ExponentialEaseInOut.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Animation.Easings diff --git a/src/Avalonia.Animation/Easing/ExponentialEaseOut.cs b/src/Avalonia.Animation/Easing/ExponentialEaseOut.cs index b89f8e9fa2..7f91388e08 100644 --- a/src/Avalonia.Animation/Easing/ExponentialEaseOut.cs +++ b/src/Avalonia.Animation/Easing/ExponentialEaseOut.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Animation.Easings diff --git a/src/Avalonia.Animation/Easing/IEasing.cs b/src/Avalonia.Animation/Easing/IEasing.cs index 3c954a8cab..9c9c7b3a99 100644 --- a/src/Avalonia.Animation/Easing/IEasing.cs +++ b/src/Avalonia.Animation/Easing/IEasing.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Animation.Easings { /// diff --git a/src/Avalonia.Animation/Easing/LinearEasing.cs b/src/Avalonia.Animation/Easing/LinearEasing.cs index f7a90c0eef..78a56005f6 100644 --- a/src/Avalonia.Animation/Easing/LinearEasing.cs +++ b/src/Avalonia.Animation/Easing/LinearEasing.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Animation.Easings { /// diff --git a/src/Avalonia.Animation/Easing/QuadraticEaseIn.cs b/src/Avalonia.Animation/Easing/QuadraticEaseIn.cs index 6c29b31516..49faec6187 100644 --- a/src/Avalonia.Animation/Easing/QuadraticEaseIn.cs +++ b/src/Avalonia.Animation/Easing/QuadraticEaseIn.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Animation.Easings { /// diff --git a/src/Avalonia.Animation/Easing/QuadraticEaseInOut.cs b/src/Avalonia.Animation/Easing/QuadraticEaseInOut.cs index 16ba02cc20..4d95bdc393 100644 --- a/src/Avalonia.Animation/Easing/QuadraticEaseInOut.cs +++ b/src/Avalonia.Animation/Easing/QuadraticEaseInOut.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Animation.Easings { /// diff --git a/src/Avalonia.Animation/Easing/QuadraticEaseOut.cs b/src/Avalonia.Animation/Easing/QuadraticEaseOut.cs index 5252b88cf3..8ec7265841 100644 --- a/src/Avalonia.Animation/Easing/QuadraticEaseOut.cs +++ b/src/Avalonia.Animation/Easing/QuadraticEaseOut.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Animation.Easings { /// diff --git a/src/Avalonia.Animation/Easing/QuarticEaseIn.cs b/src/Avalonia.Animation/Easing/QuarticEaseIn.cs index 988fd1f7fb..69785aec3c 100644 --- a/src/Avalonia.Animation/Easing/QuarticEaseIn.cs +++ b/src/Avalonia.Animation/Easing/QuarticEaseIn.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Animation.Easings { /// diff --git a/src/Avalonia.Animation/Easing/QuarticEaseInOut.cs b/src/Avalonia.Animation/Easing/QuarticEaseInOut.cs index 0ad908f6bb..5a7205087d 100644 --- a/src/Avalonia.Animation/Easing/QuarticEaseInOut.cs +++ b/src/Avalonia.Animation/Easing/QuarticEaseInOut.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Animation.Easings { /// diff --git a/src/Avalonia.Animation/Easing/QuarticEaseOut.cs b/src/Avalonia.Animation/Easing/QuarticEaseOut.cs index 8499575653..764a1a6ce6 100644 --- a/src/Avalonia.Animation/Easing/QuarticEaseOut.cs +++ b/src/Avalonia.Animation/Easing/QuarticEaseOut.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Animation.Easings { /// diff --git a/src/Avalonia.Animation/Easing/QuinticEaseIn.cs b/src/Avalonia.Animation/Easing/QuinticEaseIn.cs index 4b0c598c69..688f5b789d 100644 --- a/src/Avalonia.Animation/Easing/QuinticEaseIn.cs +++ b/src/Avalonia.Animation/Easing/QuinticEaseIn.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Animation.Easings { /// diff --git a/src/Avalonia.Animation/Easing/QuinticEaseInOut.cs b/src/Avalonia.Animation/Easing/QuinticEaseInOut.cs index 53c29f75dc..5b6c0c1162 100644 --- a/src/Avalonia.Animation/Easing/QuinticEaseInOut.cs +++ b/src/Avalonia.Animation/Easing/QuinticEaseInOut.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Animation.Easings { /// diff --git a/src/Avalonia.Animation/Easing/QuinticEaseOut.cs b/src/Avalonia.Animation/Easing/QuinticEaseOut.cs index 1ad31a9623..5cb5663dff 100644 --- a/src/Avalonia.Animation/Easing/QuinticEaseOut.cs +++ b/src/Avalonia.Animation/Easing/QuinticEaseOut.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Animation.Easings { /// diff --git a/src/Avalonia.Animation/Easing/SineEaseIn.cs b/src/Avalonia.Animation/Easing/SineEaseIn.cs index a4b496e4b6..01ae7bca04 100644 --- a/src/Avalonia.Animation/Easing/SineEaseIn.cs +++ b/src/Avalonia.Animation/Easing/SineEaseIn.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Animation.Utils; diff --git a/src/Avalonia.Animation/Easing/SineEaseInOut.cs b/src/Avalonia.Animation/Easing/SineEaseInOut.cs index 51052575e2..6b8419f2a1 100644 --- a/src/Avalonia.Animation/Easing/SineEaseInOut.cs +++ b/src/Avalonia.Animation/Easing/SineEaseInOut.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Animation.Easings diff --git a/src/Avalonia.Animation/Easing/SineEaseOut.cs b/src/Avalonia.Animation/Easing/SineEaseOut.cs index 267b4b3809..f7ead3feac 100644 --- a/src/Avalonia.Animation/Easing/SineEaseOut.cs +++ b/src/Avalonia.Animation/Easing/SineEaseOut.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Animation.Utils; diff --git a/src/Avalonia.Animation/FillMode.cs b/src/Avalonia.Animation/FillMode.cs index 39beecf455..001e1cdeb4 100644 --- a/src/Avalonia.Animation/FillMode.cs +++ b/src/Avalonia.Animation/FillMode.cs @@ -1,7 +1,4 @@ -// 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. - -namespace Avalonia.Animation +namespace Avalonia.Animation { public enum FillMode { diff --git a/src/Avalonia.Animation/IAnimation.cs b/src/Avalonia.Animation/IAnimation.cs index 26cf4a6fd1..ff85535d8a 100644 --- a/src/Avalonia.Animation/IAnimation.cs +++ b/src/Avalonia.Animation/IAnimation.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Threading.Tasks; diff --git a/src/Avalonia.Animation/IAnimationSetter.cs b/src/Avalonia.Animation/IAnimationSetter.cs index 9c8365ea37..2d22377286 100644 --- a/src/Avalonia.Animation/IAnimationSetter.cs +++ b/src/Avalonia.Animation/IAnimationSetter.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Animation { public interface IAnimationSetter diff --git a/src/Avalonia.Animation/IAnimator.cs b/src/Avalonia.Animation/IAnimator.cs index d09f396d70..d0fb173c54 100644 --- a/src/Avalonia.Animation/IAnimator.cs +++ b/src/Avalonia.Animation/IAnimator.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Collections.Generic; namespace Avalonia.Animation diff --git a/src/Avalonia.Animation/ITransition.cs b/src/Avalonia.Animation/ITransition.cs index e5d8466f04..ade2ec8b9e 100644 --- a/src/Avalonia.Animation/ITransition.cs +++ b/src/Avalonia.Animation/ITransition.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; namespace Avalonia.Animation { diff --git a/src/Avalonia.Animation/IterationCount.cs b/src/Avalonia.Animation/IterationCount.cs index e9cd0686d8..9463718608 100644 --- a/src/Avalonia.Animation/IterationCount.cs +++ b/src/Avalonia.Animation/IterationCount.cs @@ -1,6 +1,3 @@ -// 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 System; using System.ComponentModel; using System.Globalization; @@ -63,7 +60,7 @@ namespace Avalonia.Animation public IterationType RepeatType => _type; /// - /// Gets a value that indicates whether the is set to loop. + /// Gets a value that indicates whether the is set to Infinite. /// public bool IsInfinite => _type == IterationType.Infinite; diff --git a/src/Avalonia.Animation/IterationCountTypeConverter.cs b/src/Avalonia.Animation/IterationCountTypeConverter.cs index a2bb9a20f1..1c63f8cdf1 100644 --- a/src/Avalonia.Animation/IterationCountTypeConverter.cs +++ b/src/Avalonia.Animation/IterationCountTypeConverter.cs @@ -1,6 +1,3 @@ -// 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 System; using System.ComponentModel; using System.Globalization; diff --git a/src/Avalonia.Animation/KeyFrame.cs b/src/Avalonia.Animation/KeyFrame.cs index a6577505ee..c2cc1aa051 100644 --- a/src/Avalonia.Animation/KeyFrame.cs +++ b/src/Avalonia.Animation/KeyFrame.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Collections.Generic; using Avalonia.Collections; using Avalonia.Metadata; @@ -22,6 +19,7 @@ namespace Avalonia.Animation { private TimeSpan _ktimeSpan; private Cue _kCue; + private KeySpline _kKeySpline; public KeyFrame() { @@ -77,6 +75,25 @@ namespace Avalonia.Animation } } + /// + /// Gets or sets the KeySpline of this . + /// + /// The key spline. + public KeySpline KeySpline + { + get + { + return _kKeySpline; + } + set + { + _kKeySpline = value; + if (value != null && !value.IsValid()) + { + throw new ArgumentException($"{nameof(KeySpline)} must have X coordinates >= 0.0 and <= 1.0."); + } + } + } } diff --git a/src/Avalonia.Animation/KeyFrames.cs b/src/Avalonia.Animation/KeyFrames.cs index 9e3b1d9f51..2e9efa8691 100644 --- a/src/Avalonia.Animation/KeyFrames.cs +++ b/src/Avalonia.Animation/KeyFrames.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Collections.Generic; using Avalonia.Collections; diff --git a/src/Avalonia.Animation/KeySpline.cs b/src/Avalonia.Animation/KeySpline.cs new file mode 100644 index 0000000000..5a4f7a15a3 --- /dev/null +++ b/src/Avalonia.Animation/KeySpline.cs @@ -0,0 +1,349 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Text; +using Avalonia; +using Avalonia.Utilities; + +// 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 +{ + /// + /// Determines how an animation is used based on a cubic bezier curve. + /// X1 and X2 must be between 0.0 and 1.0, inclusive. + /// See https://docs.microsoft.com/en-us/dotnet/api/system.windows.media.animation.keyspline + /// + [TypeConverter(typeof(KeySplineTypeConverter))] + public class KeySpline : AvaloniaObject + { + // Control points + private double _controlPointX1; + private double _controlPointY1; + private double _controlPointX2; + private double _controlPointY2; + private bool _isSpecified; + private bool _isDirty; + + // The parameter that corresponds to the most recent time + private double _parameter; + + // Cached coefficients + private double _Bx; // 3*points[0].X + private double _Cx; // 3*points[1].X + private double _Cx_Bx; // 2*(Cx - Bx) + private double _three_Cx; // 3 - Cx + + private double _By; // 3*points[0].Y + private double _Cy; // 3*points[1].Y + + // constants + private const double _accuracy = .001; // 1/3 the desired accuracy in X + private const double _fuzz = .000001; // computational zero + + /// + /// Create a with X1 = Y1 = 0 and X2 = Y2 = 1. + /// + public KeySpline() + { + _controlPointX1 = 0.0; + _controlPointY1 = 0.0; + _controlPointX2 = 1.0; + _controlPointY2 = 1.0; + _isDirty = true; + } + + /// + /// Create a with the given parameters + /// + /// X coordinate for the first control point + /// Y coordinate for the first control point + /// X coordinate for the second control point + /// Y coordinate for the second control point + public KeySpline(double x1, double y1, double x2, double y2) + { + _controlPointX1 = x1; + _controlPointY1 = y1; + _controlPointX2 = x2; + _controlPointY2 = y2; + _isDirty = true; + } + + /// + /// Parse a from a string. The string + /// needs to contain 4 values in it for the 2 control points. + /// + /// string with 4 values in it + /// culture of the string + /// Thrown if the string does not have 4 values + /// 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.")) + { + return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble()); + } + } + + /// + /// X coordinate of the first control point + /// + public double ControlPointX1 + { + get => _controlPointX1; + set + { + if (IsValidXValue(value)) + { + _controlPointX1 = value; + } + else + { + throw new ArgumentException("Invalid KeySpline X1 value. Must be >= 0.0 and <= 1.0."); + } + } + } + + /// + /// Y coordinate of the first control point + /// + public double ControlPointY1 + { + get => _controlPointY1; + set => _controlPointY1 = value; + } + + /// + /// X coordinate of the second control point + /// + public double ControlPointX2 + { + get => _controlPointX2; + set + { + if (IsValidXValue(value)) + { + _controlPointX2 = value; + } + else + { + throw new ArgumentException("Invalid KeySpline X2 value. Must be >= 0.0 and <= 1.0."); + } + } + } + + /// + /// Y coordinate of the second control point + /// + public double ControlPointY2 + { + get => _controlPointY2; + set => _controlPointY2 = value; + } + + /// + /// Calculates spline progress from a linear progress. + /// + /// the linear progress + /// the spline progress + public double GetSplineProgress(double linearProgress) + { + if (_isDirty) + { + Build(); + } + + if (!_isSpecified) + { + return linearProgress; + } + else + { + SetParameterFromX(linearProgress); + + return GetBezierValue(_By, _Cy, _parameter); + } + } + + /// + /// Check to see whether the is valid by looking + /// at its X values. + /// + /// true if the X values for this fall in + /// acceptable range; false otherwise. + public bool IsValid() + { + return IsValidXValue(_controlPointX1) && IsValidXValue(_controlPointX2); + } + + /// + /// + /// + /// + /// + private bool IsValidXValue(double value) + { + return value >= 0.0 && value <= 1.0; + } + + /// + /// Compute cached coefficients. + /// + private void Build() + { + if (_controlPointX1 == 0 && _controlPointY1 == 0 && _controlPointX2 == 1 && _controlPointY2 == 1) + { + // This KeySpline would have no effect on the progress. + _isSpecified = false; + } + else + { + _isSpecified = true; + + _parameter = 0; + + // X coefficients + _Bx = 3 * _controlPointX1; + _Cx = 3 * _controlPointX2; + _Cx_Bx = 2 * (_Cx - _Bx); + _three_Cx = 3 - _Cx; + + // Y coefficients + _By = 3 * _controlPointY1; + _Cy = 3 * _controlPointY2; + } + + _isDirty = false; + } + + /// + /// Get an X or Y value with the Bezier formula. + /// + /// the second Bezier coefficient + /// the third Bezier coefficient + /// the parameter value to evaluate at + /// the value of the Bezier function at the given parameter + static private double GetBezierValue(double b, double c, double t) + { + double s = 1.0 - t; + double t2 = t * t; + + return b * t * s * s + c * t2 * s + t2 * t; + } + + /// + /// Get X and dX/dt at a given parameter + /// + /// the parameter value to evaluate at + /// the value of x there + /// the value of dx/dt there + private void GetXAndDx(double t, out double x, out double dx) + { + double s = 1.0 - t; + double t2 = t * t; + double s2 = s * s; + + x = _Bx * t * s2 + _Cx * t2 * s + t2 * t; + dx = _Bx * s2 + _Cx_Bx * s * t + _three_Cx * t2; + } + + /// + /// Compute the parameter value that corresponds to a given X value, using a modified + /// clamped Newton-Raphson algorithm to solve the equation X(t) - time = 0. We make + /// use of some known properties of this particular function: + /// * We are only interested in solutions in the interval [0,1] + /// * X(t) is increasing, so we can assume that if X(t) > time t > solution. We use + /// that to clamp down the search interval with every probe. + /// * The derivative of X and Y are between 0 and 3. + /// + /// the time, scaled to fit in [0,1] + private void SetParameterFromX(double time) + { + // Dynamic search interval to clamp with + double bottom = 0; + double top = 1; + + if (time == 0) + { + _parameter = 0; + } + else if (time == 1) + { + _parameter = 1; + } + else + { + // Loop while improving the guess + while (top - bottom > _fuzz) + { + double x, dx, absdx; + + // Get x and dx/dt at the current parameter + GetXAndDx(_parameter, out x, out dx); + absdx = Math.Abs(dx); + + // Clamp down the search interval, relying on the monotonicity of X(t) + if (x > time) + { + top = _parameter; // because parameter > solution + } + else + { + bottom = _parameter; // because parameter < solution + } + + // The desired accuracy is in ultimately in y, not in x, so the + // accuracy needs to be multiplied by dx/dy = (dx/dt) / (dy/dt). + // But dy/dt <=3, so we omit that + if (Math.Abs(x - time) < _accuracy * absdx) + { + break; // We're there + } + + if (absdx > _fuzz) + { + // Nonzero derivative, use Newton-Raphson to obtain the next guess + double next = _parameter - (x - time) / dx; + + // If next guess is out of the search interval then clamp it in + if (next >= top) + { + _parameter = (_parameter + top) / 2; + } + else if (next <= bottom) + { + _parameter = (_parameter + bottom) / 2; + } + else + { + // Next guess is inside the search interval, accept it + _parameter = next; + } + } + else // Zero derivative, halve the search interval + { + _parameter = (bottom + top) / 2; + } + } + } + } + } + + /// + /// 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/PlayState.cs b/src/Avalonia.Animation/PlayState.cs index 8d28f06eb1..313d33d586 100644 --- a/src/Avalonia.Animation/PlayState.cs +++ b/src/Avalonia.Animation/PlayState.cs @@ -1,7 +1,4 @@ -// 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. - -namespace Avalonia.Animation +namespace Avalonia.Animation { /// /// Determines the playback state of an animation. diff --git a/src/Avalonia.Animation/PlaybackDirection.cs b/src/Avalonia.Animation/PlaybackDirection.cs index a44dd388ae..bbce6106e1 100644 --- a/src/Avalonia.Animation/PlaybackDirection.cs +++ b/src/Avalonia.Animation/PlaybackDirection.cs @@ -1,7 +1,4 @@ -// 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. - -namespace Avalonia.Animation +namespace Avalonia.Animation { /// /// Determines the playback direction of an animation. diff --git a/src/Avalonia.Animation/Properties/AssemblyInfo.cs b/src/Avalonia.Animation/Properties/AssemblyInfo.cs index eb38a66a84..d34fd06272 100644 --- a/src/Avalonia.Animation/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Animation/Properties/AssemblyInfo.cs @@ -1,6 +1,3 @@ -// 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.Metadata; using System.Reflection; using System.Runtime.CompilerServices; @@ -10,3 +7,4 @@ using System.Runtime.CompilerServices; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Animators")] [assembly: InternalsVisibleTo("Avalonia.LeakTests")] +[assembly: InternalsVisibleTo("Avalonia.Animation.UnitTests")] diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs index 10ea6bf523..5184341324 100644 --- a/src/Avalonia.Animation/TransitionInstance.cs +++ b/src/Avalonia.Animation/TransitionInstance.cs @@ -1,12 +1,10 @@ -// 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.Metadata; using System; using System.Reactive.Linq; using Avalonia.Animation.Easings; using Avalonia.Animation.Utils; using Avalonia.Reactive; +using Avalonia.Utilities; namespace Avalonia.Animation { @@ -16,29 +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) { - _duration = Duration; + clock = clock ?? throw new ArgumentNullException(nameof(clock)); + + _delay = delay; + _duration = duration; _baseClock = clock; } private void TimerTick(TimeSpan t) { - var interpVal = (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 cd0d5d9ce9..4542a137e5 100644 --- a/src/Avalonia.Animation/Transition`1.cs +++ b/src/Avalonia.Animation/Transition`1.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Reactive.Linq; using Avalonia.Animation.Easings; @@ -16,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. /// @@ -50,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.Animation/Transitions.cs b/src/Avalonia.Animation/Transitions.cs index 7e742fc0b4..6687a2902d 100644 --- a/src/Avalonia.Animation/Transitions.cs +++ b/src/Avalonia.Animation/Transitions.cs @@ -1,7 +1,6 @@ -// 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 System; using Avalonia.Collections; +using Avalonia.Threading; namespace Avalonia.Animation { @@ -16,6 +15,17 @@ namespace Avalonia.Animation public Transitions() { ResetBehavior = ResetBehavior.Remove; + Validate = ValidateTransition; + } + + private void ValidateTransition(ITransition obj) + { + Dispatcher.UIThread.VerifyAccess(); + + if (obj.Property.IsDirect) + { + throw new InvalidOperationException("Cannot animate a direct property."); + } } } } diff --git a/src/Avalonia.Animation/Transitions/DoubleTransition.cs b/src/Avalonia.Animation/Transitions/DoubleTransition.cs index d7dd93c743..8cae1e1f81 100644 --- a/src/Avalonia.Animation/Transitions/DoubleTransition.cs +++ b/src/Avalonia.Animation/Transitions/DoubleTransition.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Reactive.Linq; diff --git a/src/Avalonia.Animation/Transitions/FloatTransition.cs b/src/Avalonia.Animation/Transitions/FloatTransition.cs index 825a3f958f..427563e559 100644 --- a/src/Avalonia.Animation/Transitions/FloatTransition.cs +++ b/src/Avalonia.Animation/Transitions/FloatTransition.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Reactive.Linq; diff --git a/src/Avalonia.Animation/Transitions/IntegerTransition.cs b/src/Avalonia.Animation/Transitions/IntegerTransition.cs index e3dca5b26d..7a85bd75dc 100644 --- a/src/Avalonia.Animation/Transitions/IntegerTransition.cs +++ b/src/Avalonia.Animation/Transitions/IntegerTransition.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Reactive.Linq; diff --git a/src/Avalonia.Animation/Utils/BounceEaseUtils.cs b/src/Avalonia.Animation/Utils/BounceEaseUtils.cs index 71b4ec4d94..0fc9f923f6 100644 --- a/src/Avalonia.Animation/Utils/BounceEaseUtils.cs +++ b/src/Avalonia.Animation/Utils/BounceEaseUtils.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Animation.Utils { /// diff --git a/src/Avalonia.Animation/Utils/EasingUtils.cs b/src/Avalonia.Animation/Utils/EasingUtils.cs index 1a7688cace..1179ff6919 100644 --- a/src/Avalonia.Animation/Utils/EasingUtils.cs +++ b/src/Avalonia.Animation/Utils/EasingUtils.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; namespace Avalonia.Animation.Utils { diff --git a/src/Avalonia.Base/AttachedProperty.cs b/src/Avalonia.Base/AttachedProperty.cs index fdb04b6dfc..b8ab4d6bf5 100644 --- a/src/Avalonia.Base/AttachedProperty.cs +++ b/src/Avalonia.Base/AttachedProperty.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia @@ -18,12 +15,14 @@ namespace Avalonia /// The class that is registering the property. /// The property metadata. /// Whether the property inherits its value. + /// A value validation callback. public AttachedProperty( string name, - Type ownerType, + Type ownerType, StyledPropertyMetadata metadata, - bool inherits = false) - : base(name, ownerType, metadata, inherits) + bool inherits = false, + Func validate = null) + : base(name, ownerType, metadata, inherits, validate) { } diff --git a/src/Avalonia.Base/AvaloniaInternalException.cs b/src/Avalonia.Base/AvaloniaInternalException.cs index 205dc29a97..a536073174 100644 --- a/src/Avalonia.Base/AvaloniaInternalException.cs +++ b/src/Avalonia.Base/AvaloniaInternalException.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 2450f1a3a1..d18f0b3f94 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -1,16 +1,11 @@ -// 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 System; using System.Collections.Generic; using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Logging; +using Avalonia.PropertyStore; using Avalonia.Threading; -using Avalonia.Utilities; namespace Avalonia { @@ -20,13 +15,13 @@ namespace Avalonia /// /// This class is analogous to DependencyObject in WPF. /// - public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged + public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IValueSink { private IAvaloniaObject _inheritanceParent; - private List _directBindings; + private List _directBindings; private PropertyChangedEventHandler _inpcChanged; private EventHandler _propertyChanged; - private EventHandler _inheritablePropertyChanged; + private List _inheritanceChildren; private ValueStore _values; private ValueStore Values => _values ?? (_values = new ValueStore(this)); @@ -36,7 +31,6 @@ namespace Avalonia public AvaloniaObject() { VerifyAccess(); - AvaloniaPropertyRegistry.Instance.NotifyInitialized(this); } /// @@ -57,15 +51,6 @@ namespace Avalonia remove { _inpcChanged -= value; } } - /// - /// Raised when an inheritable value changes on this object. - /// - event EventHandler IAvaloniaObject.InheritablePropertyChanged - { - add { _inheritablePropertyChanged += value; } - remove { _inheritablePropertyChanged -= value; } - } - /// /// Gets or sets the parent object that inherited values /// are inherited from. @@ -83,47 +68,31 @@ namespace Avalonia set { VerifyAccess(); + if (_inheritanceParent != value) { - if (_inheritanceParent != null) - { - _inheritanceParent.InheritablePropertyChanged -= ParentPropertyChanged; - } + var oldParent = _inheritanceParent; + var valuestore = _values; - var oldInheritanceParent = _inheritanceParent; + _inheritanceParent?.RemoveInheritanceChild(this); _inheritanceParent = value; - var valuestore = _values; - foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType())) + var properties = AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType()); + var propertiesCount = properties.Count; + + for (var i = 0; i < propertiesCount; i++) { - if (valuestore != null && valuestore.GetValue(property) != AvaloniaProperty.UnsetValue) + var property = properties[i]; + if (valuestore?.IsSet(property) == true) { - // if local value set there can be no change + // If local value set there can be no change. continue; } - // get the value as it would have been with the previous InheritanceParent - object oldValue; - if (oldInheritanceParent is AvaloniaObject aobj) - { - oldValue = aobj.GetValueOrDefaultUnchecked(property); - } - else - { - oldValue = ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); - } - - object newValue = GetDefaultValue(property); - if (!Equals(oldValue, newValue)) - { - RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue); - } + property.RouteInheritanceParentChanged(this, oldParent); } - if (_inheritanceParent != null) - { - _inheritanceParent.InheritablePropertyChanged += ParentPropertyChanged; - } + _inheritanceParent?.AddInheritanceChild(this); } } } @@ -166,10 +135,56 @@ namespace Avalonia /// The property. public void ClearValue(AvaloniaProperty property) { - Contract.Requires(property != null); + property = property ?? throw new ArgumentNullException(nameof(property)); + + property.RouteClearValue(this); + } + + /// + /// Clears a 's local value. + /// + /// The property. + public void ClearValue(AvaloniaProperty property) + { + property = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); + + switch (property) + { + case StyledPropertyBase styled: + ClearValue(styled); + break; + case DirectPropertyBase direct: + ClearValue(direct); + break; + default: + throw new NotSupportedException("Unsupported AvaloniaProperty type."); + } + } + + /// + /// Clears a 's local value. + /// + /// The property. + public void ClearValue(StyledPropertyBase property) + { + property = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); + + _values?.ClearLocalValue(property); + } + + /// + /// Clears a 's local value. + /// + /// The property. + public void ClearValue(DirectPropertyBase property) + { + property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); - SetValue(property, AvaloniaProperty.UnsetValue); + var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); + p.InvokeSetter(this, p.GetUnsetValue(GetType())); } /// @@ -210,17 +225,23 @@ namespace Avalonia /// The value. public object GetValue(AvaloniaProperty property) { - Contract.Requires(property != null); + property = property ?? throw new ArgumentNullException(nameof(property)); + + return property.RouteGetValue(this); + } + + /// + /// Gets a value. + /// + /// The type of the property. + /// The property. + /// The value. + public T GetValue(StyledPropertyBase property) + { + property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); - if (property.IsDirect) - { - return ((IDirectPropertyAccessor)GetRegistered(property)).GetValue(this); - } - else - { - return GetValueOrDefaultUnchecked(property); - } + return GetValueOrInheritedOrDefault(property); } /// @@ -229,11 +250,28 @@ namespace Avalonia /// The type of the property. /// The property. /// The value. - public T GetValue(AvaloniaProperty property) + public T GetValue(DirectPropertyBase property) { - Contract.Requires(property != null); + property = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); + + var registered = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); + return registered.InvokeGetter(this); + } - return (T)GetValue((AvaloniaProperty)property); + /// + public Optional GetBaseValue(StyledPropertyBase property, BindingPriority maxPriority) + { + property = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); + + if (_values is object && + _values.TryGetValue(property, maxPriority, out var value)) + { + return value; + } + + return default; } /// @@ -277,17 +315,49 @@ namespace Avalonia object value, BindingPriority priority = BindingPriority.LocalValue) { - Contract.Requires(property != null); + property = property ?? throw new ArgumentNullException(nameof(property)); + + property.RouteSetValue(this, value, priority); + } + + /// + /// Sets a value. + /// + /// The type of the property. + /// The property. + /// The value. + /// The priority of the value. + /// + /// An if setting the property can be undone, otherwise null. + /// + public IDisposable SetValue( + StyledPropertyBase property, + T value, + BindingPriority priority = BindingPriority.LocalValue) + { + property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); - if (property.IsDirect) + LogPropertySet(property, value, priority); + + if (value is UnsetValueType) { - SetDirectValue(property, value); + if (priority == BindingPriority.LocalValue) + { + Values.ClearLocalValue(property); + } + else + { + throw new NotSupportedException( + "Cannot set property to Unset at non-local value priority."); + } } - else + else if (!(value is DoNothingType)) { - SetStyledValue(property, value, priority); + return Values.SetValue(property, value, priority); } + + return null; } /// @@ -296,69 +366,35 @@ namespace Avalonia /// The type of the property. /// The property. /// The value. - /// The priority of the value. - public void SetValue( - AvaloniaProperty property, - T value, - BindingPriority priority = BindingPriority.LocalValue) + public void SetValue(DirectPropertyBase property, T value) { - Contract.Requires(property != null); + property = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); - SetValue((AvaloniaProperty)property, value, priority); + LogPropertySet(property, value, BindingPriority.LocalValue); + SetDirectValueUnchecked(property, value); } /// /// Binds a to an observable. /// + /// The type of the property. /// The property. /// The observable. /// The priority of the binding. /// /// A disposable which can be used to terminate the binding. /// - public IDisposable Bind( - AvaloniaProperty property, - IObservable source, + public IDisposable Bind( + StyledPropertyBase property, + IObservable> source, BindingPriority priority = BindingPriority.LocalValue) { - Contract.Requires(property != null); - Contract.Requires(source != null); - + property = property ?? throw new ArgumentNullException(nameof(property)); + source = source ?? throw new ArgumentNullException(nameof(source)); VerifyAccess(); - if (property.IsDirect) - { - if (property.IsReadOnly) - { - throw new ArgumentException($"The property {property.Name} is readonly."); - } - - Logger.TryGet(LogEventLevel.Verbose)?.Log( - LogArea.Property, - this, - "Bound {Property} to {Binding} with priority LocalValue", - property, - GetDescription(source)); - - if (_directBindings == null) - { - _directBindings = new List(); - } - - return new DirectBindingSubscription(this, property, source); - } - else - { - Logger.TryGet(LogEventLevel.Verbose)?.Log( - LogArea.Property, - this, - "Bound {Property} to {Binding} with priority {Priority}", - property, - GetDescription(source), - priority); - - return Values.AddBinding(property, source, priority); - } + return Values.AddBinding(property, source, priority); } /// @@ -367,59 +403,68 @@ namespace Avalonia /// The type of the property. /// The property. /// The observable. - /// The priority of the binding. /// /// A disposable which can be used to terminate the binding. /// public IDisposable Bind( - AvaloniaProperty property, - IObservable source, - BindingPriority priority = BindingPriority.LocalValue) + DirectPropertyBase property, + IObservable> source) { - Contract.Requires(property != null); + property = property ?? throw new ArgumentNullException(nameof(property)); + source = source ?? throw new ArgumentNullException(nameof(source)); + VerifyAccess(); + + property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); + + if (property.IsReadOnly) + { + throw new ArgumentException($"The property {property.Name} is readonly."); + } + + Logger.TryGet(LogEventLevel.Verbose, LogArea.Property)?.Log( + this, + "Bound {Property} to {Binding} with priority LocalValue", + property, + GetDescription(source)); - return Bind(property, source.Select(x => (object)x), priority); + _directBindings ??= new List(); + + return new DirectBindingSubscription(this, property, source); } /// - /// Forces the specified property to be revalidated. + /// Coerces the specified . /// + /// The type of the property. /// The property. - public void Revalidate(AvaloniaProperty property) + public void CoerceValue(StyledPropertyBase property) { - VerifyAccess(); - _values?.Revalidate(property); + _values?.CoerceValue(property); + } + + /// + void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child) + { + _inheritanceChildren ??= new List(); + _inheritanceChildren.Add(child); } - internal void PriorityValueChanged(AvaloniaProperty property, int priority, object oldValue, object newValue) + /// + void IAvaloniaObject.RemoveInheritanceChild(IAvaloniaObject child) { - oldValue = (oldValue == AvaloniaProperty.UnsetValue) ? - GetDefaultValue(property) : - oldValue; - newValue = (newValue == AvaloniaProperty.UnsetValue) ? - GetDefaultValue(property) : - newValue; + _inheritanceChildren?.Remove(child); + } - if (!Equals(oldValue, newValue)) + void IAvaloniaObject.InheritedPropertyChanged( + AvaloniaProperty property, + Optional oldValue, + Optional newValue) + { + if (property.Inherits && (_values == null || !_values.IsSet(property))) { - RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)priority); - - Logger.TryGet(LogEventLevel.Verbose)?.Log( - LogArea.Property, - this, - "{Property} changed from {$Old} to {$Value} with priority {Priority}", - property, - oldValue, - newValue, - (BindingPriority)priority); + RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue); } } - - internal void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification) - { - LogIfError(property, notification); - UpdateDataValidation(property, notification); - } /// Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers() @@ -427,26 +472,109 @@ namespace Avalonia return _propertyChanged?.GetInvocationList(); } - /// - /// Gets all priority values set on the object. - /// - /// A collection of property/value tuples. - internal IDictionary GetSetValues() => Values?.GetSetValues(); + void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs change) + { + var property = (StyledPropertyBase)change.Property; + + LogIfError(property, change.NewValue); + + // If the change is to the effective value of the property and no old/new value is set + // then fill in the old/new value from property inheritance/default value. We don't do + // this for non-effective value changes because these are only needed for property + // transitions, where knowing e.g. that an inherited value is active at an arbitrary + // priority isn't of any use and would introduce overhead. + if (change.IsEffectiveValueChange && !change.OldValue.HasValue) + { + change.SetOldValue(GetInheritedOrDefault(property)); + } + + if (change.IsEffectiveValueChange && !change.NewValue.HasValue) + { + change.SetNewValue(GetInheritedOrDefault(property)); + } + + if (!change.IsEffectiveValueChange || + !EqualityComparer.Default.Equals(change.OldValue.Value, change.NewValue.Value)) + { + RaisePropertyChanged(change); + + if (change.IsEffectiveValueChange) + { + Logger.TryGet(LogEventLevel.Verbose, LogArea.Property)?.Log( + this, + "{Property} changed from {$Old} to {$Value} with priority {Priority}", + property, + change.OldValue, + change.NewValue, + change.Priority); + } + } + } + + void IValueSink.Completed( + StyledPropertyBase property, + IPriorityValueEntry entry, + Optional oldValue) + { + var change = new AvaloniaPropertyChangedEventArgs( + this, + property, + oldValue, + default, + BindingPriority.Unset); + ((IValueSink)this).ValueChanged(change); + } /// - /// Forces revalidation of properties when a property value changes. + /// Called for each inherited property when the changes. /// - /// The property to that affects validation. - /// The affected properties. - protected static void AffectsValidation(AvaloniaProperty property, params AvaloniaProperty[] affected) + /// The type of the property value. + /// The property. + /// The old inheritance parent. + internal void InheritanceParentChanged( + StyledPropertyBase property, + IAvaloniaObject oldParent) + { + var oldValue = oldParent switch + { + AvaloniaObject o => o.GetValueOrInheritedOrDefault(property), + null => property.GetDefaultValue(GetType()), + _ => oldParent.GetValue(property) + }; + + var newValue = GetInheritedOrDefault(property); + + if (!EqualityComparer.Default.Equals(oldValue, newValue)) + { + RaisePropertyChanged(property, oldValue, newValue); + } + } + + internal AvaloniaPropertyValue GetDiagnosticInternal(AvaloniaProperty property) { - property.Changed.Subscribe(e => + if (property.IsDirect) { - foreach (var p in affected) + return new AvaloniaPropertyValue( + property, + GetValue(property), + BindingPriority.Unset, + "Local Value"); + } + else if (_values != null) + { + var result = _values.GetDiagnostic(property); + + if (result != null) { - e.Sender.Revalidate(p); + return result; } - }); + } + + return new AvaloniaPropertyValue( + property, + GetValue(property), + BindingPriority.Unset, + "Unset"); } /// @@ -456,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, @@ -470,18 +597,30 @@ namespace Avalonia /// enabled. /// /// The property. - /// The new validation status. - protected virtual void UpdateDataValidation( - AvaloniaProperty property, - BindingNotification status) + /// The new binding value for the property. + protected virtual void UpdateDataValidation( + AvaloniaProperty property, + BindingValue value) { } /// /// Called when a avalonia property changes on the object. /// - /// The event arguments. - protected virtual void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + /// The property change details. + protected virtual void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) + { + if (change.IsEffectiveValueChange) + { + OnPropertyChanged(change); + } + } + + /// + /// Called when a avalonia property changes on the object. + /// + /// The property change details. + protected virtual void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { } @@ -492,46 +631,18 @@ namespace Avalonia /// The old property value. /// The new property value. /// The priority of the binding that produced the value. - protected internal void RaisePropertyChanged( - AvaloniaProperty property, - object oldValue, - object newValue, + protected internal void RaisePropertyChanged( + AvaloniaProperty property, + Optional oldValue, + BindingValue newValue, BindingPriority priority = BindingPriority.LocalValue) { - Contract.Requires(property != null); - VerifyAccess(); - - AvaloniaPropertyChangedEventArgs e = new AvaloniaPropertyChangedEventArgs( + RaisePropertyChanged(new AvaloniaPropertyChangedEventArgs( this, property, oldValue, newValue, - priority); - - property.Notifying?.Invoke(this, true); - - try - { - OnPropertyChanged(e); - property.NotifyChanged(e); - - _propertyChanged?.Invoke(this, e); - - if (_inpcChanged != null) - { - PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name); - _inpcChanged(this, e2); - } - - if (property.Inherits) - { - _inheritablePropertyChanged?.Invoke(this, e); - } - } - finally - { - property.Notifying?.Invoke(this, false); - } + priority)); } /// @@ -554,216 +665,150 @@ namespace Avalonia return false; } - DeferredSetter setter = Values.GetDirectDeferredSetter(property); - - return setter.SetAndNotify(this, property, ref field, value); + var old = field; + field = value; + RaisePropertyChanged(property, old, value); + return true; } - /// - /// Tries to cast a value to a type, taking into account that the value may be a - /// . - /// - /// The value. - /// The type. - /// The cast value, or a . - private static object CastOrDefault(object value, Type type) + private T GetInheritedOrDefault(StyledPropertyBase property) { - var notification = value as BindingNotification; - - if (notification == null) + if (property.Inherits && InheritanceParent is AvaloniaObject o) { - return TypeUtilities.ConvertImplicitOrDefault(value, type); + return o.GetValueOrInheritedOrDefault(property); } - else - { - if (notification.HasValue) - { - notification.SetValue(TypeUtilities.ConvertImplicitOrDefault(notification.Value, type)); - } - return notification; - } + return property.GetDefaultValue(GetType()); } - /// - /// Gets the default value for a property. - /// - /// The property. - /// The default value. - private object GetDefaultValue(AvaloniaProperty property) + private T GetValueOrInheritedOrDefault( + StyledPropertyBase property, + BindingPriority maxPriority = BindingPriority.Animation) { - if (property.Inherits && InheritanceParent is AvaloniaObject aobj) - return aobj.GetValueOrDefaultUnchecked(property); - return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType()); - } + var o = this; + var inherits = property.Inherits; + var value = default(T); - /// - /// Gets the value or default value for a property. - /// - /// The property. - /// The default value. - private object GetValueOrDefaultUnchecked(AvaloniaProperty property) - { - var aobj = this; - var valuestore = aobj._values; - if (valuestore != null) + while (o != null) { - var result = valuestore.GetValue(property); - if (result != AvaloniaProperty.UnsetValue) + var values = o._values; + + if (values?.TryGetValue(property, maxPriority, out value) == true) { - return result; + return value; } - } - if (property.Inherits) - { - while (aobj.InheritanceParent is AvaloniaObject parent) + + if (!inherits) { - aobj = parent; - valuestore = aobj._values; - if (valuestore != null) - { - var result = valuestore.GetValue(property); - if (result != AvaloniaProperty.UnsetValue) - { - return result; - } - } + break; } + + o = o.InheritanceParent as AvaloniaObject; } - return ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); + + return property.GetDefaultValue(GetType()); } - /// - /// Sets the value of a direct property. - /// - /// The property. - /// The value. - private void SetDirectValue(AvaloniaProperty property, object value) + protected internal void RaisePropertyChanged(AvaloniaPropertyChangedEventArgs change) { - void Set() + VerifyAccess(); + + if (change.IsEffectiveValueChange) { - var notification = value as BindingNotification; + change.Property.Notifying?.Invoke(this, true); + } - if (notification != null) - { - LogIfError(property, notification); - value = notification.Value; - } + try + { + OnPropertyChangedCore(change); - if (notification == null || notification.ErrorType == BindingErrorType.Error || notification.HasValue) + if (change.IsEffectiveValueChange) { - var metadata = (IDirectPropertyMetadata)property.GetMetadata(GetType()); - var accessor = (IDirectPropertyAccessor)GetRegistered(property); - var finalValue = value == AvaloniaProperty.UnsetValue ? - metadata.UnsetValue : value; - - LogPropertySet(property, value, BindingPriority.LocalValue); + change.Property.NotifyChanged(change); + _propertyChanged?.Invoke(this, change); - accessor.SetValue(this, finalValue); - } + if (_inpcChanged != null) + { + var inpce = new PropertyChangedEventArgs(change.Property.Name); + _inpcChanged(this, inpce); + } - if (notification != null) - { - UpdateDataValidation(property, notification); + if (change.Property.Inherits && _inheritanceChildren != null) + { + foreach (var child in _inheritanceChildren) + { + child.InheritedPropertyChanged( + change.Property, + change.OldValue, + change.NewValue.ToOptional()); + } + } } } - - if (Dispatcher.UIThread.CheckAccess()) - { - Set(); - } - else + finally { - Dispatcher.UIThread.Post(Set); + if (change.IsEffectiveValueChange) + { + change.Property.Notifying?.Invoke(this, false); + } } } /// - /// Sets the value of a styled property. + /// Sets the value of a direct property. /// /// The property. /// The value. - /// The priority of the value. - private void SetStyledValue(AvaloniaProperty property, object value, BindingPriority priority) + private void SetDirectValueUnchecked(DirectPropertyBase property, T value) { - var notification = value as BindingNotification; + var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); - // We currently accept BindingNotifications for non-direct properties but we just - // strip them to their underlying value. - if (notification != null) + if (value is UnsetValueType) { - if (!notification.HasValue) - { - return; - } - else - { - value = notification.Value; - } + p.InvokeSetter(this, p.GetUnsetValue(GetType())); } - - var originalValue = value; - - if (!TypeUtilities.TryConvertImplicit(property.PropertyType, value, out value)) + else if (!(value is DoNothingType)) { - throw new ArgumentException(string.Format( - "Invalid value for Property '{0}': '{1}' ({2})", - property.Name, - originalValue, - originalValue?.GetType().FullName ?? "(null)")); + p.InvokeSetter(this, value); } - - LogPropertySet(property, value, priority); - Values.AddValue(property, value, (int)priority); } /// - /// Given a direct property, returns a registered avalonia property that is equivalent or - /// throws if not found. + /// Sets the value of a direct property. /// /// The property. - /// The registered property. - private AvaloniaProperty GetRegistered(AvaloniaProperty property) + /// The value. + private void SetDirectValueUnchecked(DirectPropertyBase property, BindingValue value) { - var direct = property as IDirectPropertyAccessor; - - if (direct == null) - { - throw new AvaloniaInternalException( - "AvaloniaObject.GetRegistered should only be called for direct properties"); - } + var p = AvaloniaPropertyRegistry.Instance.FindRegisteredDirect(this, property); - if (property.OwnerType.IsAssignableFrom(GetType())) - { - return property; - } - - var result = AvaloniaPropertyRegistry.Instance.GetRegistered(this) - .FirstOrDefault(x => x == property); - - if (result == null) + if (p == null) { throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}"); } - return result; - } - - /// - /// Called when a property is changed on the current . - /// - /// The event sender. - /// The event args. - /// - /// Checks for changes in an inherited property value. - /// - private void ParentPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - Contract.Requires(e != null); + LogIfError(property, value); + + switch (value.Type) + { + case BindingValueType.UnsetValue: + case BindingValueType.BindingError: + var fallback = value.HasValue ? value : value.WithValue(property.GetUnsetValue(GetType())); + property.InvokeSetter(this, fallback); + break; + case BindingValueType.DataValidationError: + property.InvokeSetter(this, value); + break; + case BindingValueType.Value: + case BindingValueType.BindingErrorWithFallback: + case BindingValueType.DataValidationErrorWithFallback: + property.InvokeSetter(this, value); + break; + } - if (e.Property.Inherits && !IsSet(e.Property)) + if (p.IsDataValidationEnabled) { - RaisePropertyChanged(e.Property, e.OldValue, e.NewValue, BindingPriority.LocalValue); + UpdateDataValidation(property, value); } } @@ -772,7 +817,7 @@ namespace Avalonia /// /// The observable. /// The description. - private string GetDescription(IObservable o) + private string GetDescription(object o) { var description = o as IDescription; return description?.Description ?? o.ToString(); @@ -782,12 +827,12 @@ namespace Avalonia /// Logs a mesage if the notification represents a binding error. /// /// The property being bound. - /// The binding notification. - private void LogIfError(AvaloniaProperty property, BindingNotification notification) + /// The binding notification. + private void LogIfError(AvaloniaProperty property, BindingValue value) { - if (notification.ErrorType == BindingErrorType.Error) + if (value.HasError) { - if (notification.Error is AggregateException aggregate) + if (value.Error is AggregateException aggregate) { foreach (var inner in aggregate.InnerExceptions) { @@ -796,7 +841,7 @@ namespace Avalonia } else { - LogBindingError(property, notification.Error); + LogBindingError(property, value.Error); } } } @@ -807,10 +852,9 @@ namespace Avalonia /// The property. /// The new value. /// The priority. - private void LogPropertySet(AvaloniaProperty property, object value, BindingPriority 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, @@ -818,16 +862,16 @@ namespace Avalonia priority); } - private class DirectBindingSubscription : IObserver, IDisposable + private class DirectBindingSubscription : IObserver>, IDisposable { - readonly AvaloniaObject _owner; - readonly AvaloniaProperty _property; - IDisposable _subscription; + private readonly AvaloniaObject _owner; + private readonly DirectPropertyBase _property; + private readonly IDisposable _subscription; public DirectBindingSubscription( AvaloniaObject owner, - AvaloniaProperty property, - IObservable source) + DirectPropertyBase property, + IObservable> source) { _owner = owner; _property = property; @@ -843,11 +887,22 @@ namespace Avalonia public void OnCompleted() => Dispose(); public void OnError(Exception error) => Dispose(); - - public void OnNext(object value) + public void OnNext(BindingValue value) { - var castValue = CastOrDefault(value, _property.PropertyType); - _owner.SetDirectValue(_property, castValue); + if (Dispatcher.UIThread.CheckAccess()) + { + _owner.SetDirectValueUnchecked(_property, value); + } + else + { + // To avoid allocating closure in the outer scope we need to capture variables + // locally. This allows us to skip most of the allocations when on UI thread. + var instance = _owner; + var property = _property; + var newValue = value; + + Dispatcher.UIThread.Post(() => instance.SetDirectValueUnchecked(property, newValue)); + } } } } diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index ad1cefd4ea..173c5c1a94 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Reactive; using System.Reactive.Disposables; @@ -68,6 +65,51 @@ namespace Avalonia return new AvaloniaPropertyObservable(o, property); } + /// + /// Gets an observable for a . + /// + /// The object. + /// The property. + /// + /// An observable which fires immediately with the current value of the property on the + /// object and subsequently each time the property value changes. + /// + /// + /// The subscription to is created using a weak reference. + /// + public static IObservable> GetBindingObservable( + this IAvaloniaObject o, + AvaloniaProperty property) + { + Contract.Requires(o != null); + Contract.Requires(property != null); + + return new AvaloniaPropertyBindingObservable(o, property); + } + + /// + /// Gets an observable for a . + /// + /// The object. + /// The property type. + /// The property. + /// + /// An observable which fires immediately with the current value of the property on the + /// object and subsequently each time the property value changes. + /// + /// + /// The subscription to is created using a weak reference. + /// + public static IObservable> GetBindingObservable( + this IAvaloniaObject o, + AvaloniaProperty property) + { + Contract.Requires(o != null); + Contract.Requires(property != null); + + return new AvaloniaPropertyBindingObservable(o, property); + } + /// /// Gets an observable that listens for property changed events for an /// . @@ -80,7 +122,7 @@ namespace Avalonia /// for the specified property. /// public static IObservable GetPropertyChangedObservable( - this IAvaloniaObject o, + this IAvaloniaObject o, AvaloniaProperty property) { Contract.Requires(o != null); @@ -134,6 +176,167 @@ namespace Avalonia o.GetObservable(property)); } + /// + /// Gets a subject for a . + /// + /// The object. + /// The property. + /// + /// The priority with which binding values are written to the object. + /// + /// + /// An which can be used for two-way binding to/from the + /// property. + /// + public static ISubject> GetBindingSubject( + this IAvaloniaObject o, + AvaloniaProperty property, + BindingPriority priority = BindingPriority.LocalValue) + { + return Subject.Create>( + Observer.Create>(x => + { + if (x.HasValue) + { + o.SetValue(property, x.Value, priority); + } + }), + o.GetBindingObservable(property)); + } + + /// + /// Gets a subject for a . + /// + /// The property type. + /// The object. + /// The property. + /// + /// The priority with which binding values are written to the object. + /// + /// + /// An which can be used for two-way binding to/from the + /// property. + /// + public static ISubject> GetBindingSubject( + this IAvaloniaObject o, + AvaloniaProperty property, + BindingPriority priority = BindingPriority.LocalValue) + { + return Subject.Create>( + Observer.Create>(x => + { + if (x.HasValue) + { + o.SetValue(property, x.Value, priority); + } + }), + o.GetBindingObservable(property)); + } + + /// + /// Binds a to an observable. + /// + /// The object. + /// The property. + /// The observable. + /// The priority of the binding. + /// + /// A disposable which can be used to terminate the binding. + /// + public static IDisposable Bind( + this IAvaloniaObject target, + AvaloniaProperty property, + IObservable> source, + BindingPriority priority = BindingPriority.LocalValue) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + source = source ?? throw new ArgumentNullException(nameof(source)); + + return property.RouteBind(target, source, priority); + } + + /// + /// Binds a to an observable. + /// + /// The type of the property. + /// The object. + /// The property. + /// The observable. + /// The priority of the binding. + /// + /// A disposable which can be used to terminate the binding. + /// + public static IDisposable Bind( + this IAvaloniaObject target, + AvaloniaProperty property, + IObservable> source, + BindingPriority priority = BindingPriority.LocalValue) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + source = source ?? throw new ArgumentNullException(nameof(source)); + + return property switch + { + StyledPropertyBase styled => target.Bind(styled, source, priority), + DirectPropertyBase direct => target.Bind(direct, source), + _ => throw new NotSupportedException("Unsupported AvaloniaProperty type."), + }; + } + + /// + /// Binds a to an observable. + /// + /// The object. + /// The property. + /// The observable. + /// The priority of the binding. + /// + /// A disposable which can be used to terminate the binding. + /// + public static IDisposable Bind( + this IAvaloniaObject target, + AvaloniaProperty property, + IObservable source, + BindingPriority priority = BindingPriority.LocalValue) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + source = source ?? throw new ArgumentNullException(nameof(source)); + + return target.Bind( + property, + source.ToBindingValue(), + priority); + } + + /// + /// Binds a to an observable. + /// + /// The object. + /// The property. + /// The observable. + /// The priority of the binding. + /// + /// A disposable which can be used to terminate the binding. + /// + public static IDisposable Bind( + this IAvaloniaObject target, + AvaloniaProperty property, + IObservable source, + BindingPriority priority = BindingPriority.LocalValue) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + source = source ?? throw new ArgumentNullException(nameof(source)); + + return target.Bind( + property, + source.ToBindingValue(), + priority); + } + /// /// Binds a property on an to an . /// @@ -153,16 +356,16 @@ namespace Avalonia IBinding binding, object anchor = null) { - Contract.Requires(target != null); - Contract.Requires(property != null); - Contract.Requires(binding != null); + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + binding = binding ?? throw new ArgumentNullException(nameof(binding)); var metadata = property.GetMetadata(target.GetType()) as IDirectPropertyMetadata; var result = binding.Initiate( target, property, - anchor, + anchor, metadata?.EnableDataValidation ?? false); if (result != null) @@ -175,6 +378,189 @@ namespace Avalonia } } + /// + /// Clears a 's local value. + /// + /// The object. + /// The property. + public static void ClearValue(this IAvaloniaObject target, AvaloniaProperty property) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + property.RouteClearValue(target); + } + + /// + /// Clears a 's local value. + /// + /// The object. + /// The property. + public static void ClearValue(this IAvaloniaObject target, AvaloniaProperty property) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + switch (property) + { + case StyledPropertyBase styled: + target.ClearValue(styled); + break; + case DirectPropertyBase direct: + target.ClearValue(direct); + break; + default: + throw new NotSupportedException("Unsupported AvaloniaProperty type."); + } + } + + /// + /// Gets a value. + /// + /// The object. + /// The property. + /// The value. + public static object GetValue(this IAvaloniaObject target, AvaloniaProperty property) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + return property.RouteGetValue(target); + } + + /// + /// Gets a value. + /// + /// The type of the property. + /// The object. + /// The property. + /// The value. + public static T GetValue(this IAvaloniaObject target, AvaloniaProperty property) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + return property switch + { + StyledPropertyBase styled => target.GetValue(styled), + DirectPropertyBase direct => target.GetValue(direct), + _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") + }; + } + + /// + /// Gets an base value. + /// + /// The object. + /// The property. + /// The maximum priority for the value. + /// + /// For styled properties, gets the value of the property if set on the object with a + /// priority equal or lower to , otherwise + /// . Note that this method does not return + /// property values that come from inherited or default values. + /// + /// For direct properties returns . + /// + public static object GetBaseValue( + this IAvaloniaObject target, + AvaloniaProperty property, + BindingPriority maxPriority) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + return property.RouteGetBaseValue(target, maxPriority); + } + + /// + /// Gets an base value. + /// + /// The object. + /// The property. + /// The maximum priority for the value. + /// + /// For styled properties, gets the value of the property if set on the object with a + /// priority equal or lower to , otherwise + /// . Note that this method does not return property values + /// that come from inherited or default values. + /// + /// For direct properties returns + /// . + /// + public static Optional GetBaseValue( + this IAvaloniaObject target, + AvaloniaProperty property, + BindingPriority maxPriority) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + return property switch + { + StyledPropertyBase styled => target.GetBaseValue(styled, maxPriority), + DirectPropertyBase direct => target.GetValue(direct), + _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") + }; + } + + /// + /// Sets a value. + /// + /// The object. + /// The property. + /// The value. + /// The priority of the value. + /// + /// An if setting the property can be undone, otherwise null. + /// + public static IDisposable SetValue( + this IAvaloniaObject target, + AvaloniaProperty property, + object value, + BindingPriority priority = BindingPriority.LocalValue) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + return property.RouteSetValue(target, value, priority); + } + + /// + /// Sets a value. + /// + /// The type of the property. + /// The object. + /// The property. + /// The value. + /// The priority of the value. + /// + /// An if setting the property can be undone, otherwise null. + /// + public static IDisposable SetValue( + this IAvaloniaObject target, + AvaloniaProperty property, + T value, + BindingPriority priority = BindingPriority.LocalValue) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + switch (property) + { + case StyledPropertyBase styled: + return target.SetValue(styled, value, priority); + case DirectPropertyBase direct: + target.SetValue(direct, value); + return null; + default: + throw new NotSupportedException("Unsupported AvaloniaProperty type."); + } + } + /// /// Subscribes to a property changed notifications for changes that originate from a /// . diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 27c20e57c4..c6733f47b4 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -1,11 +1,6 @@ -// 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 System; using System.Collections.Generic; -using System.Diagnostics; using System.Reactive.Subjects; -using System.Reflection; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Utilities; @@ -15,7 +10,7 @@ namespace Avalonia /// /// Base class for avalonia properties. /// - public class AvaloniaProperty : IEquatable, IPropertyInfo + public abstract class AvaloniaProperty : IEquatable, IPropertyInfo { /// /// Represents an unset property value. @@ -23,12 +18,13 @@ namespace Avalonia public static readonly object UnsetValue = new UnsetValueType(); private static int s_nextId; - private readonly Subject _initialized; private readonly Subject _changed; private readonly PropertyMetadata _defaultMetadata; private readonly Dictionary _metadata; private readonly Dictionary _metadataCache = new Dictionary(); + private bool _hasMetadataOverrides; + /// /// Initializes a new instance of the class. /// @@ -54,7 +50,6 @@ namespace Avalonia throw new ArgumentException("'name' may not contain periods."); } - _initialized = new Subject(); _changed = new Subject(); _metadata = new Dictionary(); @@ -82,7 +77,6 @@ namespace Avalonia Contract.Requires(source != null); Contract.Requires(ownerType != null); - _initialized = source._initialized; _changed = source._changed; _metadata = new Dictionary(); @@ -93,6 +87,9 @@ namespace Avalonia Id = source.Id; _defaultMetadata = source._defaultMetadata; + // Properties that have different owner can't use fast path for metadata. + _hasMetadataOverrides = true; + if (metadata != null) { _metadata.Add(ownerType, metadata); @@ -134,22 +131,6 @@ namespace Avalonia /// public virtual bool IsReadOnly => false; - /// - /// Gets an observable that is fired when this property is initialized on a - /// new instance. - /// - /// - /// This observable is fired each time a new is constructed - /// for all properties registered on the object's type. The default value of the property - /// for the object is passed in the args' NewValue (OldValue will always be - /// . - /// - /// - /// An observable that is fired when this property is initialized on a new - /// instance. - /// - public IObservable Initialized => _initialized; - /// /// Gets an observable that is fired when this property changes on any /// instance. @@ -179,6 +160,8 @@ namespace Avalonia /// internal int Id { get; } + internal bool HasChangedSubscriptions => _changed?.HasObservers ?? false; + /// /// Provides access to a property's binding via the /// indexer. @@ -251,7 +234,8 @@ namespace Avalonia /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. - /// A validation function. + /// A value validation callback. + /// A value coercion callback. /// /// A method that gets called before and after the property starts being notified on an /// object; the bool argument will be true before and false afterwards. This callback is @@ -263,7 +247,8 @@ namespace Avalonia TValue defaultValue = default(TValue), bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, - Func validate = null, + Func validate = null, + Func coerce = null, Action notifying = null) where TOwner : IAvaloniaObject { @@ -271,14 +256,15 @@ namespace Avalonia var metadata = new StyledPropertyMetadata( defaultValue, - validate: Cast(validate), - defaultBindingMode: defaultBindingMode); + defaultBindingMode: defaultBindingMode, + coerce: coerce); var result = new StyledProperty( name, typeof(TOwner), metadata, inherits, + validate, notifying); AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result); return result; @@ -294,24 +280,26 @@ namespace Avalonia /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. - /// A validation function. + /// A value validation callback. + /// A value coercion callback. /// A public static AttachedProperty RegisterAttached( string name, TValue defaultValue = default(TValue), bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, - Func validate = null) + Func validate = null, + Func coerce = null) where THost : IAvaloniaObject { Contract.Requires(name != null); var metadata = new StyledPropertyMetadata( defaultValue, - validate: Cast(validate), - defaultBindingMode: defaultBindingMode); + defaultBindingMode: defaultBindingMode, + coerce: coerce); - var result = new AttachedProperty(name, typeof(TOwner), metadata, inherits); + var result = new AttachedProperty(name, typeof(TOwner), metadata, inherits, validate); var registry = AvaloniaPropertyRegistry.Instance; registry.Register(typeof(TOwner), result); registry.RegisterAttached(typeof(THost), result); @@ -328,7 +316,8 @@ namespace Avalonia /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. - /// A validation function. + /// A value validation callback. + /// A value coercion callback. /// A public static AttachedProperty RegisterAttached( string name, @@ -336,17 +325,18 @@ namespace Avalonia TValue defaultValue = default(TValue), bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, - Func validate = null) + Func validate = null, + Func coerce = null) where THost : IAvaloniaObject { Contract.Requires(name != null); var metadata = new StyledPropertyMetadata( defaultValue, - validate: Cast(validate), - defaultBindingMode: defaultBindingMode); + defaultBindingMode: defaultBindingMode, + coerce: coerce); - var result = new AttachedProperty(name, ownerType, metadata, inherits); + var result = new AttachedProperty(name, ownerType, metadata, inherits, validate); var registry = AvaloniaPropertyRegistry.Instance; registry.Register(ownerType, result); registry.RegisterAttached(typeof(THost), result); @@ -361,9 +351,7 @@ namespace Avalonia /// The name of the property. /// Gets the current value of the property. /// Sets the value of the property. - /// - /// The value to use when the property is set to - /// + /// The value to use when the property is cleared. /// The default binding mode for the property. /// /// Whether the property is interested in data validation. @@ -379,13 +367,18 @@ namespace Avalonia where TOwner : IAvaloniaObject { Contract.Requires(name != null); + Contract.Requires(getter != null); var metadata = new DirectPropertyMetadata( unsetValue: unsetValue, - defaultBindingMode: defaultBindingMode, - enableDataValidation: enableDataValidation); + defaultBindingMode: defaultBindingMode); - var result = new DirectProperty(name, getter, setter, metadata); + var result = new DirectProperty( + name, + getter, + setter, + metadata, + enableDataValidation); AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result); return result; } @@ -447,31 +440,12 @@ namespace Avalonia /// public PropertyMetadata GetMetadata(Type type) { - Contract.Requires(type != null); - - PropertyMetadata result; - Type currentType = type; - - if (_metadataCache.TryGetValue(type, out result)) + if (!_hasMetadataOverrides) { - return result; - } - - while (currentType != null) - { - if (_metadata.TryGetValue(currentType, out result)) - { - _metadataCache[type] = result; - - return result; - } - - currentType = currentType.GetTypeInfo().BaseType; + return _defaultMetadata; } - _metadataCache[type] = _defaultMetadata; - - return _defaultMetadata; + return GetMetadataWithOverrides(type); } /// @@ -494,18 +468,13 @@ namespace Avalonia } /// - /// True if has any observers. - /// - internal bool HasNotifyInitializedObservers => _initialized.HasObservers; - - /// - /// Notifies the observable. + /// Uses the visitor pattern to resolve an untyped property to a typed property. /// - /// The observable arguments. - internal void NotifyInitialized(AvaloniaPropertyChangedEventArgs e) - { - _initialized.OnNext(e); - } + /// The type of user data passed. + /// The visitor which will accept the typed property. + /// The user data to pass. + public abstract void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) + where TData : struct; /// /// Notifies the observable. @@ -516,6 +485,52 @@ namespace Avalonia _changed.OnNext(e); } + /// + /// Routes an untyped ClearValue call to a typed call. + /// + /// The object instance. + internal abstract void RouteClearValue(IAvaloniaObject o); + + /// + /// Routes an untyped GetValue call to a typed call. + /// + /// The object instance. + internal abstract object RouteGetValue(IAvaloniaObject o); + + /// + /// Routes an untyped GetBaseValue call to a typed call. + /// + /// The object instance. + /// The maximum priority for the value. + internal abstract object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority); + + /// + /// Routes an untyped SetValue call to a typed call. + /// + /// The object instance. + /// The value. + /// The priority. + /// + /// An if setting the property can be undone, otherwise null. + /// + internal abstract IDisposable? RouteSetValue( + IAvaloniaObject o, + object value, + BindingPriority priority); + + /// + /// Routes an untyped Bind call to a typed call. + /// + /// The object instance. + /// The binding source. + /// The priority. + internal abstract IDisposable RouteBind( + IAvaloniaObject o, + IObservable> source, + BindingPriority priority); + + internal abstract void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject oldParent); + /// /// Overrides the metadata for the property on the specified type. /// @@ -536,20 +551,39 @@ namespace Avalonia metadata.Merge(baseMetadata, this); _metadata.Add(type, metadata); _metadataCache.Clear(); + + _hasMetadataOverrides = true; } - [DebuggerHidden] - private static Func Cast(Func f) - where TOwner : IAvaloniaObject + private PropertyMetadata GetMetadataWithOverrides(Type type) { - if (f != null) + if (type is null) { - return (o, v) => (o is TOwner) ? f((TOwner)o, v) : v; + throw new ArgumentNullException(nameof(type)); } - else + + if (_metadataCache.TryGetValue(type, out PropertyMetadata result)) { - return null; + return result; + } + + Type currentType = type; + + while (currentType != null) + { + if (_metadata.TryGetValue(currentType, out result)) + { + _metadataCache[type] = result; + + return result; + } + + currentType = currentType.BaseType; } + + _metadataCache[type] = _defaultMetadata; + + return _defaultMetadata; } bool IPropertyInfo.CanGet => true; @@ -557,11 +591,14 @@ namespace Avalonia object IPropertyInfo.Get(object target) => ((AvaloniaObject)target).GetValue(this); void IPropertyInfo.Set(object target, object value) => ((AvaloniaObject)target).SetValue(this, value); } + /// /// Class representing the . /// - public class UnsetValueType + public sealed class UnsetValueType { + internal UnsetValueType() { } + /// /// Returns the string representation of the . /// diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs index 6082367723..c1a2832fde 100644 --- a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs +++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs @@ -1,43 +1,29 @@ -// 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 System; using Avalonia.Data; +#nullable enable + namespace Avalonia { /// /// Provides information for a avalonia property change. /// - public class AvaloniaPropertyChangedEventArgs : EventArgs + public abstract class AvaloniaPropertyChangedEventArgs : EventArgs { - /// - /// Initializes a new instance of the class. - /// - /// The object that the property changed on. - /// The property that changed. - /// The old value of the property. - /// The new value of the property. - /// The priority of the binding that produced the value. public AvaloniaPropertyChangedEventArgs( - AvaloniaObject sender, - AvaloniaProperty property, - object oldValue, - object newValue, + IAvaloniaObject sender, BindingPriority priority) { Sender = sender; - Property = property; - OldValue = oldValue; - NewValue = newValue; Priority = priority; + IsEffectiveValueChange = true; } /// /// Gets the that the property changed on. /// /// The sender object. - public AvaloniaObject Sender { get; private set; } + public IAvaloniaObject Sender { get; } /// /// Gets the property that changed. @@ -45,30 +31,42 @@ namespace Avalonia /// /// The property that changed. /// - public AvaloniaProperty Property { get; private set; } + public AvaloniaProperty Property => GetProperty(); /// /// Gets the old value of the property. /// - /// - /// The old value of the property. - /// - public object OldValue { get; private set; } + public object? OldValue => GetOldValue(); /// /// Gets the new value of the property. /// - /// - /// The new value of the property. - /// - public object NewValue { get; private set; } + public object? NewValue => GetNewValue(); /// /// Gets the priority of the binding that produced the value. /// /// - /// The priority of the binding that produced the value. + /// The priority of the new value. /// public BindingPriority Priority { get; private set; } + + /// + /// Gets a value indicating whether the change represents a change to the effective value of + /// the property. + /// + /// + /// This will usually be true, except in + /// + /// which recieves notifications for all changes to property values, whether a value with a higher + /// priority is present or not. When this property is false, the change that is being signalled + /// has not resulted in a change to the property value on the object. + /// + public bool IsEffectiveValueChange { get; private set; } + + internal void MarkNonEffectiveValue() => IsEffectiveValueChange = false; + protected abstract AvaloniaProperty GetProperty(); + protected abstract object? GetOldValue(); + protected abstract object? GetNewValue(); } } diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs new file mode 100644 index 0000000000..054bf93b3a --- /dev/null +++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs @@ -0,0 +1,72 @@ +using Avalonia.Data; + +#nullable enable + +namespace Avalonia +{ + /// + /// Provides information for an Avalonia property change. + /// + public class AvaloniaPropertyChangedEventArgs : AvaloniaPropertyChangedEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The object that the property changed on. + /// The property that changed. + /// The old value of the property. + /// The new value of the property. + /// The priority of the binding that produced the value. + public AvaloniaPropertyChangedEventArgs( + IAvaloniaObject sender, + AvaloniaProperty property, + Optional oldValue, + BindingValue newValue, + BindingPriority priority) + : base(sender, priority) + { + Property = property; + OldValue = oldValue; + NewValue = newValue; + } + + /// + /// Gets the property that changed. + /// + /// + /// The property that changed. + /// + public new AvaloniaProperty Property { get; } + + /// + /// Gets the old value of the property. + /// + /// + /// When is true, returns the + /// old value of the property on the object. + /// When is false, returns + /// . + /// + public new Optional OldValue { get; private set; } + + /// + /// Gets the new value of the property. + /// + /// + /// When is true, returns the + /// value of the property on the object. + /// When is false returns the + /// changed value, or if the value was removed. + /// + public new BindingValue NewValue { get; private set; } + + internal void SetOldValue(Optional value) => OldValue = value; + internal void SetNewValue(BindingValue value) => NewValue = value; + + protected override AvaloniaProperty GetProperty() => Property; + + protected override object? GetOldValue() => OldValue.GetValueOrDefault(AvaloniaProperty.UnsetValue); + + protected override object? GetNewValue() => NewValue.GetValueOrDefault(AvaloniaProperty.UnsetValue); + } +} diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index d718f5917c..4a3b104f2a 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -1,11 +1,6 @@ -// 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 System; using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; -using Avalonia.Data; namespace Avalonia { @@ -20,12 +15,14 @@ namespace Avalonia new Dictionary>(); private readonly Dictionary> _attached = new Dictionary>(); + private readonly Dictionary> _direct = + new Dictionary>(); private readonly Dictionary> _registeredCache = new Dictionary>(); private readonly Dictionary> _attachedCache = new Dictionary>(); - private readonly Dictionary> _initializedCache = - new Dictionary>(); + private readonly Dictionary> _directCache = + new Dictionary>(); private readonly Dictionary> _inheritedCache = new Dictionary>(); @@ -45,7 +42,7 @@ namespace Avalonia /// /// The type. /// A collection of definitions. - public IEnumerable GetRegistered(Type type) + public IReadOnlyList GetRegistered(Type type) { Contract.Requires(type != null); @@ -79,7 +76,7 @@ namespace Avalonia /// /// The type. /// A collection of definitions. - public IEnumerable GetRegisteredAttached(Type type) + public IReadOnlyList GetRegisteredAttached(Type type) { Contract.Requires(type != null); @@ -105,12 +102,43 @@ namespace Avalonia return result; } + /// + /// Gets all direct s registered on a type. + /// + /// The type. + /// A collection of definitions. + public IReadOnlyList GetRegisteredDirect(Type type) + { + Contract.Requires(type != null); + + if (_directCache.TryGetValue(type, out var result)) + { + return result; + } + + var t = type; + result = new List(); + + while (t != null) + { + if (_direct.TryGetValue(t, out var direct)) + { + result.AddRange(direct.Values); + } + + t = t.BaseType; + } + + _directCache.Add(type, result); + return result; + } + /// /// Gets all inherited s registered on a type. /// /// The type. /// A collection of definitions. - public IEnumerable GetRegisteredInherited(Type type) + public IReadOnlyList GetRegisteredInherited(Type type) { Contract.Requires(type != null); @@ -122,16 +150,27 @@ namespace Avalonia result = new List(); var visited = new HashSet(); - foreach (var property in GetRegistered(type)) + var registered = GetRegistered(type); + var registeredCount = registered.Count; + + for (var i = 0; i < registeredCount; i++) { + var property = registered[i]; + if (property.Inherits) { result.Add(property); visited.Add(property); } } - foreach (var property in GetRegisteredAttached(type)) + + var registeredAttached = GetRegisteredAttached(type); + var registeredAttachedCount = registeredAttached.Count; + + for (var i = 0; i < registeredAttachedCount; i++) { + var property = registeredAttached[i]; + if (property.Inherits) { if (!visited.Contains(property)) @@ -150,13 +189,29 @@ namespace Avalonia /// /// The object. /// A collection of definitions. - public IEnumerable GetRegistered(AvaloniaObject o) + public IReadOnlyList GetRegistered(IAvaloniaObject o) { Contract.Requires(o != null); return GetRegistered(o.GetType()); } + /// + /// Finds a direct property as registered on an object. + /// + /// The object. + /// The direct property. + /// + /// The registered property or null if no matching property found. + /// + public DirectPropertyBase GetRegisteredDirect( + IAvaloniaObject o, + DirectPropertyBase property) + { + return FindRegisteredDirect(o, property) ?? + throw new ArgumentException($"Property '{property.Name} not registered on '{o.GetType()}"); + } + /// /// Finds a registered property on a type by name. /// @@ -173,12 +228,25 @@ namespace Avalonia Contract.Requires(type != null); Contract.Requires(name != null); - if (name.Contains('.')) + if (name.Contains(".")) { throw new InvalidOperationException("Attached properties not supported."); } - return GetRegistered(type).FirstOrDefault(x => x.Name == name); + var registered = GetRegistered(type); + var registeredCount = registered.Count; + + for (var i = 0; i < registeredCount; i++) + { + AvaloniaProperty x = registered[i]; + + if (x.Name == name) + { + return x; + } + } + + return null; } /// @@ -192,7 +260,7 @@ namespace Avalonia /// /// The property name contains a '.'. /// - public AvaloniaProperty FindRegistered(AvaloniaObject o, string name) + public AvaloniaProperty FindRegistered(IAvaloniaObject o, string name) { Contract.Requires(o != null); Contract.Requires(name != null); @@ -200,6 +268,39 @@ namespace Avalonia return FindRegistered(o.GetType(), name); } + /// + /// Finds a direct property as registered on an object. + /// + /// The object. + /// The direct property. + /// + /// The registered property or null if no matching property found. + /// + public DirectPropertyBase FindRegisteredDirect( + IAvaloniaObject o, + DirectPropertyBase property) + { + if (property.Owner == o.GetType()) + { + return property; + } + + var registeredDirect = GetRegisteredDirect(o.GetType()); + var registeredDirectCount = registeredDirect.Count; + + for (var i = 0; i < registeredDirectCount; i++) + { + var p = registeredDirect[i]; + + if (p == property) + { + return (DirectPropertyBase)p; + } + } + + return null; + } + /// /// Finds a registered property by Id. /// @@ -221,8 +322,23 @@ namespace Avalonia Contract.Requires(type != null); Contract.Requires(property != null); - return Instance.GetRegistered(type).Any(x => x == property) || - Instance.GetRegisteredAttached(type).Any(x => x == property); + static bool ContainsProperty(IReadOnlyList properties, AvaloniaProperty property) + { + var propertiesCount = properties.Count; + + for (var i = 0; i < propertiesCount; i++) + { + if (properties[i] == property) + { + return true; + } + } + + return false; + } + + return ContainsProperty(Instance.GetRegistered(type), property) || + ContainsProperty(Instance.GetRegisteredAttached(type), property); } /// @@ -265,13 +381,28 @@ namespace Avalonia inner.Add(property.Id, property); } + if (property.IsDirect) + { + if (!_direct.TryGetValue(type, out inner)) + { + inner = new Dictionary(); + inner.Add(property.Id, property); + _direct.Add(type, inner); + } + else if (!inner.ContainsKey(property.Id)) + { + inner.Add(property.Id, property); + } + + _directCache.Clear(); + } + if (!_properties.ContainsKey(property.Id)) { _properties.Add(property.Id, property); } _registeredCache.Clear(); - _initializedCache.Clear(); _inheritedCache.Clear(); } @@ -308,96 +439,7 @@ namespace Avalonia } _attachedCache.Clear(); - _initializedCache.Clear(); _inheritedCache.Clear(); } - - internal void NotifyInitialized(AvaloniaObject o) - { - Contract.Requires(o != null); - - var type = o.GetType(); - - void Notify(AvaloniaProperty property, object value) - { - var e = new AvaloniaPropertyChangedEventArgs( - o, - property, - AvaloniaProperty.UnsetValue, - value, - BindingPriority.Unset); - - property.NotifyInitialized(e); - } - - if (!_initializedCache.TryGetValue(type, out var initializationData)) - { - var visited = new HashSet(); - - initializationData = new List(); - - foreach (AvaloniaProperty property in GetRegistered(type)) - { - if (property.IsDirect) - { - initializationData.Add(new PropertyInitializationData(property, (IDirectPropertyAccessor)property)); - } - else - { - initializationData.Add(new PropertyInitializationData(property, (IStyledPropertyAccessor)property, type)); - } - - visited.Add(property); - } - - foreach (AvaloniaProperty property in GetRegisteredAttached(type)) - { - if (!visited.Contains(property)) - { - initializationData.Add(new PropertyInitializationData(property, (IStyledPropertyAccessor)property, type)); - - visited.Add(property); - } - } - - _initializedCache.Add(type, initializationData); - } - - foreach (PropertyInitializationData data in initializationData) - { - if (!data.Property.HasNotifyInitializedObservers) - { - continue; - } - - object value = data.IsDirect ? data.DirectAccessor.GetValue(o) : data.Value; - - Notify(data.Property, value); - } - } - - private readonly struct PropertyInitializationData - { - public AvaloniaProperty Property { get; } - public object Value { get; } - public bool IsDirect { get; } - public IDirectPropertyAccessor DirectAccessor { get; } - - public PropertyInitializationData(AvaloniaProperty property, IDirectPropertyAccessor directAccessor) - { - Property = property; - Value = null; - IsDirect = true; - DirectAccessor = directAccessor; - } - - public PropertyInitializationData(AvaloniaProperty property, IStyledPropertyAccessor styledAccessor, Type type) - { - Property = property; - Value = styledAccessor.GetDefaultValue(type); - IsDirect = false; - DirectAccessor = null; - } - } } } diff --git a/src/Avalonia.Base/AvaloniaProperty`1.cs b/src/Avalonia.Base/AvaloniaProperty`1.cs index 0a223cf7ee..2f26d855f2 100644 --- a/src/Avalonia.Base/AvaloniaProperty`1.cs +++ b/src/Avalonia.Base/AvaloniaProperty`1.cs @@ -1,7 +1,6 @@ -// 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 System; +using Avalonia.Data; +using Avalonia.Utilities; namespace Avalonia { @@ -9,7 +8,7 @@ namespace Avalonia /// A typed avalonia property. /// /// The value type of the property. - public class AvaloniaProperty : AvaloniaProperty + public abstract class AvaloniaProperty : AvaloniaProperty { /// /// Initializes a new instance of the class. @@ -40,5 +39,29 @@ namespace Avalonia : base(source, ownerType, metadata) { } + + protected BindingValue TryConvert(object value) + { + if (value == UnsetValue) + { + return BindingValue.Unset; + } + else if (value == BindingOperations.DoNothing) + { + return BindingValue.DoNothing; + } + + if (!TypeUtilities.TryConvertImplicit(PropertyType, value, out var converted)) + { + var error = new ArgumentException(string.Format( + "Invalid value for Property '{0}': '{1}' ({2})", + Name, + value, + value?.GetType().FullName ?? "(null)")); + return BindingValue.BindingError(error); + } + + return converted; + } } } diff --git a/src/Avalonia.Base/BoxedValue.cs b/src/Avalonia.Base/BoxedValue.cs deleted file mode 100644 index 5fc515f299..0000000000 --- a/src/Avalonia.Base/BoxedValue.cs +++ /dev/null @@ -1,28 +0,0 @@ -// 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. - -namespace Avalonia -{ - /// - /// Represents boxed value of type . - /// - /// Type of stored value. - internal readonly struct BoxedValue - { - public BoxedValue(T value) - { - Boxed = value; - Typed = value; - } - - /// - /// Boxed value. - /// - public object Boxed { get; } - - /// - /// Typed value. - /// - public T Typed { get; } - } -} diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs index 4de4c540a0..a3c51dc965 100644 --- a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs +++ b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections; using System.Collections.Generic; diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index 3c8d4ca7e6..f201cfab1f 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections; using System.Collections.Generic; @@ -327,6 +324,8 @@ namespace Avalonia.Collections } else { + EnsureCapacity(_inner.Count + list.Count); + using (IEnumerator en = items.GetEnumerator()) { int insertIndex = index; @@ -550,6 +549,24 @@ namespace Avalonia.Collections /// Delegate[] INotifyCollectionChangedDebug.GetCollectionChangedSubscribers() => _collectionChanged?.GetInvocationList(); + private void EnsureCapacity(int capacity) + { + // Adapted from List implementation. + var currentCapacity = _inner.Capacity; + + if (currentCapacity < capacity) + { + var newCapacity = currentCapacity == 0 ? 4 : currentCapacity * 2; + + if (newCapacity < capacity) + { + newCapacity = capacity; + } + + _inner.Capacity = newCapacity; + } + } + /// /// Raises the event with an add action. /// diff --git a/src/Avalonia.Base/Collections/AvaloniaListConverter.cs b/src/Avalonia.Base/Collections/AvaloniaListConverter.cs index 63c2e07ecf..f3e22bfe69 100644 --- a/src/Avalonia.Base/Collections/AvaloniaListConverter.cs +++ b/src/Avalonia.Base/Collections/AvaloniaListConverter.cs @@ -1,6 +1,3 @@ -// 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 System; using System.ComponentModel; using System.Globalization; diff --git a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs index 58f3413780..d915887e4c 100644 --- a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs +++ b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections; using System.Collections.Generic; diff --git a/src/Avalonia.Base/Collections/IAvaloniaList.cs b/src/Avalonia.Base/Collections/IAvaloniaList.cs index b7ed9b1ad6..250d4faeb9 100644 --- a/src/Avalonia.Base/Collections/IAvaloniaList.cs +++ b/src/Avalonia.Base/Collections/IAvaloniaList.cs @@ -1,6 +1,3 @@ -// 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 System.Collections.Generic; namespace Avalonia.Collections diff --git a/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs index 755efa3eaf..a6a5953827 100644 --- a/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs +++ b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs @@ -1,6 +1,3 @@ -// 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 System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; diff --git a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs index ba84c37e4a..fe9ef667b0 100644 --- a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs +++ b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Collections.Specialized; using System.Reactive.Linq; using Avalonia.Reactive; diff --git a/src/Avalonia.Base/Collections/Pooled/ClearMode.cs b/src/Avalonia.Base/Collections/Pooled/ClearMode.cs new file mode 100644 index 0000000000..d78ac8feab --- /dev/null +++ b/src/Avalonia.Base/Collections/Pooled/ClearMode.cs @@ -0,0 +1,40 @@ +// This source file is adapted from the Collections.Pooled. +// (https://github.com/jtmueller/Collections.Pooled/tree/master/Collections.Pooled/) + +namespace Avalonia.Collections.Pooled +{ + /// + /// This enum allows control over how data is treated when internal + /// arrays are returned to the ArrayPool. Be careful to understand + /// what each option does before using anything other than the default + /// of Auto. + /// + public enum ClearMode + { + /// + /// Auto has different behavior depending on the host project's target framework. + /// .NET Core 2.1: Reference types and value types that contain reference types are cleared + /// when the internal arrays are returned to the pool. Value types that do not contain reference + /// types are not cleared when returned to the pool. + /// .NET Standard 2.0: All user types are cleared before returning to the pool, in case they + /// contain reference types. + /// For .NET Standard, Auto and Always have the same behavior. + /// + Auto = 0, + /// + /// The Always setting has the effect of always clearing user types before returning to the pool. + /// This is the default behavior on .NET Standard.You might want to turn this on in a .NET Core project + /// if you were concerned about sensitive data stored in value types leaking to other pars of your application. + /// + Always = 1, + /// + /// Never will cause pooled collections to never clear user types before returning them to the pool. + /// You might want to use this setting in a .NET Standard project when you know that a particular collection stores + /// only value types and you want the performance benefit of not taking time to reset array items to their default value. + /// Be careful with this setting: if used for a collection that contains reference types, or value types that contain + /// reference types, this setting could cause memory issues by making the garbage collector unable to clean up instances + /// that are still being referenced by arrays sitting in the ArrayPool. + /// + Never = 2 + } +} diff --git a/src/Avalonia.Base/Collections/Pooled/ICollectionDebugView.cs b/src/Avalonia.Base/Collections/Pooled/ICollectionDebugView.cs new file mode 100644 index 0000000000..2b15388a13 --- /dev/null +++ b/src/Avalonia.Base/Collections/Pooled/ICollectionDebugView.cs @@ -0,0 +1,31 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Avalonia.Collections.Pooled +{ + internal sealed class ICollectionDebugView + { + private readonly ICollection _collection; + + public ICollectionDebugView(ICollection collection) + { + _collection = collection ?? throw new ArgumentNullException(nameof(collection)); + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public T[] Items + { + get + { + T[] items = new T[_collection.Count]; + _collection.CopyTo(items, 0); + return items; + } + } + } +} diff --git a/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs b/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs new file mode 100644 index 0000000000..9bc3609dc5 --- /dev/null +++ b/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs @@ -0,0 +1,21 @@ +// This source file is adapted from the Collections.Pooled. +// (https://github.com/jtmueller/Collections.Pooled/tree/master/Collections.Pooled/) + +using System; +using System.Collections.Generic; + +namespace Avalonia.Collections.Pooled +{ + /// + /// Represents a read-only collection of pooled elements that can be accessed by index + /// + /// The type of elements in the read-only pooled list. + + public interface IReadOnlyPooledList : IReadOnlyList + { + /// + /// Gets a for the items currently in the collection. + /// + ReadOnlySpan Span { get; } + } +} diff --git a/src/Avalonia.Base/Collections/Pooled/PooledList.cs b/src/Avalonia.Base/Collections/Pooled/PooledList.cs new file mode 100644 index 0000000000..f0d6b292cc --- /dev/null +++ b/src/Avalonia.Base/Collections/Pooled/PooledList.cs @@ -0,0 +1,1531 @@ +// 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. + +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Threading; + +namespace Avalonia.Collections.Pooled +{ + /// + /// Implements a variable-size list that uses a pooled array to store the + /// elements. A PooledList has a capacity, which is the allocated length + /// of the internal array. As elements are added to a PooledList, the capacity + /// of the PooledList is automatically increased as required by reallocating the + /// internal array. + /// + /// + /// This class is based on the code for but it supports + /// and uses when allocating internal arrays. + /// + [DebuggerDisplay("Count = {Count}")] + [DebuggerTypeProxy(typeof(ICollectionDebugView<>))] + [Serializable] + public class PooledList : IList, IReadOnlyPooledList, IList, IDisposable, IDeserializationCallback + { + // internal constant copied from Array.MaxArrayLength + private const int MaxArrayLength = 0x7FEFFFFF; + private const int DefaultCapacity = 4; + private static readonly T[] s_emptyArray = Array.Empty(); + + [NonSerialized] + private ArrayPool _pool; + [NonSerialized] + private object _syncRoot; + + private T[] _items; // Do not rename (binary serialization) + private int _size; // Do not rename (binary serialization) + private int _version; // Do not rename (binary serialization) + private readonly bool _clearOnFree; + + #region Constructors + + /// + /// Constructs a PooledList. The list is initially empty and has a capacity + /// of zero. Upon adding the first element to the list the capacity is + /// increased to DefaultCapacity, and then increased in multiples of two + /// as required. + /// + public PooledList() : this(ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList. The list is initially empty and has a capacity + /// of zero. Upon adding the first element to the list the capacity is + /// increased to DefaultCapacity, and then increased in multiples of two + /// as required. + /// + public PooledList(ClearMode clearMode) : this(clearMode, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList. The list is initially empty and has a capacity + /// of zero. Upon adding the first element to the list the capacity is + /// increased to DefaultCapacity, and then increased in multiples of two + /// as required. + /// + public PooledList(ArrayPool customPool) : this(ClearMode.Auto, customPool) { } + + /// + /// Constructs a PooledList. The list is initially empty and has a capacity + /// of zero. Upon adding the first element to the list the capacity is + /// increased to DefaultCapacity, and then increased in multiples of two + /// as required. + /// + public PooledList(ClearMode clearMode, ArrayPool customPool) + { + _items = s_emptyArray; + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity) : this(capacity, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, bool sizeToCapacity) : this(capacity, ClearMode.Auto, ArrayPool.Shared, sizeToCapacity) { } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, ClearMode clearMode) : this(capacity, clearMode, ArrayPool.Shared) { } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, ClearMode clearMode, bool sizeToCapacity) : this(capacity, clearMode, ArrayPool.Shared, sizeToCapacity) { } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, ArrayPool customPool) : this(capacity, ClearMode.Auto, customPool) { } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, ArrayPool customPool, bool sizeToCapacity) : this(capacity, ClearMode.Auto, customPool, sizeToCapacity) { } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, ClearMode clearMode, ArrayPool customPool) : this(capacity, clearMode, customPool, false) { } + + /// + /// Constructs a List with a given initial capacity. The list is + /// 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) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + + if (capacity == 0) + { + _items = s_emptyArray; + } + else + { + _items = _pool.Rent(capacity); + } + + if (sizeToCapacity) + { + _size = capacity; + if (clearMode != ClearMode.Never) + { + Array.Clear(_items, 0, _size); + } + } + } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(T[] array) : this(array.AsSpan(), ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(T[] array, ClearMode clearMode) : this(array.AsSpan(), clearMode, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(T[] array, ArrayPool customPool) : this(array.AsSpan(), ClearMode.Auto, customPool) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(T[] array, ClearMode clearMode, ArrayPool customPool) : this(array.AsSpan(), clearMode, customPool) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(ReadOnlySpan span) : this(span, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(ReadOnlySpan span, ClearMode clearMode) : this(span, clearMode, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(ReadOnlySpan span, ArrayPool customPool) : this(span, ClearMode.Auto, customPool) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(ReadOnlySpan span, ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + + int count = span.Length; + if (count == 0) + { + _items = s_emptyArray; + } + else + { + _items = _pool.Rent(count); + span.CopyTo(_items); + _size = count; + } + } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(IEnumerable collection) : this(collection, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(IEnumerable collection, ClearMode clearMode) : this(collection, clearMode, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(IEnumerable collection, ArrayPool customPool) : this(collection, ClearMode.Auto, customPool) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(IEnumerable collection, ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + + switch (collection) + { + case null: + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection); + break; + + case ICollection c: + int count = c.Count; + if (count == 0) + { + _items = s_emptyArray; + } + else + { + _items = _pool.Rent(count); + c.CopyTo(_items, 0); + _size = count; + } + break; + + default: + _size = 0; + _items = s_emptyArray; + using (var en = collection.GetEnumerator()) + { + while (en.MoveNext()) + Add(en.Current); + } + break; + } + } + + #endregion + + /// + /// Gets a for the items currently in the collection. + /// + public Span Span => _items.AsSpan(0, _size); + + /// + ReadOnlySpan IReadOnlyPooledList.Span => Span; + + /// + /// Gets and sets the capacity of this list. The capacity is the size of + /// the internal array used to hold items. When set, the internal + /// Memory of the list is reallocated to the given capacity. + /// Note that the return value for this property may be larger than the property was set to. + /// + public int Capacity + { + get => _items.Length; + set + { + if (value < _size) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_SmallCapacity); + } + + if (value != _items.Length) + { + if (value > 0) + { + var newItems = _pool.Rent(value); + if (_size > 0) + { + Array.Copy(_items, newItems, _size); + } + ReturnArray(); + _items = newItems; + } + else + { + ReturnArray(); + _size = 0; + } + } + } + } + + /// + /// Read-only property describing how many elements are in the List. + /// + public int Count => _size; + + /// + /// Returns the ClearMode behavior for the collection, denoting whether values are + /// cleared from internal arrays before returning them to the pool. + /// + public ClearMode ClearMode => _clearOnFree ? ClearMode.Always : ClearMode.Never; + + bool IList.IsFixedSize => false; + + bool ICollection.IsReadOnly => false; + + bool IList.IsReadOnly => false; + + int ICollection.Count => _size; + + bool ICollection.IsSynchronized => false; + + // Synchronization root for this object. + object ICollection.SyncRoot + { + get + { + if (_syncRoot == null) + { + Interlocked.CompareExchange(ref _syncRoot, new object(), null); + } + return _syncRoot; + } + } + + /// + /// Gets or sets the element at the given index. + /// + public T this[int index] + { + get + { + // Following trick can reduce the range check by one + if ((uint)index >= (uint)_size) + { + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + } + return _items[index]; + } + + set + { + if ((uint)index >= (uint)_size) + { + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + } + _items[index] = value; + _version++; + } + } + + private static bool IsCompatibleObject(object value) + { + // Non-null values are fine. Only accept nulls if T is a class or Nullable. + // Note that default(T) is not equal to null for value types except when T is Nullable. + return ((value is T) || (value == null && default(T) == null)); + } + + object IList.this[int index] + { + get + { + return this[index]; + } + set + { + ThrowHelper.IfNullAndNullsAreIllegalThenThrow(value, ExceptionArgument.value); + + try + { + this[index] = (T)value; + } + catch (InvalidCastException) + { + ThrowHelper.ThrowWrongValueTypeArgumentException(value, typeof(T)); + } + } + } + + /// + /// Adds the given object to the end of this list. The size of the list is + /// increased by one. If required, the capacity of the list is doubled + /// before adding the new element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(T item) + { + _version++; + int size = _size; + if ((uint)size < (uint)_items.Length) + { + _size = size + 1; + _items[size] = item; + } + else + { + AddWithResize(item); + } + } + + // Non-inline from List.Add to improve its code quality as uncommon path + [MethodImpl(MethodImplOptions.NoInlining)] + private void AddWithResize(T item) + { + int size = _size; + EnsureCapacity(size + 1); + _size = size + 1; + _items[size] = item; + } + + int IList.Add(object item) + { + ThrowHelper.IfNullAndNullsAreIllegalThenThrow(item, ExceptionArgument.item); + + try + { + Add((T)item); + } + catch (InvalidCastException) + { + ThrowHelper.ThrowWrongValueTypeArgumentException(item, typeof(T)); + } + + return Count - 1; + } + + /// + /// Adds the elements of the given collection 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. + /// + public void AddRange(IEnumerable collection) + => InsertRange(_size, collection); + + /// + /// Adds the elements of the given array 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. + /// + public void AddRange(T[] array) + => AddRange(array.AsSpan()); + + /// + /// 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. + /// + public void AddRange(ReadOnlySpan span) + { + var newSpan = InsertSpan(_size, span.Length, false); + span.CopyTo(newSpan); + } + + /// + /// Advances the by the number of items specified, + /// increasing the capacity if required, then returns a Span representing + /// the set of items to be added, allowing direct writes to that section + /// of the collection. + /// + /// The number of items to add. + public Span AddSpan(int count) + => InsertSpan(_size, count); + + public ReadOnlyCollection AsReadOnly() + => new ReadOnlyCollection(this); + + /// + /// Searches a section of the list for a given element using a binary search + /// algorithm. + /// + /// + /// Elements of the list are compared to the search value using + /// the given IComparer interface. If comparer is null, elements of + /// the list are compared to the search value using the IComparable + /// interface, which in that case must be implemented by all elements of the + /// list and the given search value. This method assumes that the given + /// section of the list is already sorted; if this is not the case, the + /// result will be incorrect. + /// + /// The method returns the index of the given value in the list. If the + /// list does not contain the given value, the method returns a negative + /// integer. The bitwise complement operator (~) can be applied to a + /// negative result to produce the index of the first element (if any) that + /// is larger than the given search value. This is also the index at which + /// the search value should be inserted into the list in order for the list + /// to remain sorted. + /// + public int BinarySearch(int index, int count, T item, IComparer comparer) + { + if (index < 0) + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + if (count < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + if (_size - index < count) + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + + return Array.BinarySearch(_items, index, count, item, comparer); + } + + /// + /// Searches the list for a given element using a binary search + /// algorithm. If the item implements + /// then that is used for comparison, otherwise is used. + /// + public int BinarySearch(T item) + => BinarySearch(0, Count, item, null); + + /// + /// Searches the list for a given element using a binary search + /// algorithm. If the item implements + /// then that is used for comparison, otherwise is used. + /// + public int BinarySearch(T item, IComparer comparer) + => BinarySearch(0, Count, item, comparer); + + /// + /// Clears the contents of the PooledList. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() + { + _version++; + int size = _size; + _size = 0; + + if (size > 0 && _clearOnFree) + { + // Clear the elements so that the gc can reclaim the references. + Array.Clear(_items, 0, _size); + } + } + + /// + /// Contains returns true if the specified element is in the List. + /// It does a linear, O(n) search. Equality is determined by calling + /// EqualityComparer{T}.Default.Equals. + /// + public bool Contains(T item) + { + // PERF: IndexOf calls Array.IndexOf, which internally + // calls EqualityComparer.Default.IndexOf, which + // is specialized for different types. This + // boosts performance since instead of making a + // virtual method call each iteration of the loop, + // via EqualityComparer.Default.Equals, we + // only make one virtual call to EqualityComparer.IndexOf. + + return _size != 0 && IndexOf(item) != -1; + } + + bool IList.Contains(object item) + { + if (IsCompatibleObject(item)) + { + return Contains((T)item); + } + return false; + } + + public PooledList ConvertAll(Func converter) + { + if (converter == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.converter); + } + + var list = new PooledList(_size); + for (int i = 0; i < _size; i++) + { + list._items[i] = converter(_items[i]); + } + list._size = _size; + return list; + } + + /// + /// Copies this list to the given span. + /// + public void CopyTo(Span span) + { + if (span.Length < Count) + throw new ArgumentException("Destination span is shorter than the list to be copied."); + + Span.CopyTo(span); + } + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + Array.Copy(_items, 0, array, arrayIndex, _size); + } + + // Copies this List into array, which must be of a + // compatible array type. + void ICollection.CopyTo(Array array, int arrayIndex) + { + if ((array != null) && (array.Rank != 1)) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_RankMultiDimNotSupported); + } + + try + { + // Array.Copy will check for NULL. + Array.Copy(_items, 0, array, arrayIndex, _size); + } + catch (ArrayTypeMismatchException) + { + ThrowHelper.ThrowArgumentException_Argument_InvalidArrayType(); + } + } + + /// + /// Ensures that the capacity of this list is at least the given minimum + /// value. If the current capacity of the list is less than min, the + /// capacity is increased to twice the current capacity or to min, + /// whichever is larger. + /// + private void EnsureCapacity(int min) + { + if (_items.Length < min) + { + int newCapacity = _items.Length == 0 ? DefaultCapacity : _items.Length * 2; + // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow. + // Note that this check works even when _items.Length overflowed thanks to the (uint) cast + if ((uint)newCapacity > MaxArrayLength) + newCapacity = MaxArrayLength; + if (newCapacity < min) + newCapacity = min; + Capacity = newCapacity; + } + } + + public bool Exists(Func match) + => FindIndex(match) != -1; + + public bool TryFind(Func match, out T result) + { + if (match == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + + for (int i = 0; i < _size; i++) + { + if (match(_items[i])) + { + result = _items[i]; + return true; + } + } + + result = default; + return false; + } + + public PooledList FindAll(Func match) + { + if (match == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + + var list = new PooledList(); + for (int i = 0; i < _size; i++) + { + if (match(_items[i])) + { + list.Add(_items[i]); + } + } + return list; + } + + public int FindIndex(Func match) + => FindIndex(0, _size, match); + + public int FindIndex(int startIndex, Func match) + => FindIndex(startIndex, _size - startIndex, match); + + public int FindIndex(int startIndex, int count, Func match) + { + if ((uint)startIndex > (uint)_size) + ThrowHelper.ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index(); + + if (count < 0 || startIndex > _size - count) + ThrowHelper.ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count(); + + if (match is null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + + int endIndex = startIndex + count; + for (int i = startIndex; i < endIndex; i++) + { + if (match(_items[i])) + return i; + } + return -1; + } + + public bool TryFindLast(Func match, out T result) + { + if (match is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + } + + for (int i = _size - 1; i >= 0; i--) + { + if (match(_items[i])) + { + result = _items[i]; + return true; + } + } + + result = default; + return false; + } + + public int FindLastIndex(Func match) + => FindLastIndex(_size - 1, _size, match); + + public int FindLastIndex(int startIndex, Func match) + => FindLastIndex(startIndex, startIndex + 1, match); + + public int FindLastIndex(int startIndex, int count, Func match) + { + if (match == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + } + + if (_size == 0) + { + // Special case for 0 length List + if (startIndex != -1) + { + ThrowHelper.ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index(); + } + } + else + { + // Make sure we're not out of range + if ((uint)startIndex >= (uint)_size) + { + ThrowHelper.ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index(); + } + } + + // 2nd half of this also catches when startIndex == MAXINT, so MAXINT - 0 + 1 == -1, which is < 0. + if (count < 0 || startIndex - count + 1 < 0) + { + ThrowHelper.ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count(); + } + + int endIndex = startIndex - count; + for (int i = startIndex; i > endIndex; i--) + { + if (match(_items[i])) + { + return i; + } + } + return -1; + } + + public void ForEach(Action action) + { + if (action == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.action); + } + + int version = _version; + for (int i = 0; i < _size; i++) + { + if (version != _version) + { + break; + } + action(_items[i]); + } + + if (version != _version) + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + } + + /// + /// Returns an enumerator for this list with the given + /// permission for removal of elements. If modifications made to the list + /// while an enumeration is in progress, the MoveNext and + /// GetObject methods of the enumerator will throw an exception. + /// + public Enumerator GetEnumerator() + => new Enumerator(this); + + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(this); + + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(this); + + /// + /// Equivalent to PooledList.Span.Slice(index, count). + /// + public Span GetRange(int index, int count) + { + if (index < 0) + { + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + } + + if (count < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + if (_size - index < count) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + } + + return Span.Slice(index, count); + } + + /// + /// Returns the index of the first occurrence of a given value in + /// this list. The list is searched forwards from beginning to end. + /// + public int IndexOf(T item) + => Array.IndexOf(_items, item, 0, _size); + + int IList.IndexOf(object item) + { + if (IsCompatibleObject(item)) + { + return IndexOf((T)item); + } + return -1; + } + + /// + /// Returns the index of the first occurrence of a given value in a range of + /// this list. The list is searched forwards, starting at index + /// index and ending at count number of elements. + /// + public int IndexOf(T item, int index) + { + if (index > _size) + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + return Array.IndexOf(_items, item, index, _size - index); + } + + /// + /// Returns the index of the first occurrence of a given value in a range of + /// this list. The list is searched forwards, starting at index + /// index and upto count number of elements. + /// + public int IndexOf(T item, int index, int count) + { + if (index > _size) + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + + if (count < 0 || index > _size - count) + ThrowHelper.ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count(); + + return Array.IndexOf(_items, item, index, count); + } + + /// + /// Inserts an element into this list at a given index. The size of the list + /// is increased by one. If required, the capacity of the list is doubled + /// before inserting the new element. + /// + public void Insert(int index, T item) + { + // Note that insertions at the end are legal. + if ((uint)index > (uint)_size) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_ListInsert); + } + + if (_size == _items.Length) + EnsureCapacity(_size + 1); + if (index < _size) + { + Array.Copy(_items, index, _items, index + 1, _size - index); + } + _items[index] = item; + _size++; + _version++; + } + + void IList.Insert(int index, object item) + { + ThrowHelper.IfNullAndNullsAreIllegalThenThrow(item, ExceptionArgument.item); + + try + { + Insert(index, (T)item); + } + catch (InvalidCastException) + { + ThrowHelper.ThrowWrongValueTypeArgumentException(item, typeof(T)); + } + } + + /// + /// Inserts the elements of the given collection at a given index. If + /// required, the capacity of the list is increased to twice the previous + /// capacity or the new size, whichever is larger. Ranges may be added + /// to the end of the list by setting index to the List's size. + /// + public void InsertRange(int index, IEnumerable collection) + { + if ((uint)index > (uint)_size) + { + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + } + + switch (collection) + { + case null: + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection); + break; + + case ICollection c: + int count = c.Count; + if (count > 0) + { + EnsureCapacity(_size + count); + if (index < _size) + { + Array.Copy(_items, index, _items, index + count, _size - index); + } + + // If we're inserting a List into itself, we want to be able to deal with that. + if (this == c) + { + // Copy first part of _items to insert location + Array.Copy(_items, 0, _items, index, index); + // Copy last part of _items back to inserted location + Array.Copy(_items, index + count, _items, index * 2, _size - index); + } + else + { + c.CopyTo(_items, index); + } + _size += count; + } + break; + + default: + using (var en = collection.GetEnumerator()) + { + while (en.MoveNext()) + { + Insert(index++, en.Current); + } + } + break; + } + + _version++; + } + + /// + /// Inserts the elements of the given collection at a given index. If + /// required, the capacity of the list is increased to twice the previous + /// capacity or the new size, whichever is larger. Ranges may be added + /// to the end of the list by setting index to the List's size. + /// + public void InsertRange(int index, ReadOnlySpan span) + { + var newSpan = InsertSpan(index, span.Length, false); + span.CopyTo(newSpan); + } + + /// + /// Inserts the elements of the given collection at a given index. If + /// required, the capacity of the list is increased to twice the previous + /// capacity or the new size, whichever is larger. Ranges may be added + /// to the end of the list by setting index to the List's size. + /// + public void InsertRange(int index, T[] array) + { + if (array is null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); + InsertRange(index, array.AsSpan()); + } + + /// + /// Advances the by the number of items specified, + /// increasing the capacity if required, then returns a Span representing + /// the set of items to be added, allowing direct writes to that section + /// of the collection. + /// + public Span InsertSpan(int index, int count) + => InsertSpan(index, count, true); + + private Span InsertSpan(int index, int count, bool clearOutput) + { + EnsureCapacity(_size + count); + + if (index < _size) + { + Array.Copy(_items, index, _items, index + count, _size - index); + } + + _size += count; + _version++; + + var output = _items.AsSpan(index, count); + + if (clearOutput && _clearOnFree) + { + output.Clear(); + } + + return output; + } + + /// + /// Returns the index of the last occurrence of a given value in a range of + /// this list. The list is searched backwards, starting at the end + /// and ending at the first element in the list. + /// + public int LastIndexOf(T item) + { + if (_size == 0) + { // Special case for empty list + return -1; + } + else + { + return LastIndexOf(item, _size - 1, _size); + } + } + + /// + /// Returns the index of the last occurrence of a given value in a range of + /// this list. The list is searched backwards, starting at index + /// index and ending at the first element in the list. + /// + public int LastIndexOf(T item, int index) + { + if (index >= _size) + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + return LastIndexOf(item, index, index + 1); + } + + /// + /// Returns the index of the last occurrence of a given value in a range of + /// this list. The list is searched backwards, starting at index + /// index and upto count elements + /// + public int LastIndexOf(T item, int index, int count) + { + if (Count != 0 && index < 0) + { + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + } + + if (Count != 0 && count < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + if (_size == 0) + { + // Special case for empty list + return -1; + } + + if (index >= _size) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_BiggerThanCollection); + } + + if (count > index + 1) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_BiggerThanCollection); + } + + return Array.LastIndexOf(_items, item, index, count); + } + + // Removes the element at the given index. The size of the list is + // decreased by one. + public bool Remove(T item) + { + int index = IndexOf(item); + if (index >= 0) + { + RemoveAt(index); + return true; + } + + return false; + } + + void IList.Remove(object item) + { + if (IsCompatibleObject(item)) + { + Remove((T)item); + } + } + + /// + /// This method removes all items which match the predicate. + /// The complexity is O(n). + /// + public int RemoveAll(Func match) + { + if (match == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + + int freeIndex = 0; // the first free slot in items array + + // Find the first item which needs to be removed. + while (freeIndex < _size && !match(_items[freeIndex])) + freeIndex++; + if (freeIndex >= _size) + return 0; + + int current = freeIndex + 1; + while (current < _size) + { + // Find the first item which needs to be kept. + while (current < _size && match(_items[current])) + current++; + + if (current < _size) + { + // copy item to the free slot. + _items[freeIndex++] = _items[current++]; + } + } + + if (_clearOnFree) + { + // Clear the removed elements so that the gc can reclaim the references. + Array.Clear(_items, freeIndex, _size - freeIndex); + } + + int result = _size - freeIndex; + _size = freeIndex; + _version++; + return result; + } + + /// + /// Removes the element at the given index. The size of the list is + /// decreased by one. + /// + public void RemoveAt(int index) + { + if ((uint)index >= (uint)_size) + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + + _size--; + if (index < _size) + { + Array.Copy(_items, index + 1, _items, index, _size - index); + } + _version++; + + if (_clearOnFree) + { + // Clear the removed element so that the gc can reclaim the reference. + _items[_size] = default; + } + } + + /// + /// Removes a range of elements from this list. + /// + public void RemoveRange(int index, int count) + { + if (index < 0) + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + + if (count < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + + if (_size - index < count) + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + + if (count > 0) + { + _size -= count; + if (index < _size) + { + Array.Copy(_items, index + count, _items, index, _size - index); + } + + _version++; + + if (_clearOnFree) + { + // Clear the removed elements so that the gc can reclaim the references. + Array.Clear(_items, _size, count); + } + } + } + + /// + /// Reverses the elements in this list. + /// + public void Reverse() + => Reverse(0, _size); + + /// + /// Reverses the elements in a range of this list. Following a call to this + /// method, an element in the range given by index and count + /// which was previously located at index i will now be located at + /// index index + (index + count - i - 1). + /// + public void Reverse(int index, int count) + { + if (index < 0) + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + + if (count < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + + if (_size - index < count) + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + + if (count > 1) + { + Array.Reverse(_items, index, count); + } + _version++; + } + + /// + /// Sorts the elements in this list. Uses the default comparer and + /// Array.Sort. + /// + public void Sort() + => Sort(0, Count, null); + + /// + /// Sorts the elements in this list. Uses Array.Sort with the + /// provided comparer. + /// + /// + public void Sort(IComparer comparer) + => Sort(0, Count, comparer); + + /// + /// Sorts the elements in a section of this list. The sort compares the + /// elements to each other using the given IComparer interface. If + /// comparer is null, the elements are compared to each other using + /// the IComparable interface, which in that case must be implemented by all + /// elements of the list. + /// + /// This method uses the Array.Sort method to sort the elements. + /// + public void Sort(int index, int count, IComparer comparer) + { + if (index < 0) + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + + if (count < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + + if (_size - index < count) + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + + if (count > 1) + { + Array.Sort(_items, index, count, comparer); + } + _version++; + } + + public void Sort(Func comparison) + { + if (comparison == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.comparison); + } + + if (_size > 1) + { + // List uses ArraySortHelper here but since it's an internal class, + // we're creating an IComparer using the comparison function to avoid + // duplicating all that code. + Array.Sort(_items, 0, _size, new Comparer(comparison)); + } + _version++; + } + + /// + /// ToArray returns an array containing the contents of the List. + /// This requires copying the List, which is an O(n) operation. + /// + public T[] ToArray() + { + if (_size == 0) + { + return s_emptyArray; + } + + return Span.ToArray(); + } + + /// + /// Sets the capacity of this list to the size of the list. This method can + /// be used to minimize a list's memory overhead once it is known that no + /// new elements will be added to the list. To completely clear a list and + /// release all memory referenced by the list, execute the following + /// statements: + /// + /// list.Clear(); + /// list.TrimExcess(); + /// + /// + public void TrimExcess() + { + int threshold = (int)(_items.Length * 0.9); + if (_size < threshold) + { + Capacity = _size; + } + } + + public bool TrueForAll(Func match) + { + if (match == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + } + + for (int i = 0; i < _size; i++) + { + if (!match(_items[i])) + { + return false; + } + } + return true; + } + + private void ReturnArray() + { + if (_items.Length == 0) + return; + + try + { + // Clear the elements so that the gc can reclaim the references. + _pool.Return(_items, clearArray: _clearOnFree); + } + catch (ArgumentException) + { + // oh well, the array pool didn't like our array + } + + _items = s_emptyArray; + } + + private static bool ShouldClear(ClearMode mode) + { +#if NETCOREAPP2_1 + return mode == ClearMode.Always + || (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences()); +#else + return mode != ClearMode.Never; +#endif + } + + /// + /// Returns the internal buffers to the ArrayPool. + /// + public void Dispose() + { + ReturnArray(); + _size = 0; + _version++; + } + + void IDeserializationCallback.OnDeserialization(object sender) + { + // We can't serialize array pools, so deserialized PooledLists will + // have to use the shared pool, even if they were using a custom pool + // before serialization. + _pool = ArrayPool.Shared; + } + + public struct Enumerator : IEnumerator, IEnumerator + { + private readonly PooledList _list; + private int _index; + private readonly int _version; + private T _current; + + internal Enumerator(PooledList list) + { + _list = list; + _index = 0; + _version = list._version; + _current = default; + } + + public void Dispose() + { + } + + public bool MoveNext() + { + var localList = _list; + + if (_version == localList._version && ((uint)_index < (uint)localList._size)) + { + _current = localList._items[_index]; + _index++; + return true; + } + return MoveNextRare(); + } + + private bool MoveNextRare() + { + if (_version != _list._version) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + } + + _index = _list._size + 1; + _current = default; + return false; + } + + public T Current => _current; + + object IEnumerator.Current + { + get + { + if (_index == 0 || _index == _list._size + 1) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); + } + return Current; + } + } + + void IEnumerator.Reset() + { + if (_version != _list._version) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + } + + _index = 0; + _current = default; + } + } + + private readonly struct Comparer : IComparer + { + private readonly Func _comparison; + + public Comparer(Func comparison) + { + _comparison = comparison; + } + + public int Compare(T x, T y) => _comparison(x, y); + } + } +} diff --git a/src/Avalonia.Base/Collections/Pooled/PooledStack.cs b/src/Avalonia.Base/Collections/Pooled/PooledStack.cs new file mode 100644 index 0000000000..104a7f97e9 --- /dev/null +++ b/src/Avalonia.Base/Collections/Pooled/PooledStack.cs @@ -0,0 +1,699 @@ +// 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. + +/*============================================================================= +** +** +** Purpose: An array implementation of a generic stack. +** +** +=============================================================================*/ + +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Threading; + +namespace Avalonia.Collections.Pooled +{ + /// + /// A simple stack of objects. Internally it is implemented as an array, + /// so Push can be O(n). Pop is O(1). + /// + [DebuggerTypeProxy(typeof(StackDebugView<>))] + [DebuggerDisplay("Count = {Count}")] + [Serializable] + public class PooledStack : IEnumerable, ICollection, IReadOnlyCollection, IDisposable, IDeserializationCallback + { + [NonSerialized] + private ArrayPool _pool; + [NonSerialized] + private object _syncRoot; + + private T[] _array; // Storage for stack elements. Do not rename (binary serialization) + private int _size; // Number of items in the stack. Do not rename (binary serialization) + private int _version; // Used to keep enumerator in sync w/ collection. Do not rename (binary serialization) + private readonly bool _clearOnFree; + + private const int DefaultCapacity = 4; + + #region Constructors + + /// + /// Create a stack with the default initial capacity. + /// + public PooledStack() : this(ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Create a stack with the default initial capacity. + /// + public PooledStack(ClearMode clearMode) : this(clearMode, ArrayPool.Shared) { } + + /// + /// Create a stack with the default initial capacity. + /// + public PooledStack(ArrayPool customPool) : this(ClearMode.Auto, customPool) { } + + /// + /// Create a stack with the default initial capacity and a custom ArrayPool. + /// + public PooledStack(ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _array = Array.Empty(); + _clearOnFree = ShouldClear(clearMode); + } + + /// + /// Create a stack with a specific initial capacity. The initial capacity + /// must be a non-negative number. + /// + public PooledStack(int capacity) : this(capacity, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Create a stack with a specific initial capacity. The initial capacity + /// must be a non-negative number. + /// + public PooledStack(int capacity, ClearMode clearMode) : this(capacity, clearMode, ArrayPool.Shared) { } + + /// + /// Create a stack with a specific initial capacity. The initial capacity + /// must be a non-negative number. + /// + public PooledStack(int capacity, ArrayPool customPool) : this(capacity, ClearMode.Auto, customPool) { } + + /// + /// Create a stack with a specific initial capacity. The initial capacity + /// must be a non-negative number. + /// + public PooledStack(int capacity, ClearMode clearMode, ArrayPool customPool) + { + if (capacity < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + _pool = customPool ?? ArrayPool.Shared; + _array = _pool.Rent(capacity); + _clearOnFree = ShouldClear(clearMode); + } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(IEnumerable enumerable) : this(enumerable, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(IEnumerable enumerable, ClearMode clearMode) : this(enumerable, clearMode, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(IEnumerable enumerable, ArrayPool customPool) : this(enumerable, ClearMode.Auto, customPool) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(IEnumerable enumerable, ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + + switch (enumerable) + { + case null: + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.enumerable); + break; + + case ICollection collection: + if (collection.Count == 0) + { + _array = Array.Empty(); + } + else + { + _array = _pool.Rent(collection.Count); + collection.CopyTo(_array, 0); + _size = collection.Count; + } + break; + + default: + using (var list = new PooledList(enumerable)) + { + _array = _pool.Rent(list.Count); + list.Span.CopyTo(_array); + _size = list.Count; + } + break; + } + } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(T[] array) : this(array.AsSpan(), ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(T[] array, ClearMode clearMode) : this(array.AsSpan(), clearMode, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(T[] array, ArrayPool customPool) : this(array.AsSpan(), ClearMode.Auto, customPool) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(T[] array, ClearMode clearMode, ArrayPool customPool) : this(array.AsSpan(), clearMode, customPool) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(ReadOnlySpan span) : this(span, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(ReadOnlySpan span, ClearMode clearMode) : this(span, clearMode, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(ReadOnlySpan span, ArrayPool customPool) : this(span, ClearMode.Auto, customPool) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(ReadOnlySpan span, ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + _array = _pool.Rent(span.Length); + span.CopyTo(_array); + _size = span.Length; + } + + #endregion + + /// + /// The number of items in the stack. + /// + public int Count => _size; + + /// + /// Returns the ClearMode behavior for the collection, denoting whether values are + /// cleared from internal arrays before returning them to the pool. + /// + public ClearMode ClearMode => _clearOnFree ? ClearMode.Always : ClearMode.Never; + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot + { + get + { + if (_syncRoot == null) + { + Interlocked.CompareExchange(ref _syncRoot, new object(), null); + } + return _syncRoot; + } + } + + /// + /// Removes all Objects from the Stack. + /// + public void Clear() + { + if (_clearOnFree) + { + Array.Clear(_array, 0, _size); // clear the elements so that the gc can reclaim the references. + } + _size = 0; + _version++; + } + + /// + /// Compares items using the default equality comparer + /// + public bool Contains(T item) + { + // PERF: Internally Array.LastIndexOf calls + // EqualityComparer.Default.LastIndexOf, which + // is specialized for different types. This + // boosts performance since instead of making a + // virtual method call each iteration of the loop, + // via EqualityComparer.Default.Equals, we + // only make one virtual call to EqualityComparer.LastIndexOf. + + return _size != 0 && Array.LastIndexOf(_array, item, _size - 1) != -1; + } + + /// + /// This method removes all items which match the predicate. + /// The complexity is O(n). + /// + public int RemoveWhere(Func match) + { + if (match == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + + int freeIndex = 0; // the first free slot in items array + + // Find the first item which needs to be removed. + while (freeIndex < _size && !match(_array[freeIndex])) + freeIndex++; + if (freeIndex >= _size) + return 0; + + int current = freeIndex + 1; + while (current < _size) + { + // Find the first item which needs to be kept. + while (current < _size && match(_array[current])) + current++; + + if (current < _size) + { + // copy item to the free slot. + _array[freeIndex++] = _array[current++]; + } + } + + if (_clearOnFree) + { + // Clear the removed elements so that the gc can reclaim the references. + Array.Clear(_array, freeIndex, _size - freeIndex); + } + + int result = _size - freeIndex; + _size = freeIndex; + _version++; + return result; + } + + // Copies the stack into an array. + public void CopyTo(T[] array, int arrayIndex) + { + if (array == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); + } + + if (arrayIndex < 0 || arrayIndex > array.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.arrayIndex); + } + + if (array.Length - arrayIndex < _size) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall); + } + + Debug.Assert(array != _array); + int srcIndex = 0; + int dstIndex = arrayIndex + _size; + while (srcIndex < _size) + { + array[--dstIndex] = _array[srcIndex++]; + } + } + + public void CopyTo(Span span) + { + if (span.Length < _size) + { + ThrowHelper.ThrowArgumentException_DestinationTooShort(); + } + + int srcIndex = 0; + int dstIndex = _size; + while (srcIndex < _size) + { + span[--dstIndex] = _array[srcIndex++]; + } + } + + void ICollection.CopyTo(Array array, int arrayIndex) + { + if (array == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); + } + + if (array.Rank != 1) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_RankMultiDimNotSupported); + } + + if (array.GetLowerBound(0) != 0) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_NonZeroLowerBound, ExceptionArgument.array); + } + + if (arrayIndex < 0 || arrayIndex > array.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.arrayIndex); + } + + if (array.Length - arrayIndex < _size) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + } + + try + { + Array.Copy(_array, 0, array, arrayIndex, _size); + Array.Reverse(array, arrayIndex, _size); + } + catch (ArrayTypeMismatchException) + { + ThrowHelper.ThrowArgumentException_Argument_InvalidArrayType(); + } + } + + /// + /// Returns an IEnumerator for this PooledStack. + /// + /// + public Enumerator GetEnumerator() + => new Enumerator(this); + + /// + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(this); + + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(this); + + public void TrimExcess() + { + if (_size == 0) + { + ReturnArray(replaceWith: Array.Empty()); + _version++; + return; + } + + int threshold = (int)(_array.Length * 0.9); + if (_size < threshold) + { + var newArray = _pool.Rent(_size); + if (newArray.Length < _array.Length) + { + Array.Copy(_array, newArray, _size); + ReturnArray(replaceWith: newArray); + _version++; + } + else + { + // The array from the pool wasn't any smaller than the one we already had, + // (we can only control minimum size) so return it and do nothing. + // If we create an exact-sized array not from the pool, we'll + // get an exception when returning it to the pool. + _pool.Return(newArray); + } + } + } + + /// + /// Returns the top object on the stack without removing it. If the stack + /// is empty, Peek throws an InvalidOperationException. + /// + public T Peek() + { + int size = _size - 1; + T[] array = _array; + + if ((uint)size >= (uint)array.Length) + { + ThrowForEmptyStack(); + } + + return array[size]; + } + + public bool TryPeek(out T result) + { + int size = _size - 1; + T[] array = _array; + + if ((uint)size >= (uint)array.Length) + { + result = default; + return false; + } + result = array[size]; + return true; + } + + /// + /// Pops an item from the top of the stack. If the stack is empty, Pop + /// throws an InvalidOperationException. + /// + public T Pop() + { + int size = _size - 1; + T[] array = _array; + + // if (_size == 0) is equivalent to if (size == -1), and this case + // is covered with (uint)size, thus allowing bounds check elimination + // https://github.com/dotnet/coreclr/pull/9773 + if ((uint)size >= (uint)array.Length) + { + ThrowForEmptyStack(); + } + + _version++; + _size = size; + T item = array[size]; + if (_clearOnFree) + { + array[size] = default; // Free memory quicker. + } + return item; + } + + public bool TryPop(out T result) + { + int size = _size - 1; + T[] array = _array; + + if ((uint)size >= (uint)array.Length) + { + result = default; + return false; + } + + _version++; + _size = size; + result = array[size]; + if (_clearOnFree) + { + array[size] = default; // Free memory quicker. + } + return true; + } + + /// + /// Pushes an item to the top of the stack. + /// + public void Push(T item) + { + int size = _size; + T[] array = _array; + + if ((uint)size < (uint)array.Length) + { + array[size] = item; + _version++; + _size = size + 1; + } + else + { + PushWithResize(item); + } + } + + // Non-inline from Stack.Push to improve its code quality as uncommon path + [MethodImpl(MethodImplOptions.NoInlining)] + private void PushWithResize(T item) + { + var newArray = _pool.Rent((_array.Length == 0) ? DefaultCapacity : 2 * _array.Length); + Array.Copy(_array, newArray, _size); + ReturnArray(replaceWith: newArray); + _array[_size] = item; + _version++; + _size++; + } + + /// + /// Copies the Stack to an array, in the same order Pop would return the items. + /// + public T[] ToArray() + { + if (_size == 0) + return Array.Empty(); + + T[] objArray = new T[_size]; + int i = 0; + while (i < _size) + { + objArray[i] = _array[_size - i - 1]; + i++; + } + return objArray; + } + + private void ThrowForEmptyStack() + { + Debug.Assert(_size == 0); + throw new InvalidOperationException("Stack was empty."); + } + + private void ReturnArray(T[] replaceWith = null) + { + if (_array?.Length > 0) + { + try + { + _pool.Return(_array, clearArray: _clearOnFree); + } + catch (ArgumentException) + { + // oh well, the array pool didn't like our array + } + } + + if (!(replaceWith is null)) + { + _array = replaceWith; + } + } + + private static bool ShouldClear(ClearMode mode) + { +#if NETCOREAPP2_1 + return mode == ClearMode.Always + || (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences()); +#else + return mode != ClearMode.Never; +#endif + } + + public void Dispose() + { + ReturnArray(replaceWith: Array.Empty()); + _size = 0; + _version++; + } + + void IDeserializationCallback.OnDeserialization(object sender) + { + // We can't serialize array pools, so deserialized PooledStacks will + // have to use the shared pool, even if they were using a custom pool + // before serialization. + _pool = ArrayPool.Shared; + } + + [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes", Justification = "not an expected scenario")] + public struct Enumerator : IEnumerator, IEnumerator + { + private readonly PooledStack _stack; + private readonly int _version; + private int _index; + private T _currentElement; + + internal Enumerator(PooledStack stack) + { + _stack = stack; + _version = stack._version; + _index = -2; + _currentElement = default; + } + + public void Dispose() + { + _index = -1; + } + + public bool MoveNext() + { + bool retval; + if (_version != _stack._version) + throw new InvalidOperationException("Collection was modified during enumeration."); + if (_index == -2) + { // First call to enumerator. + _index = _stack._size - 1; + retval = (_index >= 0); + if (retval) + _currentElement = _stack._array[_index]; + return retval; + } + if (_index == -1) + { // End of enumeration. + return false; + } + + retval = (--_index >= 0); + if (retval) + _currentElement = _stack._array[_index]; + else + _currentElement = default; + return retval; + } + + public T Current + { + get + { + if (_index < 0) + ThrowEnumerationNotStartedOrEnded(); + return _currentElement; + } + } + + private void ThrowEnumerationNotStartedOrEnded() + { + Debug.Assert(_index == -1 || _index == -2); + throw new InvalidOperationException(_index == -2 ? "Enumeration was not started." : "Enumeration has ended."); + } + + object IEnumerator.Current + { + get { return Current; } + } + + void IEnumerator.Reset() + { + if (_version != _stack._version) + throw new InvalidOperationException("Collection was modified during enumeration."); + _index = -2; + _currentElement = default; + } + } + } +} diff --git a/src/Avalonia.Base/Collections/Pooled/StackDebugView.cs b/src/Avalonia.Base/Collections/Pooled/StackDebugView.cs new file mode 100644 index 0000000000..b042388079 --- /dev/null +++ b/src/Avalonia.Base/Collections/Pooled/StackDebugView.cs @@ -0,0 +1,28 @@ +// 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. + +using System; +using System.Diagnostics; + +namespace Avalonia.Collections.Pooled +{ + internal sealed class StackDebugView + { + private readonly PooledStack _stack; + + public StackDebugView(PooledStack stack) + { + _stack = stack ?? throw new ArgumentNullException(nameof(stack)); + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public T[] Items + { + get + { + return _stack.ToArray(); + } + } + } +} diff --git a/src/Avalonia.Base/Collections/Pooled/ThrowHelper.cs b/src/Avalonia.Base/Collections/Pooled/ThrowHelper.cs new file mode 100644 index 0000000000..74558229c3 --- /dev/null +++ b/src/Avalonia.Base/Collections/Pooled/ThrowHelper.cs @@ -0,0 +1,691 @@ +// 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. + + +// This file defines an internal class used to throw exceptions in BCL code. +// The main purpose is to reduce code size. +// +// The old way to throw an exception generates quite a lot IL code and assembly code. +// Following is an example: +// C# source +// throw new ArgumentNullException(nameof(key), SR.ArgumentNull_Key); +// IL code: +// IL_0003: ldstr "key" +// IL_0008: ldstr "ArgumentNull_Key" +// IL_000d: call string System.Environment::GetResourceString(string) +// IL_0012: newobj instance void System.ArgumentNullException::.ctor(string,string) +// IL_0017: throw +// which is 21bytes in IL. +// +// So we want to get rid of the ldstr and call to Environment.GetResource in IL. +// In order to do that, I created two enums: ExceptionResource, ExceptionArgument to represent the +// argument name and resource name in a small integer. The source code will be changed to +// ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key, ExceptionResource.ArgumentNull_Key); +// +// The IL code will be 7 bytes. +// IL_0008: ldc.i4.4 +// IL_0009: ldc.i4.4 +// IL_000a: call void System.ThrowHelper::ThrowArgumentNullException(valuetype System.ExceptionArgument) +// IL_000f: ldarg.0 +// +// This will also reduce the Jitted code size a lot. +// +// It is very important we do this for generic classes because we can easily generate the same code +// multiple times for different instantiation. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; + +namespace Avalonia.Collections.Pooled +{ + internal static class ThrowHelper + { + internal static void ThrowArrayTypeMismatchException() + { + throw new ArrayTypeMismatchException(); + } + + internal static void ThrowIndexOutOfRangeException() + { + throw new IndexOutOfRangeException(); + } + + internal static void ThrowArgumentOutOfRangeException() + { + throw new ArgumentOutOfRangeException(); + } + + internal static void ThrowArgumentException_DestinationTooShort() + { + throw new ArgumentException("Destination too short."); + } + + internal static void ThrowArgumentException_OverlapAlignmentMismatch() + { + throw new ArgumentException("Overlap alignment mismatch."); + } + + internal static void ThrowArgumentOutOfRange_IndexException() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.index, + ExceptionResource.ArgumentOutOfRange_Index); + } + + internal static void ThrowIndexArgumentOutOfRange_NeedNonNegNumException() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.index, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + internal static void ThrowValueArgumentOutOfRange_NeedNonNegNumException() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.value, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + internal static void ThrowLengthArgumentOutOfRange_ArgumentOutOfRange_NeedNonNegNum() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.length, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + internal static void ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.startIndex, + ExceptionResource.ArgumentOutOfRange_Index); + } + + internal static void ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.count, + ExceptionResource.ArgumentOutOfRange_Count); + } + + internal static void ThrowWrongKeyTypeArgumentException(T key, Type targetType) + { + // Generic key to move the boxing to the right hand side of throw + throw GetWrongKeyTypeArgumentException((object)key, targetType); + } + + internal static void ThrowWrongValueTypeArgumentException(T value, Type targetType) + { + // Generic key to move the boxing to the right hand side of throw + throw GetWrongValueTypeArgumentException((object)value, targetType); + } + + private static ArgumentException GetAddingDuplicateWithKeyArgumentException(object key) + { + return new ArgumentException($"Error adding duplicate with key: {key}."); + } + + internal static void ThrowAddingDuplicateWithKeyArgumentException(T key) + { + // Generic key to move the boxing to the right hand side of throw + throw GetAddingDuplicateWithKeyArgumentException((object)key); + } + + internal static void ThrowKeyNotFoundException(T key) + { + // Generic key to move the boxing to the right hand side of throw + throw GetKeyNotFoundException((object)key); + } + + internal static void ThrowArgumentException(ExceptionResource resource) + { + throw GetArgumentException(resource); + } + + internal static void ThrowArgumentException(ExceptionResource resource, ExceptionArgument argument) + { + throw GetArgumentException(resource, argument); + } + + private static ArgumentNullException GetArgumentNullException(ExceptionArgument argument) + { + return new ArgumentNullException(GetArgumentName(argument)); + } + + internal static void ThrowArgumentNullException(ExceptionArgument argument) + { + throw GetArgumentNullException(argument); + } + + internal static void ThrowArgumentNullException(ExceptionResource resource) + { + throw new ArgumentNullException(GetResourceString(resource)); + } + + internal static void ThrowArgumentNullException(ExceptionArgument argument, ExceptionResource resource) + { + throw new ArgumentNullException(GetArgumentName(argument), GetResourceString(resource)); + } + + internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument) + { + throw new ArgumentOutOfRangeException(GetArgumentName(argument)); + } + + internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource) + { + throw GetArgumentOutOfRangeException(argument, resource); + } + + internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument, int paramNumber, ExceptionResource resource) + { + throw GetArgumentOutOfRangeException(argument, paramNumber, resource); + } + + internal static void ThrowInvalidOperationException(ExceptionResource resource) + { + throw GetInvalidOperationException(resource); + } + + internal static void ThrowInvalidOperationException(ExceptionResource resource, Exception e) + { + throw new InvalidOperationException(GetResourceString(resource), e); + } + + internal static void ThrowSerializationException(ExceptionResource resource) + { + throw new SerializationException(GetResourceString(resource)); + } + + internal static void ThrowSecurityException(ExceptionResource resource) + { + throw new System.Security.SecurityException(GetResourceString(resource)); + } + + internal static void ThrowRankException(ExceptionResource resource) + { + throw new RankException(GetResourceString(resource)); + } + + internal static void ThrowNotSupportedException(ExceptionResource resource) + { + throw new NotSupportedException(GetResourceString(resource)); + } + + internal static void ThrowUnauthorizedAccessException(ExceptionResource resource) + { + throw new UnauthorizedAccessException(GetResourceString(resource)); + } + + internal static void ThrowObjectDisposedException(string objectName, ExceptionResource resource) + { + throw new ObjectDisposedException(objectName, GetResourceString(resource)); + } + + internal static void ThrowObjectDisposedException(ExceptionResource resource) + { + throw new ObjectDisposedException(null, GetResourceString(resource)); + } + + internal static void ThrowNotSupportedException() + { + throw new NotSupportedException(); + } + + internal static void ThrowAggregateException(List exceptions) + { + throw new AggregateException(exceptions); + } + + internal static void ThrowOutOfMemoryException() + { + throw new OutOfMemoryException(); + } + + internal static void ThrowArgumentException_Argument_InvalidArrayType() + { + throw new ArgumentException("Invalid array type."); + } + + internal static void ThrowInvalidOperationException_InvalidOperation_EnumNotStarted() + { + throw new InvalidOperationException("Enumeration has not started."); + } + + internal static void ThrowInvalidOperationException_InvalidOperation_EnumEnded() + { + throw new InvalidOperationException("Enumeration has ended."); + } + + internal static void ThrowInvalidOperationException_EnumCurrent(int index) + { + throw GetInvalidOperationException_EnumCurrent(index); + } + + internal static void ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion() + { + throw new InvalidOperationException("Collection was modified during enumeration."); + } + + internal static void ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen() + { + throw new InvalidOperationException("Invalid enumerator state: enumeration cannot proceed."); + } + + internal static void ThrowInvalidOperationException_InvalidOperation_NoValue() + { + throw new InvalidOperationException("No value provided."); + } + + internal static void ThrowInvalidOperationException_ConcurrentOperationsNotSupported() + { + throw new InvalidOperationException("Concurrent operations are not supported."); + } + + internal static void ThrowInvalidOperationException_HandleIsNotInitialized() + { + throw new InvalidOperationException("Handle is not initialized."); + } + + internal static void ThrowFormatException_BadFormatSpecifier() + { + throw new FormatException("Bad format specifier."); + } + + private static ArgumentException GetArgumentException(ExceptionResource resource) + { + return new ArgumentException(GetResourceString(resource)); + } + + private static InvalidOperationException GetInvalidOperationException(ExceptionResource resource) + { + return new InvalidOperationException(GetResourceString(resource)); + } + + private static ArgumentException GetWrongKeyTypeArgumentException(object key, Type targetType) + { + return new ArgumentException($"Wrong key type. Expected {targetType}, got: '{key}'.", nameof(key)); + } + + private static ArgumentException GetWrongValueTypeArgumentException(object value, Type targetType) + { + return new ArgumentException($"Wrong value type. Expected {targetType}, got: '{value}'.", nameof(value)); + } + + private static KeyNotFoundException GetKeyNotFoundException(object key) + { + return new KeyNotFoundException($"Key not found: {key}"); + } + + private static ArgumentOutOfRangeException GetArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource) + { + return new ArgumentOutOfRangeException(GetArgumentName(argument), GetResourceString(resource)); + } + + private static ArgumentException GetArgumentException(ExceptionResource resource, ExceptionArgument argument) + { + return new ArgumentException(GetResourceString(resource), GetArgumentName(argument)); + } + + private static ArgumentOutOfRangeException GetArgumentOutOfRangeException(ExceptionArgument argument, int paramNumber, ExceptionResource resource) + { + return new ArgumentOutOfRangeException(GetArgumentName(argument) + "[" + paramNumber.ToString() + "]", GetResourceString(resource)); + } + + private static InvalidOperationException GetInvalidOperationException_EnumCurrent(int index) + { + return new InvalidOperationException( + index < 0 ? + "Enumeration has not started" : + "Enumeration has ended"); + } + + // Allow nulls for reference types and Nullable, but not for value types. + // Aggressively inline so the jit evaluates the if in place and either drops the call altogether + // Or just leaves null test and call to the Non-returning ThrowHelper.ThrowArgumentNullException + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void IfNullAndNullsAreIllegalThenThrow(object value, ExceptionArgument argName) + { + // Note that default(T) is not equal to null for value types except when T is Nullable. + if (!(default(T) == null) && value == null) + ThrowHelper.ThrowArgumentNullException(argName); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ThrowForUnsupportedVectorBaseType() where T : struct + { + if (typeof(T) != typeof(byte) && typeof(T) != typeof(sbyte) && + typeof(T) != typeof(short) && typeof(T) != typeof(ushort) && + typeof(T) != typeof(int) && typeof(T) != typeof(uint) && + typeof(T) != typeof(long) && typeof(T) != typeof(ulong) && + typeof(T) != typeof(float) && typeof(T) != typeof(double)) + { + ThrowNotSupportedException(ExceptionResource.Arg_TypeNotSupported); + } + } + +#if false // Reflection-based implementation does not work for CoreRT/ProjectN + // This function will convert an ExceptionArgument enum value to the argument name string. + [MethodImpl(MethodImplOptions.NoInlining)] + private static string GetArgumentName(ExceptionArgument argument) + { + Debug.Assert(Enum.IsDefined(typeof(ExceptionArgument), argument), + "The enum value is not defined, please check the ExceptionArgument Enum."); + + return argument.ToString(); + } +#endif + + private static string GetArgumentName(ExceptionArgument argument) + { + switch (argument) + { + case ExceptionArgument.obj: + return "obj"; + case ExceptionArgument.dictionary: + return "dictionary"; + case ExceptionArgument.array: + return "array"; + case ExceptionArgument.info: + return "info"; + case ExceptionArgument.key: + return "key"; + case ExceptionArgument.text: + return "text"; + case ExceptionArgument.values: + return "values"; + case ExceptionArgument.value: + return "value"; + case ExceptionArgument.startIndex: + return "startIndex"; + case ExceptionArgument.task: + return "task"; + case ExceptionArgument.ch: + return "ch"; + case ExceptionArgument.s: + return "s"; + case ExceptionArgument.input: + return "input"; + case ExceptionArgument.list: + return "list"; + case ExceptionArgument.index: + return "index"; + case ExceptionArgument.capacity: + return "capacity"; + case ExceptionArgument.collection: + return "collection"; + case ExceptionArgument.item: + return "item"; + case ExceptionArgument.converter: + return "converter"; + case ExceptionArgument.match: + return "match"; + case ExceptionArgument.count: + return "count"; + case ExceptionArgument.action: + return "action"; + case ExceptionArgument.comparison: + return "comparison"; + case ExceptionArgument.exceptions: + return "exceptions"; + case ExceptionArgument.exception: + return "exception"; + case ExceptionArgument.enumerable: + return "enumerable"; + case ExceptionArgument.start: + return "start"; + case ExceptionArgument.format: + return "format"; + case ExceptionArgument.culture: + return "culture"; + case ExceptionArgument.comparer: + return "comparer"; + case ExceptionArgument.comparable: + return "comparable"; + case ExceptionArgument.source: + return "source"; + case ExceptionArgument.state: + return "state"; + case ExceptionArgument.length: + return "length"; + case ExceptionArgument.comparisonType: + return "comparisonType"; + case ExceptionArgument.manager: + return "manager"; + case ExceptionArgument.sourceBytesToCopy: + return "sourceBytesToCopy"; + case ExceptionArgument.callBack: + return "callBack"; + case ExceptionArgument.creationOptions: + return "creationOptions"; + case ExceptionArgument.function: + return "function"; + case ExceptionArgument.delay: + return "delay"; + case ExceptionArgument.millisecondsDelay: + return "millisecondsDelay"; + case ExceptionArgument.millisecondsTimeout: + return "millisecondsTimeout"; + case ExceptionArgument.timeout: + return "timeout"; + case ExceptionArgument.type: + return "type"; + case ExceptionArgument.sourceIndex: + return "sourceIndex"; + case ExceptionArgument.sourceArray: + return "sourceArray"; + case ExceptionArgument.destinationIndex: + return "destinationIndex"; + case ExceptionArgument.destinationArray: + return "destinationArray"; + case ExceptionArgument.other: + return "other"; + case ExceptionArgument.newSize: + return "newSize"; + case ExceptionArgument.lowerBounds: + return "lowerBounds"; + case ExceptionArgument.lengths: + return "lengths"; + case ExceptionArgument.len: + return "len"; + case ExceptionArgument.keys: + return "keys"; + case ExceptionArgument.indices: + return "indices"; + case ExceptionArgument.endIndex: + return "endIndex"; + case ExceptionArgument.elementType: + return "elementType"; + case ExceptionArgument.arrayIndex: + return "arrayIndex"; + default: + Debug.Fail("The enum value is not defined, please check the ExceptionArgument Enum."); + return argument.ToString(); + } + } + +#if false // Reflection-based implementation does not work for CoreRT/ProjectN + // This function will convert an ExceptionResource enum value to the resource string. + [MethodImpl(MethodImplOptions.NoInlining)] + private static string GetResourceString(ExceptionResource resource) + { + Debug.Assert(Enum.IsDefined(typeof(ExceptionResource), resource), + "The enum value is not defined, please check the ExceptionResource Enum."); + + return SR.GetResourceString(resource.ToString()); + } +#endif + + private static string GetResourceString(ExceptionResource resource) + { + switch (resource) + { + case ExceptionResource.ArgumentOutOfRange_Index: + return "Argument 'index' was out of the range of valid values."; + case ExceptionResource.ArgumentOutOfRange_Count: + return "Argument 'count' was out of the range of valid values."; + case ExceptionResource.Arg_ArrayPlusOffTooSmall: + return "Array plus offset too small."; + case ExceptionResource.NotSupported_ReadOnlyCollection: + return "This operation is not supported on a read-only collection."; + case ExceptionResource.Arg_RankMultiDimNotSupported: + return "Multi-dimensional arrays are not supported."; + case ExceptionResource.Arg_NonZeroLowerBound: + return "Arrays with a non-zero lower bound are not supported."; + case ExceptionResource.ArgumentOutOfRange_ListInsert: + return "Insertion index was out of the range of valid values."; + case ExceptionResource.ArgumentOutOfRange_NeedNonNegNum: + return "The number must be non-negative."; + case ExceptionResource.ArgumentOutOfRange_SmallCapacity: + return "The capacity cannot be set below the current Count."; + case ExceptionResource.Argument_InvalidOffLen: + return "Invalid offset length."; + case ExceptionResource.ArgumentOutOfRange_BiggerThanCollection: + return "The given value was larger than the size of the collection."; + case ExceptionResource.Serialization_MissingKeys: + return "Serialization error: missing keys."; + case ExceptionResource.Serialization_NullKey: + return "Serialization error: null key."; + case ExceptionResource.NotSupported_KeyCollectionSet: + return "The KeyCollection does not support modification."; + case ExceptionResource.NotSupported_ValueCollectionSet: + return "The ValueCollection does not support modification."; + case ExceptionResource.InvalidOperation_NullArray: + return "Null arrays are not supported."; + case ExceptionResource.InvalidOperation_HSCapacityOverflow: + return "Set hash capacity overflow. Cannot increase size."; + case ExceptionResource.NotSupported_StringComparison: + return "String comparison not supported."; + case ExceptionResource.ConcurrentCollection_SyncRoot_NotSupported: + return "SyncRoot not supported."; + case ExceptionResource.ArgumentException_OtherNotArrayOfCorrectLength: + return "The other array is not of the correct length."; + case ExceptionResource.ArgumentOutOfRange_EndIndexStartIndex: + return "The end index does not come after the start index."; + case ExceptionResource.ArgumentOutOfRange_HugeArrayNotSupported: + return "Huge arrays are not supported."; + case ExceptionResource.Argument_AddingDuplicate: + return "Duplicate item added."; + case ExceptionResource.Argument_InvalidArgumentForComparison: + return "Invalid argument for comparison."; + case ExceptionResource.Arg_LowerBoundsMustMatch: + return "Array lower bounds must match."; + case ExceptionResource.Arg_MustBeType: + return "Argument must be of type: "; + case ExceptionResource.InvalidOperation_IComparerFailed: + return "IComparer failed."; + case ExceptionResource.NotSupported_FixedSizeCollection: + return "This operation is not suppored on a fixed-size collection."; + case ExceptionResource.Rank_MultiDimNotSupported: + return "Multi-dimensional arrays are not supported."; + case ExceptionResource.Arg_TypeNotSupported: + return "Type not supported."; + default: + Debug.Assert(false, + "The enum value is not defined, please check the ExceptionResource Enum."); + return resource.ToString(); + } + } + } + + // + // The convention for this enum is using the argument name as the enum name + // + internal enum ExceptionArgument + { + obj, + dictionary, + array, + info, + key, + text, + values, + value, + startIndex, + task, + ch, + s, + input, + list, + index, + capacity, + collection, + item, + converter, + match, + count, + action, + comparison, + exceptions, + exception, + enumerable, + start, + format, + culture, + comparer, + comparable, + source, + state, + length, + comparisonType, + manager, + sourceBytesToCopy, + callBack, + creationOptions, + function, + delay, + millisecondsDelay, + millisecondsTimeout, + timeout, + type, + sourceIndex, + sourceArray, + destinationIndex, + destinationArray, + other, + newSize, + lowerBounds, + lengths, + len, + keys, + indices, + endIndex, + elementType, + arrayIndex + } + + // + // The convention for this enum is using the resource name as the enum name + // + internal enum ExceptionResource + { + ArgumentOutOfRange_Index, + ArgumentOutOfRange_Count, + Arg_ArrayPlusOffTooSmall, + NotSupported_ReadOnlyCollection, + Arg_RankMultiDimNotSupported, + Arg_NonZeroLowerBound, + ArgumentOutOfRange_ListInsert, + ArgumentOutOfRange_NeedNonNegNum, + ArgumentOutOfRange_SmallCapacity, + Argument_InvalidOffLen, + ArgumentOutOfRange_BiggerThanCollection, + Serialization_MissingKeys, + Serialization_NullKey, + NotSupported_KeyCollectionSet, + NotSupported_ValueCollectionSet, + InvalidOperation_NullArray, + InvalidOperation_HSCapacityOverflow, + NotSupported_StringComparison, + ConcurrentCollection_SyncRoot_NotSupported, + ArgumentException_OtherNotArrayOfCorrectLength, + ArgumentOutOfRange_EndIndexStartIndex, + ArgumentOutOfRange_HugeArrayNotSupported, + Argument_AddingDuplicate, + Argument_InvalidArgumentForComparison, + Arg_LowerBoundsMustMatch, + Arg_MustBeType, + InvalidOperation_IComparerFailed, + NotSupported_FixedSizeCollection, + Rank_MultiDimNotSupported, + Arg_TypeNotSupported, + } +} diff --git a/src/Avalonia.Base/Contract.cs b/src/Avalonia.Base/Contract.cs index 0b67601db5..27427700ac 100644 --- a/src/Avalonia.Base/Contract.cs +++ b/src/Avalonia.Base/Contract.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Runtime.CompilerServices; using JetBrains.Annotations; diff --git a/src/Avalonia.Base/Data/AssignBindingAttribute.cs b/src/Avalonia.Base/Data/AssignBindingAttribute.cs index 0d050449cd..15bd32c7c2 100644 --- a/src/Avalonia.Base/Data/AssignBindingAttribute.cs +++ b/src/Avalonia.Base/Data/AssignBindingAttribute.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Data diff --git a/src/Avalonia.Base/Data/BindingChainException.cs b/src/Avalonia.Base/Data/BindingChainException.cs index a609ace4ec..86112ce12d 100644 --- a/src/Avalonia.Base/Data/BindingChainException.cs +++ b/src/Avalonia.Base/Data/BindingChainException.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; namespace Avalonia.Data { diff --git a/src/Avalonia.Base/Data/BindingMode.cs b/src/Avalonia.Base/Data/BindingMode.cs index 1e0e80f496..0d04c2eb10 100644 --- a/src/Avalonia.Base/Data/BindingMode.cs +++ b/src/Avalonia.Base/Data/BindingMode.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Data { /// diff --git a/src/Avalonia.Base/Data/BindingNotification.cs b/src/Avalonia.Base/Data/BindingNotification.cs index 7c55321a80..516c26d1d3 100644 --- a/src/Avalonia.Base/Data/BindingNotification.cs +++ b/src/Avalonia.Base/Data/BindingNotification.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; namespace Avalonia.Data { @@ -30,6 +27,12 @@ namespace Avalonia.Data /// Represents a binding notification that can be a valid binding value, or a binding or /// data validation error. /// + /// + /// This class is very similar to , but where + /// is used by typed bindings, this class is used to hold binding and data validation errors in + /// untyped bindings. As Avalonia moves towards using typed bindings by default we may want to remove + /// this class. + /// public class BindingNotification { /// @@ -236,6 +239,26 @@ namespace Avalonia.Data _value = value; } + public BindingValue ToBindingValue() + { + if (ErrorType == BindingErrorType.None) + { + return HasValue ? new BindingValue(Value) : BindingValue.Unset; + } + else if (ErrorType == BindingErrorType.Error) + { + return BindingValue.BindingError( + Error, + HasValue ? new Optional(Value) : Optional.Empty); + } + else + { + return BindingValue.DataValidationError( + Error, + HasValue ? new Optional(Value) : Optional.Empty); + } + } + /// public override string ToString() { diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index 44b47329ac..dc8421fb35 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -1,16 +1,13 @@ -// 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 System; -using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; +using Avalonia.Reactive; namespace Avalonia.Data { public static class BindingOperations { - public static readonly object DoNothing = new object(); + public static readonly object DoNothing = new DoNothingType(); /// /// Applies an a property on an . @@ -56,25 +53,51 @@ namespace Avalonia.Data if (source != null) { + // Perf: Avoid allocating closure in the outer scope. + var targetCopy = target; + var propertyCopy = property; + var bindingCopy = binding; + return source .Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue) .Take(1) - .Subscribe(x => target.SetValue(property, x, binding.Priority)); + .Subscribe(x => targetCopy.SetValue( + propertyCopy, + BindingNotification.ExtractValue(x), + bindingCopy.Priority)); } else { target.SetValue(property, binding.Value, binding.Priority); return Disposable.Empty; } + case BindingMode.OneWayToSource: + { + // Perf: Avoid allocating closure in the outer scope. + var bindingCopy = binding; + return Observable.CombineLatest( binding.Observable, target.GetObservable(property), (_, v) => v) - .Subscribe(x => binding.Subject.OnNext(x)); + .Subscribe(x => bindingCopy.Subject.OnNext(x)); + } + default: throw new ArgumentException("Invalid binding mode."); } } } + + public sealed class DoNothingType + { + internal DoNothingType() { } + + /// + /// Returns the string representation of . + /// + /// The string "(do nothing)". + public override string ToString() => "(do nothing)"; + } } diff --git a/src/Avalonia.Base/Data/BindingPriority.cs b/src/Avalonia.Base/Data/BindingPriority.cs index 55246cabf5..68b2912ffe 100644 --- a/src/Avalonia.Base/Data/BindingPriority.cs +++ b/src/Avalonia.Base/Data/BindingPriority.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Data { /// diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs new file mode 100644 index 0000000000..9aac1bacba --- /dev/null +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -0,0 +1,432 @@ +using System; +using Avalonia.Utilities; + +#nullable enable + +namespace Avalonia.Data +{ + /// + /// Describes the type of a . + /// + [Flags] + public enum BindingValueType + { + /// + /// An unset value: the target property will revert to its unbound state until a new + /// binding value is produced. + /// + UnsetValue = 0, + + /// + /// Do nothing: the binding value will be ignored. + /// + DoNothing = 1, + + /// + /// A simple value. + /// + Value = 2 | HasValue, + + /// + /// A binding error, such as a missing source property. + /// + BindingError = 3 | HasError, + + /// + /// A data validation error. + /// + DataValidationError = 4 | HasError, + + /// + /// A binding error with a fallback value. + /// + BindingErrorWithFallback = BindingError | HasValue, + + /// + /// A data validation error with a fallback value. + /// + DataValidationErrorWithFallback = DataValidationError | HasValue, + + TypeMask = 0x00ff, + HasValue = 0x0100, + HasError = 0x0200, + } + + /// + /// A value passed into a binding. + /// + /// The value type. + /// + /// The avalonia binding system is typed, and as such additional state is stored in this + /// structure. A binding value can be in a number of states, described by the + /// property: + /// + /// - : a simple value + /// - : the target property will revert to its unbound + /// state until a new binding value is produced. Represented by + /// in an untyped context + /// - : the binding value will be ignored. Represented + /// by in an untyped context + /// - : a binding error, such as a missing source + /// property, with an optional fallback value + /// - : a data validation error, with an + /// optional fallback value + /// + /// To create a new binding value you can: + /// + /// - For a simple value, call the constructor or use an implicit + /// conversion from + /// - For an unset value, use or simply `default` + /// - For other types, call one of the static factory methods + /// + public readonly struct BindingValue + { + private readonly T _value; + + /// + /// Initializes a new instance of the struct with a type of + /// + /// + /// The value. + public BindingValue(T value) + { + ValidateValue(value); + _value = value; + Type = BindingValueType.Value; + Error = null; + } + + private BindingValue(BindingValueType type, T value, Exception? error) + { + _value = value; + Type = type; + Error = error; + } + + /// + /// Gets a value indicating whether the binding value represents either a binding or data + /// validation error. + /// + public bool HasError => Type.HasFlagCustom(BindingValueType.HasError); + + /// + /// Gets a value indicating whether the binding value has a value. + /// + public bool HasValue => Type.HasFlagCustom(BindingValueType.HasValue); + + /// + /// Gets the type of the binding value. + /// + public BindingValueType Type { get; } + + /// + /// Gets the binding value or fallback value. + /// + /// + /// is false. + /// + public T Value => HasValue ? _value : throw new InvalidOperationException("BindingValue has no value."); + + /// + /// Gets the binding or data validation error. + /// + public Exception? Error { get; } + + /// + /// Converts the binding value to an . + /// + /// + public Optional ToOptional() => HasValue ? new Optional(_value) : default; + + /// + public override string ToString() => HasError ? $"Error: {Error!.Message}" : _value?.ToString() ?? "(null)"; + + /// + /// Converts the value to untyped representation, using , + /// and where + /// appropriate. + /// + /// The untyped representation of the binding value. + public object? ToUntyped() + { + return Type switch + { + BindingValueType.UnsetValue => AvaloniaProperty.UnsetValue, + BindingValueType.DoNothing => BindingOperations.DoNothing, + BindingValueType.Value => _value, + BindingValueType.BindingError => + new BindingNotification(Error, BindingErrorType.Error), + BindingValueType.BindingErrorWithFallback => + new BindingNotification(Error, BindingErrorType.Error, Value), + BindingValueType.DataValidationError => + new BindingNotification(Error, BindingErrorType.DataValidationError), + BindingValueType.DataValidationErrorWithFallback => + new BindingNotification(Error, BindingErrorType.DataValidationError, Value), + _ => throw new NotSupportedException("Invalid BindingValueType."), + }; + } + + /// + /// Returns a new binding value with the specified value. + /// + /// The new value. + /// The new binding value. + /// + /// The binding type is or + /// . + /// + public BindingValue WithValue(T value) + { + if (Type == BindingValueType.DoNothing) + { + throw new InvalidOperationException("Cannot add value to DoNothing binding value."); + } + + var type = Type == BindingValueType.UnsetValue ? BindingValueType.Value : Type; + return new BindingValue(type | BindingValueType.HasValue, value, Error); + } + + /// + /// Gets the value of the binding value if present, otherwise the default value. + /// + /// The value. + public T GetValueOrDefault() => HasValue ? _value : default; + + /// + /// Gets the value of the binding value if present, otherwise a default value. + /// + /// The default value. + /// The value. + public T GetValueOrDefault(T defaultValue) => HasValue ? _value : defaultValue; + + /// + /// Gets the value if present, otherwise the default value. + /// + /// + /// The value if present and of the correct type, `default(TResult)` if the value is + /// not present or of an incorrect type. + /// + public TResult GetValueOrDefault() + { + return HasValue ? + _value is TResult result ? result : default + : default; + } + + /// + /// Gets the value of the binding value if present, otherwise a default value. + /// + /// The default value. + /// + /// The value if present and of the correct type, `default(TResult)` if the value is + /// present but not of the correct type or null, or if the + /// value is not present. + /// + public TResult GetValueOrDefault(TResult defaultValue) + { + return HasValue ? + _value is TResult result ? result : default + : defaultValue; + } + + /// + /// Creates a from an object, handling the special values + /// and . + /// + /// The untyped value. + /// The typed binding value. + public static BindingValue FromUntyped(object? value) + { + return value switch + { + UnsetValueType _ => Unset, + DoNothingType _ => DoNothing, + BindingNotification n => n.ToBindingValue().Cast(), + _ => (T)value + }; + } + + /// + /// Creates a binding value from an instance of the underlying value type. + /// + /// The value. + public static implicit operator BindingValue(T value) => new BindingValue(value); + + /// + /// Creates a binding value from an . + /// + /// The optional value. + + public static implicit operator BindingValue(Optional optional) + { + return optional.HasValue ? optional.Value : Unset; + } + + /// + /// Returns a binding value with a type of . + /// + public static BindingValue Unset => new BindingValue(BindingValueType.UnsetValue, default, null); + + /// + /// Returns a binding value with a type of . + /// + public static BindingValue DoNothing => new BindingValue(BindingValueType.DoNothing, default, null); + + /// + /// Returns a binding value with a type of . + /// + /// The binding error. + public static BindingValue BindingError(Exception e) + { + e = e ?? throw new ArgumentNullException("e"); + + return new BindingValue(BindingValueType.BindingError, default, e); + } + + /// + /// Returns a binding value with a type of . + /// + /// The binding error. + /// The fallback value. + public static BindingValue BindingError(Exception e, T fallbackValue) + { + e = e ?? throw new ArgumentNullException("e"); + + return new BindingValue(BindingValueType.BindingErrorWithFallback, fallbackValue, e); + } + + /// + /// Returns a binding value with a type of or + /// . + /// + /// The binding error. + /// The fallback value. + public static BindingValue BindingError(Exception e, Optional fallbackValue) + { + e = e ?? throw new ArgumentNullException("e"); + + return new BindingValue( + fallbackValue.HasValue ? + BindingValueType.BindingErrorWithFallback : + BindingValueType.BindingError, + fallbackValue.HasValue ? fallbackValue.Value : default, + e); + } + + /// + /// Returns a binding value with a type of . + /// + /// The data validation error. + public static BindingValue DataValidationError(Exception e) + { + e = e ?? throw new ArgumentNullException("e"); + + return new BindingValue(BindingValueType.DataValidationError, default, e); + } + + /// + /// Returns a binding value with a type of . + /// + /// The data validation error. + /// The fallback value. + public static BindingValue DataValidationError(Exception e, T fallbackValue) + { + e = e ?? throw new ArgumentNullException("e"); + + return new BindingValue(BindingValueType.DataValidationErrorWithFallback, fallbackValue, e); + } + + /// + /// Returns a binding value with a type of or + /// . + /// + /// The binding error. + /// The fallback value. + public static BindingValue DataValidationError(Exception e, Optional fallbackValue) + { + e = e ?? throw new ArgumentNullException("e"); + + return new BindingValue( + fallbackValue.HasValue ? + BindingValueType.DataValidationErrorWithFallback : + BindingValueType.DataValidationError, + fallbackValue.HasValue ? fallbackValue.Value : default, + e); + } + + private static void ValidateValue(T value) + { + if (value is UnsetValueType) + { + throw new InvalidOperationException("AvaloniaValue.UnsetValue is not a valid value for BindingValue<>."); + } + + if (value is DoNothingType) + { + throw new InvalidOperationException("BindingOperations.DoNothing is not a valid value for BindingValue<>."); + } + + if (value is BindingValue) + { + throw new InvalidOperationException("BindingValue cannot be wrapped in a BindingValue<>."); + } + } + } + + public static class BindingValueExtensions + { + /// + /// Casts the type of a using only the C# cast operator. + /// + /// The target type. + /// The binding value. + /// The cast value. + public static BindingValue Cast(this BindingValue value) + { + return value.Type switch + { + BindingValueType.DoNothing => BindingValue.DoNothing, + BindingValueType.UnsetValue => BindingValue.Unset, + BindingValueType.Value => new BindingValue((T)value.Value), + BindingValueType.BindingError => BindingValue.BindingError(value.Error!), + BindingValueType.BindingErrorWithFallback => BindingValue.BindingError( + value.Error!, + (T)value.Value), + BindingValueType.DataValidationError => BindingValue.DataValidationError(value.Error!), + BindingValueType.DataValidationErrorWithFallback => BindingValue.DataValidationError( + value.Error!, + (T)value.Value), + _ => throw new NotSupportedException("Invalid BindingValue type."), + }; + } + + /// + /// Casts the type of a using the implicit conversions + /// allowed by the C# language. + /// + /// The target type. + /// The binding value. + /// The cast value. + /// + /// Note that this method uses reflection and as such may be slow. + /// + public static BindingValue Convert(this BindingValue value) + { + return value.Type switch + { + BindingValueType.DoNothing => BindingValue.DoNothing, + BindingValueType.UnsetValue => BindingValue.Unset, + BindingValueType.Value => new BindingValue(TypeUtilities.ConvertImplicit(value.Value)), + BindingValueType.BindingError => BindingValue.BindingError(value.Error!), + BindingValueType.BindingErrorWithFallback => BindingValue.BindingError( + value.Error!, + TypeUtilities.ConvertImplicit(value.Value)), + BindingValueType.DataValidationError => BindingValue.DataValidationError(value.Error!), + BindingValueType.DataValidationErrorWithFallback => BindingValue.DataValidationError( + value.Error!, + TypeUtilities.ConvertImplicit(value.Value)), + _ => throw new NotSupportedException("Invalid BindingValue type."), + }; + } + } +} diff --git a/src/Avalonia.Base/Data/Converters/BoolConverters.cs b/src/Avalonia.Base/Data/Converters/BoolConverters.cs index 6b429e1087..6740c2168f 100644 --- a/src/Avalonia.Base/Data/Converters/BoolConverters.cs +++ b/src/Avalonia.Base/Data/Converters/BoolConverters.cs @@ -1,12 +1,9 @@ -// 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 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/Converters/DefaultValueConverter.cs b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs index 0ffd6a9539..5e80a15059 100644 --- a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Globalization; using System.Windows.Input; diff --git a/src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs b/src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs index 6e1c4cb0e3..988cc123f9 100644 --- a/src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Globalization; @@ -30,7 +27,23 @@ namespace Avalonia.Data.Converters /// public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) { - var converted = values.OfType().ToList(); + //standard OfType skip null values, even they are valid for the Type + static IEnumerable OfTypeWithDefaultSupport(IList list) + { + foreach (object obj in list) + { + if (obj is TIn result) + { + yield return result; + } + else if (Equals(obj, default(TIn))) + { + yield return default(TIn); + } + } + } + + var converted = OfTypeWithDefaultSupport(values).ToList(); if (converted.Count == values.Count) { diff --git a/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs b/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs index b747587b4a..9ec600d2bc 100644 --- a/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Globalization; using Avalonia.Utilities; diff --git a/src/Avalonia.Base/Data/Converters/IMultiValueConverter.cs b/src/Avalonia.Base/Data/Converters/IMultiValueConverter.cs index 3f84fcb3e7..0468835c41 100644 --- a/src/Avalonia.Base/Data/Converters/IMultiValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/IMultiValueConverter.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Globalization; diff --git a/src/Avalonia.Base/Data/Converters/IValueConverter.cs b/src/Avalonia.Base/Data/Converters/IValueConverter.cs index 754aa7975d..f31f4d522e 100644 --- a/src/Avalonia.Base/Data/Converters/IValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/IValueConverter.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Globalization; diff --git a/src/Avalonia.Base/Data/Converters/ObjectConverters.cs b/src/Avalonia.Base/Data/Converters/ObjectConverters.cs index 3cadfd5b47..755d826dda 100644 --- a/src/Avalonia.Base/Data/Converters/ObjectConverters.cs +++ b/src/Avalonia.Base/Data/Converters/ObjectConverters.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Data.Converters { diff --git a/src/Avalonia.Base/Data/Converters/StringConverters.cs b/src/Avalonia.Base/Data/Converters/StringConverters.cs index 42eb8e1d81..4b0a845770 100644 --- a/src/Avalonia.Base/Data/Converters/StringConverters.cs +++ b/src/Avalonia.Base/Data/Converters/StringConverters.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Data.Converters { diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 986e2cf012..c4f61dfedb 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Globalization; using System.Reactive.Linq; @@ -21,6 +18,7 @@ namespace Avalonia.Data.Core private readonly ExpressionObserver _inner; private readonly Type _targetType; private readonly object _fallbackValue; + private readonly object _targetNullValue; private readonly BindingPriority _priority; InnerListener _innerListener; WeakReference _value; @@ -51,7 +49,7 @@ namespace Avalonia.Data.Core IValueConverter converter, object converterParameter = null, BindingPriority priority = BindingPriority.LocalValue) - : this(inner, targetType, AvaloniaProperty.UnsetValue, converter, converterParameter, priority) + : this(inner, targetType, AvaloniaProperty.UnsetValue, AvaloniaProperty.UnsetValue, converter, converterParameter, priority) { } @@ -63,6 +61,9 @@ namespace Avalonia.Data.Core /// /// The value to use when the binding is unable to produce a value. /// + /// + /// The value to use when the binding result is null. + /// /// The value converter to use. /// /// A parameter to pass to . @@ -72,6 +73,7 @@ namespace Avalonia.Data.Core ExpressionObserver inner, Type targetType, object fallbackValue, + object targetNullValue, IValueConverter converter, object converterParameter = null, BindingPriority priority = BindingPriority.LocalValue) @@ -85,6 +87,7 @@ namespace Avalonia.Data.Core Converter = converter; ConverterParameter = converterParameter; _fallbackValue = fallbackValue; + _targetNullValue = targetNullValue; _priority = priority; } @@ -165,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, @@ -196,6 +198,11 @@ namespace Avalonia.Data.Core /// private object ConvertValue(object value) { + if (value == null && _targetNullValue != AvaloniaProperty.UnsetValue) + { + return _targetNullValue; + } + if (value == BindingOperations.DoNothing) { return value; diff --git a/src/Avalonia.Base/Data/Core/CommonPropertyNames.cs b/src/Avalonia.Base/Data/Core/CommonPropertyNames.cs index 6760c3f259..da6f407d81 100644 --- a/src/Avalonia.Base/Data/Core/CommonPropertyNames.cs +++ b/src/Avalonia.Base/Data/Core/CommonPropertyNames.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Data.Core { public static class CommonPropertyNames diff --git a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs index d5658b124b..4e142fbee9 100644 --- a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs @@ -1,7 +1,4 @@ -// 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. - -namespace Avalonia.Data.Core +namespace Avalonia.Data.Core { public class EmptyExpressionNode : ExpressionNode { diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index c2e5c8e4f3..84cbe32318 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Data.Core diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index d3ebb7d2c1..7ecaa278d7 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Linq.Expressions; @@ -21,7 +18,7 @@ namespace Avalonia.Data.Core /// An ordered collection of property accessor plugins that can be used to customize /// the reading and subscription of property values on a type. /// - public static readonly IList PropertyAccessors = + public static readonly List PropertyAccessors = new List { new AvaloniaPropertyAccessorPlugin(), @@ -33,7 +30,7 @@ namespace Avalonia.Data.Core /// An ordered collection of validation checker plugins that can be used to customize /// the validation of view model and model data. /// - public static readonly IList DataValidators = + public static readonly List DataValidators = new List { new DataAnnotationsValidationPlugin(), @@ -45,7 +42,7 @@ namespace Avalonia.Data.Core /// An ordered collection of stream plugins that can be used to customize the behavior /// of the '^' stream binding operator. /// - public static readonly IList StreamHandlers = + public static readonly List StreamHandlers = new List { new TaskStreamPlugin(), diff --git a/src/Avalonia.Base/Data/Core/ExpressionParseException.cs b/src/Avalonia.Base/Data/Core/ExpressionParseException.cs index 2432410203..b0d5c21dab 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionParseException.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionParseException.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Data.Core diff --git a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs index 6d58b18a9f..7c402f42f6 100644 --- a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs +++ b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Globalization; diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs index ab4a109cc2..c1e8eb115d 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -1,8 +1,5 @@ -// 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 System; -using System.Reactive.Linq; +using System.Runtime.ExceptionServices; namespace Avalonia.Data.Core.Plugins { @@ -76,7 +73,7 @@ namespace Avalonia.Data.Core.Plugins return false; } - private class Accessor : PropertyAccessorBase + private class Accessor : PropertyAccessorBase, IObserver { private readonly WeakReference _reference; private readonly AvaloniaProperty _property; @@ -117,7 +114,7 @@ namespace Avalonia.Data.Core.Plugins protected override void SubscribeCore() { - _subscription = Instance?.GetObservable(_property).Subscribe(PublishValue); + _subscription = Instance?.GetObservable(_property).Subscribe(this); } protected override void UnsubscribeCore() @@ -125,6 +122,20 @@ namespace Avalonia.Data.Core.Plugins _subscription?.Dispose(); _subscription = null; } + + void IObserver.OnCompleted() + { + } + + void IObserver.OnError(Exception error) + { + ExceptionDispatchInfo.Capture(error).Throw(); + } + + void IObserver.OnNext(object value) + { + PublishValue(value); + } } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs index f5b545d2ff..3b61f6d898 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; diff --git a/src/Avalonia.Base/Data/Core/Plugins/DataValidationBase.cs b/src/Avalonia.Base/Data/Core/Plugins/DataValidationBase.cs index 2e0d1aa805..63e2c3a97b 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/DataValidationBase.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/DataValidationBase.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; namespace Avalonia.Data.Core.Plugins { diff --git a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs index f305912fe1..f7ab48943a 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Reflection; namespace Avalonia.Data.Core.Plugins diff --git a/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs index 5b1af22f14..324279e9f0 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; namespace Avalonia.Data.Core.Plugins { diff --git a/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs b/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs index 33ea5bba08..688fb8cdc2 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Data.Core.Plugins diff --git a/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs index a0021fa4d4..b176225fba 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Data.Core.Plugins diff --git a/src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs index 3df578d25b..514e6f430a 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; namespace Avalonia.Data.Core.Plugins { diff --git a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs index b353d03bcd..7be2a6db86 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index cbceb58204..8fc2a7b77c 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -1,10 +1,5 @@ -// 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 System; using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; using System.Reflection; using Avalonia.Utilities; @@ -17,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. @@ -34,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) { @@ -48,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/ObservableStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs index ef5ce05821..33a1d02211 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Linq; using System.Reactive.Linq; using System.Reflection; diff --git a/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs b/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs index e840b2c5c9..59e8e01a44 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; namespace Avalonia.Data.Core.Plugins { diff --git a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs index 5817e93314..debf050a97 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Reflection; diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index 6052019869..e621512ded 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -1,9 +1,5 @@ -// 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 System; -using System.Linq; -using System.Reactive.Linq; +using System.Diagnostics; using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core @@ -52,6 +48,18 @@ namespace Avalonia.Data.Core var plugin = _customPlugin ?? GetPropertyAccessorPluginForObject(target); var accessor = plugin?.Start(reference, PropertyName); + // We need to handle accessor fallback before handling validation. Validators do not support null accessors. + if (accessor == null) + { + reference.TryGetTarget(out object instance); + + 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) { foreach (var validator in ExpressionObserver.DataValidators) @@ -63,14 +71,24 @@ namespace Avalonia.Data.Core } } - _accessor = accessor ?? throw new NotSupportedException( - $"Could not find a matching property accessor for {PropertyName}."); + if (accessor is null) + { + throw new AvaloniaInternalException("Data validators must return non-null accessor."); + } + + _accessor = accessor; accessor.Subscribe(ValueChanged); } private IPropertyAccessorPlugin GetPropertyAccessorPluginForObject(object target) { - return ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(target, PropertyName)); + foreach (IPropertyAccessorPlugin x in ExpressionObserver.PropertyAccessors) + { + if (x.Match(target, PropertyName)) + { + return x; + } + } } protected override void StopListeningCore() diff --git a/src/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs index eaf1083b27..023999f5c5 100644 --- a/src/Avalonia.Base/Data/Core/StreamNode.cs +++ b/src/Avalonia.Base/Data/Core/StreamNode.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Reactive.Linq; using Avalonia.Data.Core.Plugins; diff --git a/src/Avalonia.Base/Data/IBinding.cs b/src/Avalonia.Base/Data/IBinding.cs index 95239cae06..bc91cf8960 100644 --- a/src/Avalonia.Base/Data/IBinding.cs +++ b/src/Avalonia.Base/Data/IBinding.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Data { /// diff --git a/src/Avalonia.Base/Data/IndexerBinding.cs b/src/Avalonia.Base/Data/IndexerBinding.cs index 6b5a84989a..cc3baa4530 100644 --- a/src/Avalonia.Base/Data/IndexerBinding.cs +++ b/src/Avalonia.Base/Data/IndexerBinding.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; namespace Avalonia.Data { diff --git a/src/Avalonia.Base/Data/IndexerDescriptor.cs b/src/Avalonia.Base/Data/IndexerDescriptor.cs index fef21a55c4..3cf6767b89 100644 --- a/src/Avalonia.Base/Data/IndexerDescriptor.cs +++ b/src/Avalonia.Base/Data/IndexerDescriptor.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Reactive; diff --git a/src/Avalonia.Base/Data/InstancedBinding.cs b/src/Avalonia.Base/Data/InstancedBinding.cs index dc35fe076b..0d20f25fe2 100644 --- a/src/Avalonia.Base/Data/InstancedBinding.cs +++ b/src/Avalonia.Base/Data/InstancedBinding.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Reactive.Subjects; diff --git a/src/Avalonia.Base/Data/Optional.cs b/src/Avalonia.Base/Data/Optional.cs new file mode 100644 index 0000000000..dd952c895c --- /dev/null +++ b/src/Avalonia.Base/Data/Optional.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; + +#nullable enable + +namespace Avalonia.Data +{ + /// + /// An optional typed value. + /// + /// The value type. + /// + /// This struct is similar to except it also accepts reference types: + /// note that null is a valid value for reference types. It is also similar to + /// but has only two states: "value present" and "value missing". + /// + /// To create a new optional value you can: + /// + /// - For a simple value, call the constructor or use an implicit + /// conversion from + /// - For an missing value, use or simply `default` + /// + public readonly struct Optional : IEquatable> + { + private readonly T _value; + + /// + /// Initializes a new instance of the struct with value. + /// + /// The value. + public Optional(T value) + { + _value = value; + HasValue = true; + } + + /// + /// Gets a value indicating whether a value is present. + /// + public bool HasValue { get; } + + /// + /// Gets the value. + /// + /// + /// is false. + /// + 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 bool Equals(Optional other) => this == other; + + /// + public override int GetHashCode() => HasValue ? _value?.GetHashCode() ?? 0 : 0; + + /// + /// Casts the value (if any) to an . + /// + /// The cast optional value. + public Optional ToObject() => HasValue ? new Optional(_value) : default; + + /// + public override string ToString() => HasValue ? _value?.ToString() ?? "(null)" : "(empty)"; + + /// + /// Gets the value if present, otherwise the default value. + /// + /// The value. + public T GetValueOrDefault() => HasValue ? _value : default; + + /// + /// Gets the value if present, otherwise a default value. + /// + /// The default value. + /// The value. + public T GetValueOrDefault(T defaultValue) => HasValue ? _value : defaultValue; + + /// + /// Gets the value if present, otherwise the default value. + /// + /// + /// The value if present and of the correct type, `default(TResult)` if the value is + /// not present or of an incorrect type. + /// + public TResult GetValueOrDefault() + { + return HasValue ? + _value is TResult result ? result : default + : default; + } + + /// + /// Gets the value if present, otherwise a default value. + /// + /// The default value. + /// + /// The value if present and of the correct type, `default(TResult)` if the value is + /// present but not of the correct type or null, or if the + /// value is not present. + /// + public TResult GetValueOrDefault(TResult defaultValue) + { + return HasValue ? + _value is TResult result ? result : default + : defaultValue; + } + + /// + /// Creates an from an instance of the underlying value type. + /// + /// The value. + public static implicit operator Optional(T value) => new Optional(value); + + /// + /// Compares two s for inequality. + /// + /// The first value. + /// The second value. + /// True if the values are unequal; otherwise false. + public static bool operator !=(Optional x, Optional y) => !(x == y); + + /// + /// Compares two s for equality. + /// + /// The first value. + /// The second value. + /// True if the values are equal; otherwise false. + public static bool operator==(Optional x, Optional y) + { + if (!x.HasValue && !y.HasValue) + { + return true; + } + else if (x.HasValue && y.HasValue) + { + return EqualityComparer.Default.Equals(x.Value, y.Value); + } + else + { + return false; + } + } + + /// + /// Returns an without a value. + /// + public static Optional Empty => default; + } +} diff --git a/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs index 7afbcabd2a..d7b1f2e053 100644 --- a/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs @@ -1,6 +1,4 @@ -// 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 System; using Avalonia.Data; namespace Avalonia.Diagnostics @@ -21,35 +19,7 @@ namespace Avalonia.Diagnostics /// public static AvaloniaPropertyValue GetDiagnostic(this AvaloniaObject o, AvaloniaProperty property) { - var set = o.GetSetValues(); - - if (set.TryGetValue(property, out var obj)) - { - if (obj is PriorityValue value) - { - return new AvaloniaPropertyValue( - property, - o.GetValue(property), - (BindingPriority)value.ValuePriority, - value.GetDiagnostic()); - } - else - { - return new AvaloniaPropertyValue( - property, - obj, - BindingPriority.LocalValue, - "Local value"); - } - } - else - { - return new AvaloniaPropertyValue( - property, - o.GetValue(property), - BindingPriority.Unset, - "Unset"); - } + return o.GetDiagnosticInternal(property); } } } diff --git a/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs b/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs index af15f66bb0..c881c389c1 100644 --- a/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs +++ b/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs @@ -1,6 +1,3 @@ -// 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; namespace Avalonia.Diagnostics diff --git a/src/Avalonia.Base/Diagnostics/IAvaloniaObjectDebug.cs b/src/Avalonia.Base/Diagnostics/IAvaloniaObjectDebug.cs index f1d31cc1e7..7f09425905 100644 --- a/src/Avalonia.Base/Diagnostics/IAvaloniaObjectDebug.cs +++ b/src/Avalonia.Base/Diagnostics/IAvaloniaObjectDebug.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Diagnostics diff --git a/src/Avalonia.Base/Diagnostics/INotifyCollectionChangedDebug.cs b/src/Avalonia.Base/Diagnostics/INotifyCollectionChangedDebug.cs index 557d5bdc42..36152e6430 100644 --- a/src/Avalonia.Base/Diagnostics/INotifyCollectionChangedDebug.cs +++ b/src/Avalonia.Base/Diagnostics/INotifyCollectionChangedDebug.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Specialized; using Avalonia.Collections; diff --git a/src/Avalonia.Base/DirectProperty.cs b/src/Avalonia.Base/DirectProperty.cs index 1ce73c20ba..d21969502a 100644 --- a/src/Avalonia.Base/DirectProperty.cs +++ b/src/Avalonia.Base/DirectProperty.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Data; @@ -16,7 +13,7 @@ namespace Avalonia /// system. They hold a getter and an optional setter which /// allows the avalonia property system to read and write the current value. /// - public class DirectProperty : AvaloniaProperty, IDirectPropertyAccessor + public class DirectProperty : DirectPropertyBase, IDirectPropertyAccessor where TOwner : IAvaloniaObject { /// @@ -26,12 +23,16 @@ namespace Avalonia /// Gets the current value of the property. /// Sets the value of the property. May be null. /// The property metadata. + /// + /// Whether the property is interested in data validation. + /// public DirectProperty( string name, Func getter, Action setter, - DirectPropertyMetadata metadata) - : base(name, typeof(TOwner), metadata) + DirectPropertyMetadata metadata, + bool enableDataValidation) + : base(name, typeof(TOwner), metadata, enableDataValidation) { Contract.Requires(getter != null); @@ -46,12 +47,16 @@ namespace Avalonia /// Gets the current value of the property. /// Sets the value of the property. May be null. /// Optional overridden metadata. + /// + /// Whether the property is interested in data validation. + /// private DirectProperty( - AvaloniaProperty source, + DirectPropertyBase source, Func getter, Action setter, - DirectPropertyMetadata metadata) - : base(source, typeof(TOwner), metadata) + DirectPropertyMetadata metadata, + bool enableDataValidation) + : base(source, typeof(TOwner), metadata, enableDataValidation) { Contract.Requires(getter != null); @@ -65,6 +70,9 @@ namespace Avalonia /// public override bool IsReadOnly => Setter == null; + /// + public override Type Owner => typeof(TOwner); + /// /// Gets the getter function. /// @@ -75,9 +83,6 @@ namespace Avalonia /// public Action Setter { get; } - /// - Type IDirectPropertyAccessor.Owner => typeof(TOwner); - /// /// Registers the direct property on another type. /// @@ -99,6 +104,45 @@ namespace Avalonia BindingMode defaultBindingMode = BindingMode.Default, bool enableDataValidation = false) where TNewOwner : AvaloniaObject + { + var metadata = new DirectPropertyMetadata( + unsetValue: unsetValue, + defaultBindingMode: defaultBindingMode); + + metadata.Merge(GetMetadata(), this); + + var result = new DirectProperty( + (DirectPropertyBase)this, + getter, + setter, + metadata, + enableDataValidation); + + AvaloniaPropertyRegistry.Instance.Register(typeof(TNewOwner), result); + return result; + } + + /// + /// Registers the direct property on another type. + /// + /// The type of the additional owner. + /// Gets the current value of the property. + /// Sets the value of the property. + /// + /// The value to use when the property is set to + /// + /// The default binding mode for the property. + /// + /// Whether the property is interested in data validation. + /// + /// The property. + public DirectProperty AddOwnerWithDataValidation( + Func getter, + Action setter, + TValue unsetValue = default(TValue), + BindingMode defaultBindingMode = BindingMode.Default, + bool enableDataValidation = false) + where TNewOwner : AvaloniaObject { var metadata = new DirectPropertyMetadata( unsetValue: unsetValue, @@ -111,12 +155,33 @@ namespace Avalonia this, getter, setter, - metadata); + metadata, + enableDataValidation); AvaloniaPropertyRegistry.Instance.Register(typeof(TNewOwner), result); return result; } + /// + internal override TValue InvokeGetter(IAvaloniaObject instance) + { + return Getter((TOwner)instance); + } + + /// + internal override void InvokeSetter(IAvaloniaObject instance, BindingValue value) + { + if (Setter == null) + { + throw new ArgumentException($"The property {Name} is readonly."); + } + + if (value.HasValue) + { + Setter((TOwner)instance, value.Value); + } + } + /// object IDirectPropertyAccessor.GetValue(IAvaloniaObject instance) { diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs new file mode 100644 index 0000000000..0e65379abd --- /dev/null +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -0,0 +1,167 @@ +using System; +using Avalonia.Data; +using Avalonia.Reactive; +using Avalonia.Utilities; + +#nullable enable + +namespace Avalonia +{ + /// + /// Base class for direct properties. + /// + /// The type of the property's value. + /// + /// Whereas is typed on the owner type, this base + /// class provides a non-owner-typed interface to a direct poperty. + /// + public abstract class DirectPropertyBase : AvaloniaProperty + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the property. + /// The type of the class that registers the property. + /// The property metadata. + /// + /// Whether the property is interested in data validation. + /// + protected DirectPropertyBase( + string name, + Type ownerType, + PropertyMetadata metadata, + bool enableDataValidation) + : base(name, ownerType, metadata) + { + IsDataValidationEnabled = enableDataValidation; + } + + /// + /// Initializes a new instance of the class. + /// + /// The property to copy. + /// The new owner type. + /// Optional overridden metadata. + /// + /// Whether the property is interested in data validation. + /// + protected DirectPropertyBase( + AvaloniaProperty source, + Type ownerType, + PropertyMetadata metadata, + bool enableDataValidation) + : base(source, ownerType, metadata) + { + IsDataValidationEnabled = enableDataValidation; + } + + /// + /// Gets the type that registered the property. + /// + public abstract Type Owner { get; } + + /// + /// Gets a value that indicates whether data validation is enabled for the property. + /// + public bool IsDataValidationEnabled { get; } + + /// + /// Gets the value of the property on the instance. + /// + /// The instance. + /// The property value. + internal abstract TValue InvokeGetter(IAvaloniaObject instance); + + /// + /// Sets the value of the property on the instance. + /// + /// The instance. + /// The value. + internal abstract void InvokeSetter(IAvaloniaObject instance, BindingValue value); + + /// + /// Gets the unset value for the property on the specified type. + /// + /// The type. + /// The unset value. + public TValue GetUnsetValue(Type type) + { + type = type ?? throw new ArgumentNullException(nameof(type)); + return GetMetadata(type).UnsetValue; + } + + /// + /// Gets the property metadata for the specified type. + /// + /// The type. + /// + /// The property metadata. + /// + public new DirectPropertyMetadata GetMetadata(Type type) + { + return (DirectPropertyMetadata)base.GetMetadata(type); + } + + /// + public override void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) + { + vistor.Visit(this, ref data); + } + + /// + internal override void RouteClearValue(IAvaloniaObject o) + { + o.ClearValue(this); + } + + /// + internal override object? RouteGetValue(IAvaloniaObject o) + { + return o.GetValue(this); + } + + internal override object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) + { + return o.GetValue(this); + } + + /// + internal override IDisposable? RouteSetValue( + IAvaloniaObject o, + object value, + BindingPriority priority) + { + var v = TryConvert(value); + + if (v.HasValue) + { + o.SetValue(this, (TValue)v.Value); + } + else if (v.Type == BindingValueType.UnsetValue) + { + o.ClearValue(this); + } + else if (v.HasError) + { + throw v.Error!; + } + + return null; + } + + /// + internal override IDisposable RouteBind( + IAvaloniaObject o, + IObservable> source, + BindingPriority priority) + { + var adapter = TypedBindingAdapter.Create(o, this, source); + return o.Bind(this, adapter); + } + + internal override void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject oldParent) + { + throw new NotSupportedException("Direct properties do not support inheritance."); + } + } +} diff --git a/src/Avalonia.Base/DirectPropertyMetadata`1.cs b/src/Avalonia.Base/DirectPropertyMetadata`1.cs index 26de578a45..59a60507dc 100644 --- a/src/Avalonia.Base/DirectPropertyMetadata`1.cs +++ b/src/Avalonia.Base/DirectPropertyMetadata`1.cs @@ -1,6 +1,3 @@ -// 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; namespace Avalonia diff --git a/src/Avalonia.Base/EnumExtensions.cs b/src/Avalonia.Base/EnumExtensions.cs new file mode 100644 index 0000000000..1e4864283f --- /dev/null +++ b/src/Avalonia.Base/EnumExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Avalonia +{ + /// + /// Provides extension methods for enums. + /// + public static class EnumExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe bool HasFlagCustom(this T value, T flag) where T : unmanaged, Enum + { + var intValue = *(int*)&value; + var intFlag = *(int*)&flag; + + return (intValue & intFlag) == intFlag; + } + } +} diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index 5a3829167a..0452f77d4c 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Data; @@ -17,16 +14,24 @@ namespace Avalonia event EventHandler PropertyChanged; /// - /// Raised when an inheritable value changes on this object. + /// Clears an 's local value. /// - event EventHandler InheritablePropertyChanged; + /// The property. + void ClearValue(StyledPropertyBase property); + + /// + /// Clears an 's local value. + /// + /// The property. + void ClearValue(DirectPropertyBase property); /// /// Gets a value. /// + /// The type of the property. /// The property. /// The value. - object GetValue(AvaloniaProperty property); + T GetValue(StyledPropertyBase property); /// /// Gets a value. @@ -34,7 +39,20 @@ namespace Avalonia /// The type of the property. /// The property. /// The value. - T GetValue(AvaloniaProperty property); + T GetValue(DirectPropertyBase property); + + /// + /// Gets an base value. + /// + /// The type of the property. + /// The property. + /// The maximum priority for the value. + /// + /// Gets the value of the property, if set on this object with a priority equal or lower to + /// , otherwise . Note that + /// this method does not return property values that come from inherited or default values. + /// + Optional GetBaseValue(StyledPropertyBase property, BindingPriority maxPriority); /// /// Checks whether a is animating. @@ -53,12 +71,13 @@ namespace Avalonia /// /// Sets a value. /// + /// The type of the property. /// The property. /// The value. /// The priority of the value. - void SetValue( - AvaloniaProperty property, - object value, + IDisposable SetValue( + StyledPropertyBase property, + T value, BindingPriority priority = BindingPriority.LocalValue); /// @@ -67,24 +86,21 @@ namespace Avalonia /// The type of the property. /// The property. /// The value. - /// The priority of the value. - void SetValue( - AvaloniaProperty property, - T value, - BindingPriority priority = BindingPriority.LocalValue); + void SetValue(DirectPropertyBase property, T value); /// /// Binds a to an observable. /// + /// The type of the property. /// The property. /// The observable. /// The priority of the binding. /// /// A disposable which can be used to terminate the binding. /// - IDisposable Bind( - AvaloniaProperty property, - IObservable source, + IDisposable Bind( + StyledPropertyBase property, + IObservable> source, BindingPriority priority = BindingPriority.LocalValue); /// @@ -93,13 +109,52 @@ namespace Avalonia /// The type of the property. /// The property. /// The observable. - /// The priority of the binding. /// /// A disposable which can be used to terminate the binding. /// IDisposable Bind( + DirectPropertyBase property, + IObservable> source); + + /// + /// Coerces the specified . + /// + /// The type of the property. + /// The property. + void CoerceValue(StyledPropertyBase property); + + /// + /// Registers an object as an inheritance child. + /// + /// The inheritance child. + /// + /// Inheritance children will receive a call to + /// + /// when an inheritable property value changes on the parent. + /// + void AddInheritanceChild(IAvaloniaObject child); + + /// + /// Unregisters an object as an inheritance child. + /// + /// The inheritance child. + /// + /// Removes an inheritance child that was added by a call to + /// . + /// + void RemoveInheritanceChild(IAvaloniaObject child); + + /// + /// Called when an inheritable property changes on an object registered as an inheritance + /// parent. + /// + /// The type of the value. + /// The property that has changed. + /// + /// + void InheritedPropertyChanged( AvaloniaProperty property, - IObservable source, - BindingPriority priority = BindingPriority.LocalValue); + Optional oldValue, + Optional newValue); } } diff --git a/src/Avalonia.Base/IDescription.cs b/src/Avalonia.Base/IDescription.cs index db43f7a140..b3d9f16bb7 100644 --- a/src/Avalonia.Base/IDescription.cs +++ b/src/Avalonia.Base/IDescription.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia { /// diff --git a/src/Avalonia.Base/IDirectPropertyAccessor.cs b/src/Avalonia.Base/IDirectPropertyAccessor.cs index 4f46652693..6ae71bc2c6 100644 --- a/src/Avalonia.Base/IDirectPropertyAccessor.cs +++ b/src/Avalonia.Base/IDirectPropertyAccessor.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia diff --git a/src/Avalonia.Base/IDirectPropertyMetadata.cs b/src/Avalonia.Base/IDirectPropertyMetadata.cs index c283855e5f..992fa4ffc9 100644 --- a/src/Avalonia.Base/IDirectPropertyMetadata.cs +++ b/src/Avalonia.Base/IDirectPropertyMetadata.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia { /// diff --git a/src/Avalonia.Base/IPriorityValueOwner.cs b/src/Avalonia.Base/IPriorityValueOwner.cs deleted file mode 100644 index 1d6e5e59ad..0000000000 --- a/src/Avalonia.Base/IPriorityValueOwner.cs +++ /dev/null @@ -1,51 +0,0 @@ -// 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 System; -using Avalonia.Data; -using Avalonia.Utilities; - -namespace Avalonia -{ - /// - /// An owner of a . - /// - internal interface IPriorityValueOwner - { - /// - /// Called when a 's value changes. - /// - /// The the property that has changed. - /// The priority of the value. - /// The old value. - /// The new value. - void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue); - - /// - /// Called when a is received by a - /// . - /// - /// The the property that has changed. - /// The notification. - void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification); - - /// - /// Returns deferred setter for given non-direct property. - /// - /// Property. - /// Deferred setter for given property. - DeferredSetter GetNonDirectDeferredSetter(AvaloniaProperty property); - - /// - /// Logs a binding error. - /// - /// The property the error occurred on. - /// The binding error. - void LogError(AvaloniaProperty property, Exception e); - - /// - /// Ensures that the current thread is the UI thread. - /// - void VerifyAccess(); - } -} diff --git a/src/Avalonia.Base/IStyledPropertyAccessor.cs b/src/Avalonia.Base/IStyledPropertyAccessor.cs index f2ec5bd33f..45d106164f 100644 --- a/src/Avalonia.Base/IStyledPropertyAccessor.cs +++ b/src/Avalonia.Base/IStyledPropertyAccessor.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia @@ -18,14 +15,5 @@ namespace Avalonia /// The default value. /// object GetDefaultValue(Type type); - - /// - /// Gets a validation function for the property on the specified type. - /// - /// The type. - /// - /// The validation function, or null if no validation function exists. - /// - Func GetValidationFunc(Type type); } } diff --git a/src/Avalonia.Base/IStyledPropertyMetadata.cs b/src/Avalonia.Base/IStyledPropertyMetadata.cs index 22cda075fa..f567cd930c 100644 --- a/src/Avalonia.Base/IStyledPropertyMetadata.cs +++ b/src/Avalonia.Base/IStyledPropertyMetadata.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia @@ -14,10 +11,5 @@ namespace Avalonia /// Gets the default value for the property. /// object DefaultValue { get; } - - /// - /// Gets the property's validation function. - /// - Func Validate { get; } } -} \ No newline at end of file +} 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 8b5751b0af..71268d5965 100644 --- a/src/Avalonia.Base/Logging/ILogSink.cs +++ b/src/Avalonia.Base/Logging/ILogSink.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Logging { /// @@ -9,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/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs index 6bc1609cbc..3c19b47a05 100644 --- a/src/Avalonia.Base/Logging/LogArea.cs +++ b/src/Avalonia.Base/Logging/LogArea.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Logging { /// diff --git a/src/Avalonia.Base/Logging/LogEventLevel.cs b/src/Avalonia.Base/Logging/LogEventLevel.cs index fbb3b14d59..68406667ff 100644 --- a/src/Avalonia.Base/Logging/LogEventLevel.cs +++ b/src/Avalonia.Base/Logging/LogEventLevel.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Logging { /// diff --git a/src/Avalonia.Base/Logging/Logger.cs b/src/Avalonia.Base/Logging/Logger.cs index c895c70094..ed3fad93fc 100644 --- a/src/Avalonia.Base/Logging/Logger.cs +++ b/src/Avalonia.Base/Logging/Logger.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Logging { /// @@ -17,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 1550cc1b40..adadb0f990 100644 --- a/src/Avalonia.Base/Logging/ParametrizedLogger.cs +++ b/src/Avalonia.Base/Logging/ParametrizedLogger.cs @@ -1,6 +1,3 @@ -// 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 System.Runtime.CompilerServices; namespace Avalonia.Logging @@ -12,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; } /// @@ -27,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. @@ -86,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. @@ -108,7 +98,6 @@ namespace Avalonia.Logging /// Message property value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Log( - string area, object source, string messageTemplate, T0 propertyValue0, @@ -116,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. @@ -132,7 +120,6 @@ namespace Avalonia.Logging /// Message property value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Log( - string area, object source, string messageTemplate, T0 propertyValue0, @@ -141,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. @@ -158,7 +144,6 @@ namespace Avalonia.Logging /// Message property value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Log( - string area, object source, string messageTemplate, T0 propertyValue0, @@ -168,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/AmbientAttribute.cs b/src/Avalonia.Base/Metadata/AmbientAttribute.cs index db36953300..85ca6c4ec9 100644 --- a/src/Avalonia.Base/Metadata/AmbientAttribute.cs +++ b/src/Avalonia.Base/Metadata/AmbientAttribute.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Metadata diff --git a/src/Avalonia.Base/Metadata/ContentAttribute.cs b/src/Avalonia.Base/Metadata/ContentAttribute.cs index 803769561d..a0b2fa0e1d 100644 --- a/src/Avalonia.Base/Metadata/ContentAttribute.cs +++ b/src/Avalonia.Base/Metadata/ContentAttribute.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Metadata diff --git a/src/Avalonia.Base/Metadata/DependsOnAttribute.cs b/src/Avalonia.Base/Metadata/DependsOnAttribute.cs index a719d33d59..92c6a58170 100644 --- a/src/Avalonia.Base/Metadata/DependsOnAttribute.cs +++ b/src/Avalonia.Base/Metadata/DependsOnAttribute.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Metadata diff --git a/src/Avalonia.Base/Metadata/TemplateContent.cs b/src/Avalonia.Base/Metadata/TemplateContent.cs index ecc16b04a2..fcd7d69e7b 100644 --- a/src/Avalonia.Base/Metadata/TemplateContent.cs +++ b/src/Avalonia.Base/Metadata/TemplateContent.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Metadata diff --git a/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs b/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs index d6ff2d8b84..d43fa55f5c 100644 --- a/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs +++ b/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Metadata diff --git a/src/Avalonia.Base/Platform/IAssetLoader.cs b/src/Avalonia.Base/Platform/IAssetLoader.cs index 4356fac4c0..c2e422cabb 100644 --- a/src/Avalonia.Base/Platform/IAssetLoader.cs +++ b/src/Avalonia.Base/Platform/IAssetLoader.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.IO; diff --git a/src/Avalonia.Base/Platform/IMacOSTopLevelPlatformHandle.cs b/src/Avalonia.Base/Platform/IMacOSTopLevelPlatformHandle.cs new file mode 100644 index 0000000000..e399976bbe --- /dev/null +++ b/src/Avalonia.Base/Platform/IMacOSTopLevelPlatformHandle.cs @@ -0,0 +1,12 @@ +using System; + +namespace Avalonia.Platform +{ + public interface IMacOSTopLevelPlatformHandle + { + IntPtr NSView { get; } + IntPtr GetNSViewRetained(); + IntPtr NSWindow { get; } + IntPtr GetNSWindowRetained(); + } +} diff --git a/src/Avalonia.Base/Platform/IPlatformHandle.cs b/src/Avalonia.Base/Platform/IPlatformHandle.cs index 39c34e97fd..a3b9d8f07c 100644 --- a/src/Avalonia.Base/Platform/IPlatformHandle.cs +++ b/src/Avalonia.Base/Platform/IPlatformHandle.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Platform diff --git a/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs b/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs index 9f5417ca95..2137f965cc 100644 --- a/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Threading; using Avalonia.Threading; diff --git a/src/Avalonia.Base/Platform/PlatformHandle.cs b/src/Avalonia.Base/Platform/PlatformHandle.cs index 24cc8f2651..bed25a3b5b 100644 --- a/src/Avalonia.Base/Platform/PlatformHandle.cs +++ b/src/Avalonia.Base/Platform/PlatformHandle.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Platform diff --git a/src/Avalonia.Base/PriorityBindingEntry.cs b/src/Avalonia.Base/PriorityBindingEntry.cs deleted file mode 100644 index 7f5415c2d8..0000000000 --- a/src/Avalonia.Base/PriorityBindingEntry.cs +++ /dev/null @@ -1,160 +0,0 @@ -// 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 System; -using System.Runtime.ExceptionServices; -using Avalonia.Data; -using Avalonia.Threading; - -namespace Avalonia -{ - /// - /// A registered binding in a . - /// - internal class PriorityBindingEntry : IDisposable, IObserver - { - private readonly PriorityLevel _owner; - private IDisposable _subscription; - - /// - /// Initializes a new instance of the class. - /// - /// The owner. - /// - /// The binding index. Later bindings should have higher indexes. - /// - public PriorityBindingEntry(PriorityLevel owner, int index) - { - _owner = owner; - Index = index; - } - - /// - /// Gets the observable associated with the entry. - /// - public IObservable Observable { get; private set; } - - /// - /// Gets a description of the binding. - /// - public string Description - { - get; - private set; - } - - /// - /// Gets the binding entry index. Later bindings will have higher indexes. - /// - public int Index - { - get; - } - - /// - /// Gets a value indicating whether the binding has completed. - /// - public bool HasCompleted { get; private set; } - - /// - /// The current value of the binding. - /// - public object Value - { - get; - private set; - } - - /// - /// Starts listening to the binding. - /// - /// The binding. - public void Start(IObservable binding) - { - Contract.Requires(binding != null); - - if (_subscription != null) - { - throw new Exception("PriorityValue.Entry.Start() called more than once."); - } - - Observable = binding; - Value = AvaloniaProperty.UnsetValue; - - if (binding is IDescription) - { - Description = ((IDescription)binding).Description; - } - - _subscription = binding.Subscribe(this); - } - - /// - /// Ends the binding subscription. - /// - public void Dispose() - { - _subscription?.Dispose(); - } - - void IObserver.OnNext(object value) - { - void Signal(PriorityBindingEntry instance, object newValue) - { - var notification = newValue as BindingNotification; - - if (notification != null) - { - if (notification.HasValue || notification.ErrorType == BindingErrorType.Error) - { - instance.Value = notification.Value; - instance._owner.Changed(instance); - } - - if (notification.ErrorType != BindingErrorType.None) - { - instance._owner.Error(instance, notification); - } - } - else - { - instance.Value = newValue; - instance._owner.Changed(instance); - } - } - - if (Dispatcher.UIThread.CheckAccess()) - { - Signal(this, value); - } - else - { - // To avoid allocating closure in the outer scope we need to capture variables - // locally. This allows us to skip most of the allocations when on UI thread. - var instance = this; - var newValue = value; - - Dispatcher.UIThread.Post(() => Signal(instance, newValue)); - } - } - - void IObserver.OnCompleted() - { - HasCompleted = true; - - if (Dispatcher.UIThread.CheckAccess()) - { - _owner.Completed(this); - } - else - { - Dispatcher.UIThread.Post(() => _owner.Completed(this)); - } - } - - void IObserver.OnError(Exception error) - { - ExceptionDispatchInfo.Capture(error).Throw(); - } - } -} diff --git a/src/Avalonia.Base/PriorityLevel.cs b/src/Avalonia.Base/PriorityLevel.cs deleted file mode 100644 index a2364083ea..0000000000 --- a/src/Avalonia.Base/PriorityLevel.cs +++ /dev/null @@ -1,227 +0,0 @@ -// 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 System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Threading; -using Avalonia.Data; - -namespace Avalonia -{ - /// - /// Stores bindings for a priority level in a . - /// - /// - /// - /// Each priority level in a has a current , - /// a list of and a . When there are no - /// bindings present, or all bindings return then - /// Value will equal DirectValue. - /// - /// - /// When there are bindings present, then the latest added binding that doesn't return - /// UnsetValue will take precedence. The active binding is returned by the - /// property (which refers to the active binding's - /// property rather than the index in - /// Bindings). - /// - /// - /// If DirectValue is set while a binding is active, then it will replace the - /// current value until the active binding fires again. - /// - /// - internal class PriorityLevel - { - private object _directValue; - private int _nextIndex; - - /// - /// Initializes a new instance of the class. - /// - /// The owner. - /// The priority. - public PriorityLevel( - PriorityValue owner, - int priority) - { - Contract.Requires(owner != null); - - Owner = owner; - Priority = priority; - Value = _directValue = AvaloniaProperty.UnsetValue; - ActiveBindingIndex = -1; - Bindings = new LinkedList(); - } - - /// - /// Gets the owner of the level. - /// - public PriorityValue Owner { get; } - - /// - /// Gets the priority of this level. - /// - public int Priority { get; } - - /// - /// Gets or sets the direct value for this priority level. - /// - public object DirectValue - { - get - { - return _directValue; - } - - set - { - Value = _directValue = value; - Owner.LevelValueChanged(this); - } - } - - /// - /// Gets the current binding for the priority level. - /// - public object Value { get; private set; } - - /// - /// Gets the value of the active binding, or -1 - /// if no binding is active. - /// - public int ActiveBindingIndex { get; private set; } - - /// - /// Gets the bindings for the priority level. - /// - public LinkedList Bindings { get; } - - /// - /// Adds a binding. - /// - /// The binding to add. - /// A disposable used to remove the binding. - public IDisposable Add(IObservable binding) - { - Contract.Requires(binding != null); - - var entry = new PriorityBindingEntry(this, _nextIndex++); - var node = Bindings.AddFirst(entry); - - entry.Start(binding); - - return new RemoveBindingDisposable(node, Bindings, this); - } - - /// - /// Invoked when an entry in changes value. - /// - /// The entry that changed. - public void Changed(PriorityBindingEntry entry) - { - if (entry.Index >= ActiveBindingIndex) - { - if (entry.Value != AvaloniaProperty.UnsetValue) - { - Value = entry.Value; - ActiveBindingIndex = entry.Index; - Owner.LevelValueChanged(this); - } - else - { - ActivateFirstBinding(); - } - } - } - - /// - /// Invoked when an entry in completes. - /// - /// The entry that completed. - public void Completed(PriorityBindingEntry entry) - { - Bindings.Remove(entry); - - if (entry.Index >= ActiveBindingIndex) - { - ActivateFirstBinding(); - } - } - - /// - /// Invoked when an entry in encounters a recoverable error. - /// - /// The entry that completed. - /// The error. - public void Error(PriorityBindingEntry entry, BindingNotification error) - { - Owner.LevelError(this, error); - } - - /// - /// Activates the first binding that has a value. - /// - private void ActivateFirstBinding() - { - foreach (var binding in Bindings) - { - if (binding.Value != AvaloniaProperty.UnsetValue) - { - Value = binding.Value; - ActiveBindingIndex = binding.Index; - Owner.LevelValueChanged(this); - return; - } - } - - Value = DirectValue; - ActiveBindingIndex = -1; - Owner.LevelValueChanged(this); - } - - private sealed class RemoveBindingDisposable : IDisposable - { - private readonly LinkedList _bindings; - private readonly PriorityLevel _priorityLevel; - private LinkedListNode _binding; - - public RemoveBindingDisposable( - LinkedListNode binding, - LinkedList bindings, - PriorityLevel priorityLevel) - { - _binding = binding; - _bindings = bindings; - _priorityLevel = priorityLevel; - } - - public void Dispose() - { - LinkedListNode binding = Interlocked.Exchange(ref _binding, null); - - if (binding == null) - { - // Some system is trying to remove binding twice. - Debug.Assert(false); - - return; - } - - PriorityBindingEntry entry = binding.Value; - - if (!entry.HasCompleted) - { - _bindings.Remove(binding); - - entry.Dispose(); - - if (entry.Index >= _priorityLevel.ActiveBindingIndex) - { - _priorityLevel.ActivateFirstBinding(); - } - } - } - } - } -} diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs deleted file mode 100644 index 61184ef7b1..0000000000 --- a/src/Avalonia.Base/PriorityValue.cs +++ /dev/null @@ -1,315 +0,0 @@ -// 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 System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Avalonia.Data; -using Avalonia.Logging; -using Avalonia.Utilities; - -namespace Avalonia -{ - /// - /// Maintains a list of prioritized bindings together with a current value. - /// - /// - /// Bindings, in the form of s are added to the object using - /// the method. With the observable is passed a priority, where lower values - /// represent higher priorities. The current is selected from the highest - /// priority binding that doesn't return . Where there - /// are multiple bindings registered with the same priority, the most recently added binding - /// has a higher priority. Each time the value changes, the - /// method on the - /// owner object is fired with the old and new values. - /// - internal sealed class PriorityValue : ISetAndNotifyHandler<(object,int)> - { - private readonly Type _valueType; - private readonly SingleOrDictionary _levels = new SingleOrDictionary(); - private readonly Func _validate; - private (object value, int priority) _value; - private DeferredSetter _setter; - - /// - /// Initializes a new instance of the class. - /// - /// The owner of the object. - /// The property that the value represents. - /// The value type. - /// An optional validation function. - public PriorityValue( - IPriorityValueOwner owner, - AvaloniaProperty property, - Type valueType, - Func validate = null) - { - Owner = owner; - Property = property; - _valueType = valueType; - _value = (AvaloniaProperty.UnsetValue, int.MaxValue); - _validate = validate; - } - - /// - /// Gets a value indicating whether the property is animating. - /// - public bool IsAnimating - { - get - { - return ValuePriority <= (int)BindingPriority.Animation && - GetLevel(ValuePriority).ActiveBindingIndex != -1; - } - } - - /// - /// Gets the owner of the value. - /// - public IPriorityValueOwner Owner { get; } - - /// - /// Gets the property that the value represents. - /// - public AvaloniaProperty Property { get; } - - /// - /// Gets the current value. - /// - public object Value => _value.value; - - /// - /// Gets the priority of the binding that is currently active. - /// - public int ValuePriority => _value.priority; - - /// - /// Adds a new binding. - /// - /// The binding. - /// The binding priority. - /// - /// A disposable that will remove the binding. - /// - public IDisposable Add(IObservable binding, int priority) - { - return GetLevel(priority).Add(binding); - } - - /// - /// Sets the value for a specified priority. - /// - /// The value. - /// The priority - public void SetValue(object value, int priority) - { - GetLevel(priority).DirectValue = value; - } - - /// - /// Gets the currently active bindings on this object. - /// - /// An enumerable collection of bindings. - public IEnumerable GetBindings() - { - foreach (var level in _levels) - { - foreach (var binding in level.Value.Bindings) - { - yield return binding; - } - } - } - - /// - /// Returns diagnostic string that can help the user debug the bindings in effect on - /// this object. - /// - /// A diagnostic string. - public string GetDiagnostic() - { - var b = new StringBuilder(); - var first = true; - - foreach (var level in _levels) - { - if (!first) - { - b.AppendLine(); - } - - b.Append(ValuePriority == level.Key ? "*" : string.Empty); - b.Append("Priority "); - b.Append(level.Key); - b.Append(": "); - b.AppendLine(level.Value.Value?.ToString() ?? "(null)"); - b.AppendLine("--------"); - b.Append("Direct: "); - b.AppendLine(level.Value.DirectValue?.ToString() ?? "(null)"); - - foreach (var binding in level.Value.Bindings) - { - b.Append(level.Value.ActiveBindingIndex == binding.Index ? "*" : string.Empty); - b.Append(binding.Description ?? binding.Observable.GetType().Name); - b.Append(": "); - b.AppendLine(binding.Value?.ToString() ?? "(null)"); - } - - first = false; - } - - return b.ToString(); - } - - /// - /// Called when the value for a priority level changes. - /// - /// The priority level of the changed entry. - public void LevelValueChanged(PriorityLevel level) - { - if (level.Priority <= ValuePriority) - { - if (level.Value != AvaloniaProperty.UnsetValue) - { - UpdateValue(level.Value, level.Priority); - } - else - { - foreach (var i in _levels.Values.OrderBy(x => x.Priority)) - { - if (i.Value != AvaloniaProperty.UnsetValue) - { - UpdateValue(i.Value, i.Priority); - return; - } - } - - UpdateValue(AvaloniaProperty.UnsetValue, int.MaxValue); - } - } - } - - /// - /// Called when a priority level encounters an error. - /// - /// The priority level of the changed entry. - /// The binding error. - public void LevelError(PriorityLevel level, BindingNotification error) - { - Owner.LogError(Property, error.Error); - } - - /// - /// Causes a revalidation of the value. - /// - public void Revalidate() - { - if (_validate != null) - { - PriorityLevel level; - - if (_levels.TryGetValue(ValuePriority, out level)) - { - UpdateValue(level.Value, level.Priority); - } - } - } - - /// - /// Gets the with the specified priority, creating it if it - /// doesn't already exist. - /// - /// The priority. - /// The priority level. - private PriorityLevel GetLevel(int priority) - { - PriorityLevel result; - - if (!_levels.TryGetValue(priority, out result)) - { - result = new PriorityLevel(this, priority); - _levels.Add(priority, result); - } - - return result; - } - - /// - /// Updates the current and notifies all subscribers. - /// - /// The value to set. - /// The priority level that the value came from. - private void UpdateValue(object value, int priority) - { - var newValue = (value, priority); - - if (newValue == _value) - { - return; - } - - if (_setter == null) - { - _setter = Owner.GetNonDirectDeferredSetter(Property); - } - - _setter.SetAndNotifyCallback(Property, this, ref _value, newValue); - } - - void ISetAndNotifyHandler<(object, int)>.HandleSetAndNotify(AvaloniaProperty property, ref (object, int) backing, (object, int) value) - { - SetAndNotify(ref backing, value); - } - - private void SetAndNotify(ref (object value, int priority) backing, (object value, int priority) update) - { - var val = update.value; - var notification = val as BindingNotification; - object castValue; - - if (notification != null) - { - val = (notification.HasValue) ? notification.Value : null; - } - - if (TypeUtilities.TryConvertImplicit(_valueType, val, out castValue)) - { - var old = backing.value; - - if (_validate != null && castValue != AvaloniaProperty.UnsetValue) - { - castValue = _validate(castValue); - } - - backing = (castValue, update.priority); - - if (notification?.HasValue == true) - { - notification.SetValue(castValue); - } - - if (notification == null || notification.HasValue) - { - Owner?.Changed(Property, ValuePriority, old, Value); - } - - if (notification != null) - { - Owner?.BindingNotificationReceived(Property, notification); - } - } - else - { - Logger.TryGet(LogEventLevel.Error)?.Log( - LogArea.Binding, - Owner, - "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", - Property.Name, - _valueType, - val, - val?.GetType()); - } - } - } -} 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/PropertyMetadata.cs b/src/Avalonia.Base/PropertyMetadata.cs index bec562354d..806051e1d1 100644 --- a/src/Avalonia.Base/PropertyMetadata.cs +++ b/src/Avalonia.Base/PropertyMetadata.cs @@ -1,6 +1,3 @@ -// 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; namespace Avalonia diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs new file mode 100644 index 0000000000..0d563947e7 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs @@ -0,0 +1,111 @@ +using System; +using Avalonia.Data; +using Avalonia.Threading; + +#nullable enable + +namespace Avalonia.PropertyStore +{ + /// + /// Represents an untyped interface to . + /// + internal interface IBindingEntry : IPriorityValueEntry, IDisposable + { + } + + /// + /// Stores a binding in a or . + /// + /// The property type. + internal class BindingEntry : IBindingEntry, IPriorityValueEntry, IObserver> + { + private readonly IAvaloniaObject _owner; + private IValueSink _sink; + private IDisposable? _subscription; + private Optional _value; + + public BindingEntry( + IAvaloniaObject owner, + StyledPropertyBase property, + IObservable> source, + BindingPriority priority, + IValueSink sink) + { + _owner = owner; + Property = property; + Source = source; + Priority = priority; + _sink = sink; + } + + public StyledPropertyBase Property { get; } + public BindingPriority Priority { get; } + public IObservable> Source { get; } + Optional IValue.GetValue() => _value.ToObject(); + + public Optional GetValue(BindingPriority maxPriority) + { + return Priority >= maxPriority ? _value : Optional.Empty; + } + + public void Dispose() + { + _subscription?.Dispose(); + _subscription = null; + _sink.Completed(Property, this, _value); + } + + public void OnCompleted() => _sink.Completed(Property, this, _value); + + public void OnError(Exception error) + { + throw new NotImplementedException(); + } + + public void OnNext(BindingValue value) + { + if (Dispatcher.UIThread.CheckAccess()) + { + UpdateValue(value); + } + else + { + // To avoid allocating closure in the outer scope we need to capture variables + // locally. This allows us to skip most of the allocations when on UI thread. + var instance = this; + var newValue = value; + + Dispatcher.UIThread.Post(() => instance.UpdateValue(newValue)); + } + } + + public void Start() + { + _subscription = Source.Subscribe(this); + } + + public void Reparent(IValueSink sink) => _sink = sink; + + private void UpdateValue(BindingValue value) + { + if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false) + { + value = Property.GetDefaultValue(_owner.GetType()); + } + + if (value.Type == BindingValueType.DoNothing) + { + return; + } + + var old = _value; + + if (value.Type != BindingValueType.DataValidationError) + { + _value = value.ToOptional(); + } + + _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs(_owner, Property, old, value, Priority)); + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs new file mode 100644 index 0000000000..46f6f9a137 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs @@ -0,0 +1,42 @@ +using System; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.PropertyStore +{ + /// + /// Stores a value with a priority in a or + /// . + /// + /// The property type. + internal class ConstantValueEntry : IPriorityValueEntry, IDisposable + { + private IValueSink _sink; + private Optional _value; + + public ConstantValueEntry( + StyledPropertyBase property, + T value, + BindingPriority priority, + IValueSink sink) + { + Property = property; + _value = value; + Priority = priority; + _sink = sink; + } + + public StyledPropertyBase Property { get; } + public BindingPriority Priority { get; } + Optional IValue.GetValue() => _value.ToObject(); + + public Optional GetValue(BindingPriority maxPriority = BindingPriority.Animation) + { + return Priority >= maxPriority ? _value : Optional.Empty; + } + + public void Dispose() => _sink.Completed(Property, this, _value); + public void Reparent(IValueSink sink) => _sink = sink; + } +} diff --git a/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs new file mode 100644 index 0000000000..6ed6c2ef52 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs @@ -0,0 +1,25 @@ +using System; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.PropertyStore +{ + /// + /// Represents an untyped interface to . + /// + internal interface IPriorityValueEntry : IValue + { + BindingPriority Priority { get; } + + void Reparent(IValueSink sink); + } + + /// + /// Represents an object that can act as an entry in a . + /// + /// The property type. + internal interface IPriorityValueEntry : IPriorityValueEntry, IValue + { + } +} diff --git a/src/Avalonia.Base/PropertyStore/IValue.cs b/src/Avalonia.Base/PropertyStore/IValue.cs new file mode 100644 index 0000000000..249cfc360c --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/IValue.cs @@ -0,0 +1,24 @@ +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.PropertyStore +{ + /// + /// Represents an untyped interface to . + /// + internal interface IValue + { + Optional GetValue(); + BindingPriority Priority { get; } + } + + /// + /// Represents an object that can act as an entry in a . + /// + /// The property type. + internal interface IValue : IValue + { + Optional GetValue(BindingPriority maxPriority = BindingPriority.Animation); + } +} diff --git a/src/Avalonia.Base/PropertyStore/IValueSink.cs b/src/Avalonia.Base/PropertyStore/IValueSink.cs new file mode 100644 index 0000000000..3a1e9731d8 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/IValueSink.cs @@ -0,0 +1,19 @@ +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.PropertyStore +{ + /// + /// Represents an entity that can receive change notifications in a . + /// + internal interface IValueSink + { + void ValueChanged(AvaloniaPropertyChangedEventArgs change); + + void Completed( + StyledPropertyBase property, + IPriorityValueEntry entry, + Optional oldValue); + } +} diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs new file mode 100644 index 0000000000..59c017bc09 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs @@ -0,0 +1,27 @@ +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.PropertyStore +{ + /// + /// Stores a value with local value priority in a or + /// . + /// + /// The property type. + internal class LocalValueEntry : IValue + { + private T _value; + + public LocalValueEntry(T value) => _value = value; + public BindingPriority Priority => BindingPriority.LocalValue; + Optional IValue.GetValue() => new Optional(_value); + + public Optional GetValue(BindingPriority maxPriority) + { + return BindingPriority.LocalValue >= maxPriority ? _value : Optional.Empty; + } + + public void SetValue(T value) => _value = value; + } +} diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs new file mode 100644 index 0000000000..5e223cad60 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.PropertyStore +{ + /// + /// Stores a set of prioritized values and bindings in a . + /// + /// The property type. + /// + /// When more than a single value or binding is applied to a property in an + /// , the entry in the is converted into + /// a . This class holds any number of + /// entries (sorted first by priority and then in the order + /// they were added) plus a local value. + /// + internal class PriorityValue : IValue, IValueSink + { + private readonly IAvaloniaObject _owner; + private readonly IValueSink _sink; + private readonly List> _entries = new List>(); + private readonly Func? _coerceValue; + private Optional _localValue; + private Optional _value; + + public PriorityValue( + IAvaloniaObject owner, + StyledPropertyBase property, + IValueSink sink) + { + _owner = owner; + Property = property; + _sink = sink; + + if (property.HasCoercion) + { + var metadata = property.GetMetadata(owner.GetType()); + _coerceValue = metadata.CoerceValue; + } + } + + public PriorityValue( + IAvaloniaObject owner, + StyledPropertyBase property, + IValueSink sink, + IPriorityValueEntry existing) + : this(owner, property, sink) + { + existing.Reparent(this); + _entries.Add(existing); + + var v = existing.GetValue(); + + if (v.HasValue) + { + _value = v; + Priority = existing.Priority; + } + } + + public PriorityValue( + IAvaloniaObject owner, + StyledPropertyBase property, + IValueSink sink, + LocalValueEntry existing) + : this(owner, property, sink) + { + _value = _localValue = existing.GetValue(BindingPriority.LocalValue); + Priority = BindingPriority.LocalValue; + } + + public StyledPropertyBase Property { get; } + public BindingPriority Priority { get; private set; } = BindingPriority.Unset; + public IReadOnlyList> Entries => _entries; + Optional IValue.GetValue() => _value.ToObject(); + + public void ClearLocalValue() + { + UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( + _owner, + Property, + default, + default, + BindingPriority.LocalValue)); + } + + public Optional GetValue(BindingPriority maxPriority = BindingPriority.Animation) + { + if (Priority == BindingPriority.Unset) + { + return default; + } + + if (Priority >= maxPriority) + { + return _value; + } + + return CalculateValue(maxPriority).Item1; + } + + public IDisposable? SetValue(T value, BindingPriority priority) + { + IDisposable? result = null; + + if (priority == BindingPriority.LocalValue) + { + _localValue = value; + } + else + { + var insert = FindInsertPoint(priority); + var entry = new ConstantValueEntry(Property, value, priority, this); + _entries.Insert(insert, entry); + result = entry; + } + + UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( + _owner, + Property, + default, + value, + priority)); + + return result; + } + + public BindingEntry AddBinding(IObservable> source, BindingPriority priority) + { + var binding = new BindingEntry(_owner, Property, source, priority, this); + var insert = FindInsertPoint(binding.Priority); + _entries.Insert(insert, binding); + return binding; + } + + public void CoerceValue() => UpdateEffectiveValue(null); + + void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Priority == BindingPriority.LocalValue) + { + _localValue = default; + } + + if (change is AvaloniaPropertyChangedEventArgs c) + { + UpdateEffectiveValue(c); + } + } + + void IValueSink.Completed( + StyledPropertyBase property, + IPriorityValueEntry entry, + Optional oldValue) + { + _entries.Remove((IPriorityValueEntry)entry); + + if (oldValue is Optional o) + { + UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( + _owner, + Property, + o, + default, + entry.Priority)); + } + } + + private int FindInsertPoint(BindingPriority priority) + { + var result = _entries.Count; + + for (var i = 0; i < _entries.Count; ++i) + { + if (_entries[i].Priority < priority) + { + result = i; + break; + } + } + + return result; + } + + public (Optional, BindingPriority) CalculateValue(BindingPriority maxPriority) + { + var reachedLocalValues = false; + + for (var i = _entries.Count - 1; i >= 0; --i) + { + var entry = _entries[i]; + + if (entry.Priority < maxPriority) + { + continue; + } + + if (!reachedLocalValues && + entry.Priority >= BindingPriority.LocalValue && + maxPriority <= BindingPriority.LocalValue && + _localValue.HasValue) + { + return (_localValue, BindingPriority.LocalValue); + } + + var entryValue = entry.GetValue(); + + if (entryValue.HasValue) + { + return (entryValue, entry.Priority); + } + } + + if (!reachedLocalValues && + maxPriority <= BindingPriority.LocalValue && + _localValue.HasValue) + { + return (_localValue, BindingPriority.LocalValue); + } + + return (default, BindingPriority.Unset); + } + + private void UpdateEffectiveValue(AvaloniaPropertyChangedEventArgs? change) + { + var (value, priority) = CalculateValue(BindingPriority.Animation); + + if (value.HasValue && _coerceValue != null) + { + value = _coerceValue(_owner, value.Value); + } + + Priority = priority; + + if (value != _value) + { + var old = _value; + _value = value; + + _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + _owner, + Property, + old, + value, + Priority)); + } + else if (change is object) + { + change.MarkNonEffectiveValue(); + change.SetOldValue(default); + _sink.ValueChanged(change); + } + } + } +} diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs new file mode 100644 index 0000000000..be044b0559 --- /dev/null +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.Reactive +{ + internal class AvaloniaPropertyBindingObservable : LightweightObservableBase>, IDescription + { + private readonly WeakReference _target; + private readonly AvaloniaProperty _property; + private T _value; + +#nullable disable + public AvaloniaPropertyBindingObservable( + IAvaloniaObject target, + AvaloniaProperty property) + { + _target = new WeakReference(target); + _property = property; + } +#nullable enable + + public string Description => $"{_target.GetType().Name}.{_property.Name}"; + + protected override void Initialize() + { + if (_target.TryGetTarget(out var target)) + { + _value = (T)target.GetValue(_property); + target.PropertyChanged += PropertyChanged; + } + } + + protected override void Deinitialize() + { + if (_target.TryGetTarget(out var target)) + { + target.PropertyChanged -= PropertyChanged; + } + } + + protected override void Subscribed(IObserver> observer, bool first) + { + observer.OnNext(new BindingValue(_value)); + } + + private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + if (e is AvaloniaPropertyChangedEventArgs typedArgs) + { + var newValue = e.Sender.GetValue(typedArgs.Property); + + if (!typedArgs.OldValue.HasValue || !EqualityComparer.Default.Equals(newValue, _value)) + { + _value = newValue; + PublishNext(_value); + } + } + else + { + var newValue = e.Sender.GetValue(e.Property); + + if (!Equals(newValue, _value)) + { + _value = (T)newValue; + PublishNext(_value); + } + } + } + } + } +} diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs index 4385ab13ef..238aba5c96 100644 --- a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs @@ -44,8 +44,22 @@ namespace Avalonia.Reactive { if (e.Property == _property) { - _value = (T)e.NewValue; - PublishNext(_value); + T newValue; + + if (e is AvaloniaPropertyChangedEventArgs typed) + { + newValue = typed.Sender.GetValue(typed.Property); + } + else + { + newValue = (T)e.Sender.GetValue(e.Property); + } + + if (!Equals(newValue, _value)) + { + _value = (T)newValue; + PublishNext(_value); + } } } } diff --git a/src/Avalonia.Base/Reactive/BindingValueAdapter.cs b/src/Avalonia.Base/Reactive/BindingValueAdapter.cs new file mode 100644 index 0000000000..8c80e9f48c --- /dev/null +++ b/src/Avalonia.Base/Reactive/BindingValueAdapter.cs @@ -0,0 +1,61 @@ +using System; +using System.Reactive.Subjects; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.Reactive +{ + internal class BindingValueAdapter : SingleSubscriberObservableBase>, + IObserver + { + private readonly IObservable _source; + private IDisposable? _subscription; + + public BindingValueAdapter(IObservable source) => _source = source; + public void OnCompleted() => PublishCompleted(); + public void OnError(Exception error) => PublishError(error); + public void OnNext(T value) => PublishNext(BindingValue.FromUntyped(value)); + protected override void Subscribed() => _subscription = _source.Subscribe(this); + protected override void Unsubscribed() => _subscription?.Dispose(); + } + + internal class BindingValueSubjectAdapter : SingleSubscriberObservableBase>, + ISubject> + { + private readonly ISubject _source; + private readonly Inner _inner; + private IDisposable? _subscription; + + public BindingValueSubjectAdapter(ISubject source) + { + _source = source; + _inner = new Inner(this); + } + + public void OnCompleted() => _source.OnCompleted(); + public void OnError(Exception error) => _source.OnError(error); + + public void OnNext(BindingValue value) + { + if (value.HasValue) + { + _source.OnNext(value.Value); + } + } + + protected override void Subscribed() => _subscription = _source.Subscribe(_inner); + protected override void Unsubscribed() => _subscription?.Dispose(); + + private class Inner : IObserver + { + private readonly BindingValueSubjectAdapter _owner; + + public Inner(BindingValueSubjectAdapter owner) => _owner = owner; + + public void OnCompleted() => _owner.PublishCompleted(); + public void OnError(Exception error) => _owner.PublishError(error); + public void OnNext(T value) => _owner.PublishNext(BindingValue.FromUntyped(value)); + } + } +} diff --git a/src/Avalonia.Base/Reactive/BindingValueExtensions.cs b/src/Avalonia.Base/Reactive/BindingValueExtensions.cs new file mode 100644 index 0000000000..6f0d29dd0f --- /dev/null +++ b/src/Avalonia.Base/Reactive/BindingValueExtensions.cs @@ -0,0 +1,35 @@ +using System; +using System.Reactive.Subjects; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.Reactive +{ + public static class BindingValueExtensions + { + public static IObservable> ToBindingValue(this IObservable source) + { + source = source ?? throw new ArgumentNullException(nameof(source)); + return new BindingValueAdapter(source); + } + + public static ISubject> ToBindingValue(this ISubject source) + { + source = source ?? throw new ArgumentNullException(nameof(source)); + return new BindingValueSubjectAdapter(source); + } + + public static IObservable ToUntyped(this IObservable> source) + { + source = source ?? throw new ArgumentNullException(nameof(source)); + return new UntypedBindingAdapter(source); + } + + public static ISubject ToUntyped(this ISubject> source) + { + source = source ?? throw new ArgumentNullException(nameof(source)); + return new UntypedBindingSubjectAdapter(source); + } + } +} diff --git a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs index 41009e4cd3..f5052e5858 100644 --- a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs +++ b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs @@ -116,20 +116,33 @@ namespace Avalonia.Reactive { if (Volatile.Read(ref _observers) != null) { - IObserver[] observers; - + IObserver[] observers = null; + IObserver singleObserver = null; lock (this) { if (_observers == null) { return; } - observers = _observers.ToArray(); + if (_observers.Count == 1) + { + singleObserver = _observers[0]; + } + else + { + observers = _observers.ToArray(); + } } - - foreach (var observer in observers) + if (singleObserver != null) { - observer.OnNext(value); + singleObserver.OnNext(value); + } + else + { + foreach (var observer in observers) + { + observer.OnNext(value); + } } } } diff --git a/src/Avalonia.Base/Reactive/ObservableEx.cs b/src/Avalonia.Base/Reactive/ObservableEx.cs index a1ec8f9a8a..8e963fb9d5 100644 --- a/src/Avalonia.Base/Reactive/ObservableEx.cs +++ b/src/Avalonia.Base/Reactive/ObservableEx.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Reactive.Disposables; diff --git a/src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs b/src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs index cd8ce2cd80..d3ac3dac8a 100644 --- a/src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs +++ b/src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs @@ -36,7 +36,7 @@ namespace Avalonia.Reactive return this; } - void IDisposable.Dispose() + public virtual void Dispose() { Unsubscribed(); _observer = null; diff --git a/src/Avalonia.Base/Reactive/TypedBindingAdapter.cs b/src/Avalonia.Base/Reactive/TypedBindingAdapter.cs new file mode 100644 index 0000000000..b99cef0f51 --- /dev/null +++ b/src/Avalonia.Base/Reactive/TypedBindingAdapter.cs @@ -0,0 +1,62 @@ +using System; +using Avalonia.Data; +using Avalonia.Logging; + +#nullable enable + +namespace Avalonia.Reactive +{ + internal class TypedBindingAdapter : SingleSubscriberObservableBase>, + IObserver> + { + private readonly IAvaloniaObject _target; + private readonly AvaloniaProperty _property; + private readonly IObservable> _source; + private IDisposable? _subscription; + + public TypedBindingAdapter( + IAvaloniaObject target, + AvaloniaProperty property, + IObservable> source) + { + _target = target; + _property = property; + _source = source; + } + + public void OnNext(BindingValue value) + { + try + { + PublishNext(value.Convert()); + } + catch (InvalidCastException e) + { + Logger.TryGet(LogEventLevel.Error, LogArea.Binding)?.Log( + _target, + "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", + _property.Name, + _property.PropertyType, + value.Value, + value.Value?.GetType()); + PublishNext(BindingValue.BindingError(e)); + } + } + + public void OnCompleted() => PublishCompleted(); + public void OnError(Exception error) => PublishError(error); + + public static IObservable> Create( + IAvaloniaObject target, + AvaloniaProperty property, + IObservable> source) + { + return source is IObservable> result ? + result : + new TypedBindingAdapter(target, property, source); + } + + protected override void Subscribed() => _subscription = _source.Subscribe(this); + protected override void Unsubscribed() => _subscription?.Dispose(); + } +} diff --git a/src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs b/src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs new file mode 100644 index 0000000000..03c1afcea9 --- /dev/null +++ b/src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs @@ -0,0 +1,57 @@ +using System; +using System.Reactive.Subjects; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.Reactive +{ + internal class UntypedBindingAdapter : SingleSubscriberObservableBase, + IObserver> + { + private readonly IObservable> _source; + private IDisposable? _subscription; + + public UntypedBindingAdapter(IObservable> source) => _source = source; + public void OnCompleted() => PublishCompleted(); + public void OnError(Exception error) => PublishError(error); + public void OnNext(BindingValue value) => value.ToUntyped(); + protected override void Subscribed() => _subscription = _source.Subscribe(this); + protected override void Unsubscribed() => _subscription?.Dispose(); + } + + internal class UntypedBindingSubjectAdapter : SingleSubscriberObservableBase, + ISubject + { + private readonly ISubject> _source; + private readonly Inner _inner; + private IDisposable? _subscription; + + public UntypedBindingSubjectAdapter(ISubject> source) + { + _source = source; + _inner = new Inner(this); + } + + public void OnCompleted() => _source.OnCompleted(); + public void OnError(Exception error) => _source.OnError(error); + public void OnNext(object? value) + { + _source.OnNext(BindingValue.FromUntyped(value)); + } + + protected override void Subscribed() => _subscription = _source.Subscribe(_inner); + protected override void Unsubscribed() => _subscription?.Dispose(); + + private class Inner : IObserver> + { + private readonly UntypedBindingSubjectAdapter _owner; + + public Inner(UntypedBindingSubjectAdapter owner) => _owner = owner; + + public void OnCompleted() => _owner.PublishCompleted(); + public void OnError(Exception error) => _owner.PublishError(error); + public void OnNext(BindingValue value) => _owner.PublishNext(value.ToUntyped()); + } + } +} diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index 4eb85a046e..75275858cd 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia @@ -17,14 +14,16 @@ namespace Avalonia /// The type of the class that registers the property. /// The property metadata. /// Whether the property inherits its value. + /// A value validation callback. /// A callback. public StyledProperty( string name, Type ownerType, StyledPropertyMetadata metadata, bool inherits = false, + Func validate = null, Action notifying = null) - : base(name, ownerType, metadata, inherits, notifying) + : base(name, ownerType, metadata, inherits, validate, notifying) { } diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index 27a502246a..3e92c3bdf7 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -1,15 +1,15 @@ -// 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 System; using System.Diagnostics; +using Avalonia.Data; +using Avalonia.Reactive; +using Avalonia.Utilities; namespace Avalonia { /// /// Base class for styled properties. /// - public class StyledPropertyBase : AvaloniaProperty, IStyledPropertyAccessor + public abstract class StyledPropertyBase : AvaloniaProperty, IStyledPropertyAccessor { private bool _inherits; @@ -20,12 +20,14 @@ namespace Avalonia /// The type of the class that registers the property. /// The property metadata. /// Whether the property inherits its value. + /// A value validation callback. /// A callback. protected StyledPropertyBase( string name, Type ownerType, StyledPropertyMetadata metadata, bool inherits = false, + Func validate = null, Action notifying = null) : base(name, ownerType, metadata, notifying) { @@ -38,6 +40,14 @@ namespace Avalonia } _inherits = inherits; + ValidateValue = validate; + HasCoercion |= metadata.CoerceValue != null; + + if (validate?.Invoke(metadata.DefaultValue) == false) + { + throw new ArgumentException( + $"'{metadata.DefaultValue}' is not a valid default value for '{name}'."); + } } /// @@ -59,6 +69,29 @@ namespace Avalonia /// public override bool Inherits => _inherits; + /// + /// Gets the value validation callback for the property. + /// + public Func ValidateValue { get; } + + /// + /// Gets a value indicating whether this property has any value coercion callbacks defined + /// in its metadata. + /// + internal bool HasCoercion { get; private set; } + + public TValue CoerceValue(IAvaloniaObject instance, TValue baseValue) + { + var metadata = GetMetadata(instance.GetType()); + + if (metadata.CoerceValue != null) + { + return metadata.CoerceValue.Invoke(instance, baseValue); + } + + return baseValue; + } + /// /// Gets the default value for the property on the specified type. /// @@ -68,7 +101,7 @@ namespace Avalonia { Contract.Requires(type != null); - return GetMetadata(type).DefaultValue.Typed; + return GetMetadata(type).DefaultValue; } /// @@ -120,31 +153,24 @@ namespace Avalonia /// The metadata. public void OverrideMetadata(Type type, StyledPropertyMetadata metadata) { + if (ValidateValue != null) + { + if (!ValidateValue(metadata.DefaultValue)) + { + throw new ArgumentException( + $"'{metadata.DefaultValue}' is not a valid default value for '{Name}'."); + } + } + + HasCoercion |= metadata.CoerceValue != null; + base.OverrideMetadata(type, metadata); } - /// - /// Overrides the validation function for the specified type. - /// - /// The type. - /// The validation function. - public void OverrideValidation(Func validate) - where THost : IAvaloniaObject + /// + public override void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) { - Func f; - - if (validate != null) - { - f = Cast(validate); - } - else - { - // Passing null to the validation function means that the property metadata merge - // will take the base validation function, so instead use an empty validation. - f = (o, v) => v; - } - - base.OverrideMetadata(typeof(THost), new StyledPropertyMetadata(validate: f)); + vistor.Visit(this, ref data); } /// @@ -157,20 +183,74 @@ namespace Avalonia } /// - Func IStyledPropertyAccessor.GetValidationFunc(Type type) + object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); + + /// + internal override void RouteClearValue(IAvaloniaObject o) { - Contract.Requires(type != null); - return ((IStyledPropertyMetadata)base.GetMetadata(type)).Validate; + o.ClearValue(this); } /// - object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); + internal override object RouteGetValue(IAvaloniaObject o) + { + return o.GetValue(this); + } + + /// + internal override object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) + { + var value = o.GetBaseValue(this, maxPriority); + return value.HasValue ? value.Value : AvaloniaProperty.UnsetValue; + } + + /// + internal override IDisposable RouteSetValue( + IAvaloniaObject o, + object value, + BindingPriority priority) + { + var v = TryConvert(value); + + if (v.HasValue) + { + return o.SetValue(this, (TValue)v.Value, priority); + } + else if (v.Type == BindingValueType.UnsetValue) + { + o.ClearValue(this); + } + else if (v.HasError) + { + throw v.Error; + } + + return null; + } + + /// + internal override IDisposable RouteBind( + IAvaloniaObject o, + IObservable> source, + BindingPriority priority) + { + var adapter = TypedBindingAdapter.Create(o, this, source); + return o.Bind(this, adapter, priority); + } + + /// + internal override void RouteInheritanceParentChanged( + AvaloniaObject o, + IAvaloniaObject oldParent) + { + o.InheritanceParentChanged(this, oldParent); + } private object GetDefaultBoxedValue(Type type) { Contract.Requires(type != null); - return GetMetadata(type).DefaultValue.Boxed; + return GetMetadata(type).DefaultValue; } [DebuggerHidden] diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index d1a0e2dc53..300548db0a 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Diagnostics; using Avalonia.Data; @@ -12,35 +9,35 @@ namespace Avalonia /// public class StyledPropertyMetadata : PropertyMetadata, IStyledPropertyMetadata { + private Optional _defaultValue; + /// /// Initializes a new instance of the class. /// /// The default value of the property. - /// A validation function. /// The default binding mode. + /// A value coercion callback. public StyledPropertyMetadata( - TValue defaultValue = default, - Func validate = null, - BindingMode defaultBindingMode = BindingMode.Default) + Optional defaultValue = default, + BindingMode defaultBindingMode = BindingMode.Default, + Func coerce = null) : base(defaultBindingMode) { - DefaultValue = new BoxedValue(defaultValue); - Validate = validate; + _defaultValue = defaultValue; + CoerceValue = coerce; } /// /// Gets the default value for the property. /// - internal BoxedValue DefaultValue { get; private set; } + public TValue DefaultValue => _defaultValue.GetValueOrDefault(); /// - /// Gets the validation callback. + /// Gets the value coercion callback, if any. /// - public Func Validate { get; private set; } - - object IStyledPropertyMetadata.DefaultValue => DefaultValue.Boxed; + public Func? CoerceValue { get; private set; } - Func IStyledPropertyMetadata.Validate => Cast(Validate); + object IStyledPropertyMetadata.DefaultValue => DefaultValue; /// public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty property) @@ -49,29 +46,16 @@ namespace Avalonia if (baseMetadata is StyledPropertyMetadata src) { - if (DefaultValue.Boxed == null) + if (!_defaultValue.HasValue) { - DefaultValue = src.DefaultValue; + _defaultValue = src.DefaultValue; } - if (Validate == null) + if (CoerceValue == null) { - Validate = src.Validate; + CoerceValue = src.CoerceValue; } } } - - [DebuggerHidden] - private static Func Cast(Func f) - { - if (f == null) - { - return null; - } - else - { - return (o, v) => f(o, (TValue)v); - } - } } } diff --git a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs index 46529f0a5a..0bfc713ba0 100644 --- a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs +++ b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Reactive.Concurrency; using System.Reactive.Disposables; diff --git a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs index 278efbcd0d..38a23f918f 100644 --- a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs +++ b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs @@ -1,6 +1,3 @@ -// 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 System.Threading; namespace Avalonia.Threading diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index 55a9b6984a..fe2cec11f0 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Threading; using System.Threading.Tasks; diff --git a/src/Avalonia.Base/Threading/DispatcherPriority.cs b/src/Avalonia.Base/Threading/DispatcherPriority.cs index 1faa2da7f8..a2b4b86bac 100644 --- a/src/Avalonia.Base/Threading/DispatcherPriority.cs +++ b/src/Avalonia.Base/Threading/DispatcherPriority.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Threading { /// @@ -20,7 +17,7 @@ namespace Avalonia.Threading SystemIdle = 1, /// - /// The job will be processed when the application sis idle. + /// The job will be processed when the application is idle. /// ApplicationIdle = 2, diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index 4a8a9d673f..56cde9738e 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Reactive.Disposables; using Avalonia.Platform; @@ -17,7 +14,7 @@ namespace Avalonia.Threading private readonly DispatcherPriority _priority; private TimeSpan _interval; - + /// /// Initializes a new instance of the class. /// @@ -157,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) => @@ -200,7 +199,7 @@ namespace Avalonia.Threading } } - + /// /// Raises the event on the dispatcher thread. diff --git a/src/Avalonia.Base/Threading/JobRunner.cs b/src/Avalonia.Base/Threading/JobRunner.cs index b922370e84..58f623fb3f 100644 --- a/src/Avalonia.Base/Threading/JobRunner.cs +++ b/src/Avalonia.Base/Threading/JobRunner.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Linq; diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs index ac128d83de..0238446892 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Collections.Generic; namespace Avalonia.Utilities @@ -129,6 +126,27 @@ namespace Avalonia.Utilities _entries[TryFindEntry(property.Id).Item1].Value = value; } + public void Remove(AvaloniaProperty property) + { + var (index, found) = TryFindEntry(property.Id); + + if (found) + { + Entry[] entries = new Entry[_entries.Length - 1]; + int ix = 0; + + for (int i = 0; i < _entries.Length; ++i) + { + if (i != index) + { + entries[ix++] = _entries[i]; + } + } + + _entries = entries; + } + } + public Dictionary ToDictionary() { var dict = new Dictionary(_entries.Length - 1); diff --git a/src/Avalonia.Base/Utilities/CharacterReader.cs b/src/Avalonia.Base/Utilities/CharacterReader.cs index 3ba79c6866..65c5c2d010 100644 --- a/src/Avalonia.Base/Utilities/CharacterReader.cs +++ b/src/Avalonia.Base/Utilities/CharacterReader.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Utilities diff --git a/src/Avalonia.Base/Utilities/DeferredSetter.cs b/src/Avalonia.Base/Utilities/DeferredSetter.cs deleted file mode 100644 index fe9b0e58a0..0000000000 --- a/src/Avalonia.Base/Utilities/DeferredSetter.cs +++ /dev/null @@ -1,128 +0,0 @@ -// 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 System; - -namespace Avalonia.Utilities -{ - /// - /// A utility class to enable deferring assignment until after property-changed notifications are sent. - /// Used to fix #855. - /// - /// The type of value with which to track the delayed assignment. - internal sealed class DeferredSetter - { - private readonly SingleOrQueue _pendingValues; - private bool _isNotifying; - - public DeferredSetter() - { - _pendingValues = new SingleOrQueue(); - } - - private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty property, ref TSetRecord backing, TSetRecord value) - { - var old = backing; - - backing = value; - - source.RaisePropertyChanged(property, old, value); - } - - public bool SetAndNotify( - AvaloniaObject source, - AvaloniaProperty property, - ref TSetRecord backing, - TSetRecord value) - { - if (!_isNotifying) - { - using (new NotifyDisposable(this)) - { - SetAndRaisePropertyChanged(source, property, ref backing, value); - } - - if (!_pendingValues.Empty) - { - using (new NotifyDisposable(this)) - { - while (!_pendingValues.Empty) - { - SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue()); - } - } - } - - return true; - } - - _pendingValues.Enqueue(value); - - return false; - } - - public bool SetAndNotifyCallback(AvaloniaProperty property, ISetAndNotifyHandler setAndNotifyHandler, ref TValue backing, TValue value) - where TValue : TSetRecord - { - if (!_isNotifying) - { - using (new NotifyDisposable(this)) - { - setAndNotifyHandler.HandleSetAndNotify(property, ref backing, value); - } - - if (!_pendingValues.Empty) - { - using (new NotifyDisposable(this)) - { - while (!_pendingValues.Empty) - { - setAndNotifyHandler.HandleSetAndNotify(property, ref backing, (TValue)_pendingValues.Dequeue()); - } - } - } - - return true; - } - - _pendingValues.Enqueue(value); - - return false; - } - - /// - /// Disposable that marks the property as currently notifying. - /// When disposed, marks the property as done notifying. - /// - private readonly struct NotifyDisposable : IDisposable - { - private readonly DeferredSetter _setter; - - internal NotifyDisposable(DeferredSetter setter) - { - _setter = setter; - _setter._isNotifying = true; - } - - public void Dispose() - { - _setter._isNotifying = false; - } - } - } - - /// - /// Handler for set and notify requests. - /// - /// Value type. - internal interface ISetAndNotifyHandler - { - /// - /// Handles deferred setter requests to set a value. - /// - /// Property being set. - /// Backing field reference. - /// New value. - void HandleSetAndNotify(AvaloniaProperty property, ref TValue backing, TValue value); - } -} diff --git a/src/Avalonia.Base/Utilities/DisposableLock.cs b/src/Avalonia.Base/Utilities/DisposableLock.cs new file mode 100644 index 0000000000..b06e97da00 --- /dev/null +++ b/src/Avalonia.Base/Utilities/DisposableLock.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading; + +namespace Avalonia.Utilities +{ + public class DisposableLock + { + private readonly object _lock = new object(); + + /// + /// Tries to take a lock + /// + /// IDisposable if succeeded to obtain the lock + public IDisposable TryLock() + { + if (Monitor.TryEnter(_lock)) + return new UnlockDisposable(_lock); + return null; + } + + /// + /// Enters a waiting lock + /// + public IDisposable Lock() + { + Monitor.Enter(_lock); + return new UnlockDisposable(_lock); + } + + private sealed class UnlockDisposable : IDisposable + { + private object _lock; + + public UnlockDisposable(object @lock) + { + _lock = @lock; + } + + public void Dispose() + { + object @lock = Interlocked.Exchange(ref _lock, null); + + if (@lock != null) + { + Monitor.Exit(@lock); + } + } + } + } +} diff --git a/src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs b/src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs new file mode 100644 index 0000000000..4b889eb129 --- /dev/null +++ b/src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs @@ -0,0 +1,34 @@ +#nullable enable + +namespace Avalonia.Utilities +{ + /// + /// A visitor to resolve an untyped to a typed property. + /// + /// The type of user data passed. + /// + /// Pass an instance that implements this interface to + /// + /// in order to resolve un untyped to a typed + /// or . + /// + public interface IAvaloniaPropertyVisitor + where TData : struct + { + /// + /// Called when the property is a styled property. + /// + /// The property value type. + /// The property. + /// The user data. + void Visit(StyledPropertyBase property, ref TData data); + + /// + /// Called when the property is a direct property. + /// + /// The property value type. + /// The property. + /// The user data. + void Visit(DirectPropertyBase property, ref TData data); + } +} diff --git a/src/Avalonia.Base/Utilities/IWeakSubscriber.cs b/src/Avalonia.Base/Utilities/IWeakSubscriber.cs index 5518d41168..a2907eea2e 100644 --- a/src/Avalonia.Base/Utilities/IWeakSubscriber.cs +++ b/src/Avalonia.Base/Utilities/IWeakSubscriber.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Utilities diff --git a/src/Avalonia.Base/Utilities/IdentifierParser.cs b/src/Avalonia.Base/Utilities/IdentifierParser.cs index ab14ea2c58..5bbea06d10 100644 --- a/src/Avalonia.Base/Utilities/IdentifierParser.cs +++ b/src/Avalonia.Base/Utilities/IdentifierParser.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Globalization; namespace Avalonia.Utilities @@ -15,7 +12,7 @@ namespace Avalonia.Utilities { if (IsValidIdentifierStart(r.PeekOneOrThrow)) { - return r.TakeWhile(IsValidIdentifierChar); + return r.TakeWhile(c => IsValidIdentifierChar(c)); } else { diff --git a/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs b/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs new file mode 100644 index 0000000000..a1246c57b5 --- /dev/null +++ b/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs @@ -0,0 +1,48 @@ +using System.Collections; +using System.Collections.Generic; + +namespace Avalonia.Utilities +{ + public struct ImmutableReadOnlyListStructEnumerator : IEnumerator, IEnumerator + { + private readonly IReadOnlyList _readOnlyList; + private int _pos; + + public ImmutableReadOnlyListStructEnumerator(IReadOnlyList readOnlyList) + { + _readOnlyList = readOnlyList; + _pos = -1; + Current = default; + } + + public T Current + { + get; + private set; + } + + object IEnumerator.Current => Current; + + public void Dispose() { } + + public bool MoveNext() + { + if (_pos >= _readOnlyList.Count - 1) + { + return false; + } + + Current = _readOnlyList[++_pos]; + + return true; + + } + + public void Reset() + { + _pos = -1; + + Current = default; + } + } +} diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 41b57b6e70..2a92e75f58 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -1,8 +1,4 @@ -// 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 System; -using System.Runtime.InteropServices; namespace Avalonia.Utilities { @@ -11,6 +7,11 @@ namespace Avalonia.Utilities /// 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. @@ -21,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 @@ -38,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 @@ -50,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 @@ -62,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 @@ -74,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. @@ -81,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; } /// @@ -91,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; } /// @@ -103,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; @@ -117,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. /// @@ -159,6 +215,11 @@ namespace Avalonia.Utilities /// The clamped value. public static int Clamp(int val, int min, int max) { + if (min > max) + { + ThrowCannotBeGreaterThanException(min, max); + } + if (val < min) { return min; @@ -172,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/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index d85eb4cd76..d0d88166a7 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -1,7 +1,5 @@ -// 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 System; +using System.ComponentModel; using System.Globalization; using System.Linq; using System.Reflection; @@ -92,8 +90,7 @@ namespace Avalonia.Utilities /// True if the type accepts null values; otherwise false. public static bool AcceptsNull(Type type) { - var t = type.GetTypeInfo(); - return !t.IsValueType || (t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(Nullable<>))); + return !type.IsValueType || IsNullableType(type); } /// @@ -118,47 +115,46 @@ namespace Avalonia.Utilities return true; } + var toUnderl = Nullable.GetUnderlyingType(to) ?? to; var from = value.GetType(); - var fromTypeInfo = from.GetTypeInfo(); - var toTypeInfo = to.GetTypeInfo(); - if (toTypeInfo.IsAssignableFrom(fromTypeInfo)) + if (toUnderl.IsAssignableFrom(from)) { result = value; return true; } - if (to == typeof(string)) + if (toUnderl == typeof(string)) { - result = Convert.ToString(value); + result = Convert.ToString(value, culture); return true; } - if (toTypeInfo.IsEnum && from == typeof(string)) + if (toUnderl.IsEnum && from == typeof(string)) { - if (Enum.IsDefined(to, (string)value)) + if (Enum.IsDefined(toUnderl, (string)value)) { - result = Enum.Parse(to, (string)value); + result = Enum.Parse(toUnderl, (string)value); return true; } } - if (!fromTypeInfo.IsEnum && toTypeInfo.IsEnum) + if (!from.IsEnum && toUnderl.IsEnum) { result = null; - if (TryConvert(Enum.GetUnderlyingType(to), value, culture, out object enumValue)) + if (TryConvert(Enum.GetUnderlyingType(toUnderl), value, culture, out object enumValue)) { - result = Enum.ToObject(to, enumValue); + result = Enum.ToObject(toUnderl, enumValue); return true; } } - if (fromTypeInfo.IsEnum && IsNumeric(to)) + if (from.IsEnum && IsNumeric(toUnderl)) { try { - result = Convert.ChangeType((int)value, to, culture); + result = Convert.ChangeType((int)value, toUnderl, culture); return true; } catch @@ -169,7 +165,7 @@ namespace Avalonia.Utilities } var convertableFrom = Array.IndexOf(InbuiltTypes, from); - var convertableTo = Array.IndexOf(InbuiltTypes, to); + var convertableTo = Array.IndexOf(InbuiltTypes, toUnderl); if (convertableFrom != -1 && convertableTo != -1) { @@ -177,7 +173,7 @@ namespace Avalonia.Utilities { try { - result = Convert.ChangeType(value, to, culture); + result = Convert.ChangeType(value, toUnderl, culture); return true; } catch @@ -188,7 +184,23 @@ namespace Avalonia.Utilities } } - var cast = FindTypeConversionOperatorMethod(from, to, OperatorType.Implicit | OperatorType.Explicit); + var toTypeConverter = TypeDescriptor.GetConverter(toUnderl); + + if (toTypeConverter.CanConvertFrom(from) == true) + { + result = toTypeConverter.ConvertFrom(null, culture, value); + return true; + } + + var fromTypeConverter = TypeDescriptor.GetConverter(from); + + if (fromTypeConverter.CanConvertTo(toUnderl) == true) + { + result = fromTypeConverter.ConvertTo(null, culture, value, toUnderl); + return true; + } + + var cast = FindTypeConversionOperatorMethod(from, toUnderl, OperatorType.Implicit | OperatorType.Explicit); if (cast != null) { @@ -223,10 +235,8 @@ namespace Avalonia.Utilities } var from = value.GetType(); - var fromTypeInfo = from.GetTypeInfo(); - var toTypeInfo = to.GetTypeInfo(); - if (toTypeInfo.IsAssignableFrom(fromTypeInfo)) + if (to.IsAssignableFrom(from)) { result = value; return true; @@ -289,6 +299,17 @@ namespace Avalonia.Utilities return TryConvertImplicit(type, value, out object result) ? result : Default(type); } + public static T ConvertImplicit(object value) + { + if (TryConvertImplicit(typeof(T), value, out var result)) + { + return (T)result; + } + + throw new InvalidCastException( + $"Unable to convert object '{value ?? "(null)"}' of type '{value?.GetType()}' to type '{typeof(T)}'."); + } + /// /// Gets the default value for the specified type. /// @@ -296,9 +317,7 @@ namespace Avalonia.Utilities /// The default value. public static object Default(Type type) { - var typeInfo = type.GetTypeInfo(); - - if (typeInfo.IsValueType) + if (type.IsValueType) { return Activator.CreateInstance(type); } @@ -324,9 +343,11 @@ namespace Avalonia.Utilities return false; } - if (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + Type underlyingType = Nullable.GetUnderlyingType(type); + + if (underlyingType != null) { - return IsNumeric(Nullable.GetUnderlyingType(type)); + return IsNumeric(underlyingType); } else { @@ -341,6 +362,11 @@ namespace Avalonia.Utilities Explicit = 2 } + private static bool IsNullableType(Type type) + { + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + private static MethodInfo FindTypeConversionOperatorMethod(Type fromType, Type toType, OperatorType operatorType) { const string implicitName = "op_Implicit"; diff --git a/src/Avalonia.Base/Utilities/ValueSingleOrList.cs b/src/Avalonia.Base/Utilities/ValueSingleOrList.cs new file mode 100644 index 0000000000..dc32cedb76 --- /dev/null +++ b/src/Avalonia.Base/Utilities/ValueSingleOrList.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; + +namespace Avalonia.Utilities +{ + /// + /// A list like struct optimized for holding zero or one items. + /// + /// The type of items held in the list. + /// + /// Once more than value has been added to this storage it will switch to using internally. + /// + public ref struct ValueSingleOrList + { + private bool _isSingleSet; + + /// + /// Single contained value. Only valid if is set. + /// + public T Single { get; private set; } + + /// + /// List of values. + /// + public List List { get; private set; } + + /// + /// If this struct is backed by a list. + /// + public bool HasList => List != null; + + /// + /// If this struct contains only single value and storage was not promoted to a list. + /// + public bool IsSingle => List == null && _isSingleSet; + + /// + /// Adds a value. + /// + /// Value to add. + public void Add(T value) + { + if (List != null) + { + List.Add(value); + } + else + { + if (!_isSingleSet) + { + Single = value; + + _isSingleSet = true; + } + else + { + List = new List(); + + List.Add(Single); + List.Add(value); + + Single = default; + } + } + } + + /// + /// Removes a value. + /// + /// Value to remove. + public bool Remove(T value) + { + if (List != null) + { + return List.Remove(value); + } + + if (!_isSingleSet) + { + return false; + } + + if (EqualityComparer.Default.Equals(Single, value)) + { + Single = default; + + _isSingleSet = false; + + return true; + } + + return false; + } + } +} diff --git a/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs index 37e25d0fac..28c9dcc262 100644 --- a/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs +++ b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Linq; @@ -183,11 +180,16 @@ namespace Avalonia.Utilities for (int c = 0; c < _count; c++) { var r = _data[c]; + + TSubscriber target = null; + + r.Subscriber?.TryGetTarget(out target); + //Mark current index as first empty - if (r.Subscriber == null && empty == -1) + if (target == null && empty == -1) empty = c; //If current element isn't null and we have an empty one - if (r.Subscriber != null && empty != -1) + if (target != null && empty != -1) { _data[c] = default; _data[empty] = r; diff --git a/src/Avalonia.Base/Utilities/WeakObservable.cs b/src/Avalonia.Base/Utilities/WeakObservable.cs index b1fe78c206..20e01c751f 100644 --- a/src/Avalonia.Base/Utilities/WeakObservable.cs +++ b/src/Avalonia.Base/Utilities/WeakObservable.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Reactive; using System.Reactive.Linq; diff --git a/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs b/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs index f1e8fa6f9b..65128e7932 100644 --- a/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs +++ b/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Linq; diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 1bdbd4ca7c..05e66f2e0a 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -1,205 +1,296 @@ using System; -using System.Collections.Generic; using Avalonia.Data; +using Avalonia.PropertyStore; using Avalonia.Utilities; +#nullable enable + namespace Avalonia { - internal class ValueStore : IPriorityValueOwner + /// + /// Stores styled property values for an . + /// + /// + /// At its core this class consists of an to + /// mapping which holds the current values for each set property. This + /// can be in one of 4 states: + /// + /// - For a single local value it will be an instance of . + /// - For a single value of a priority other than LocalValue it will be an instance of + /// ` + /// - For a single binding it will be an instance of + /// - For all other cases it will be an instance of + /// + internal class ValueStore : IValueSink { - private readonly AvaloniaPropertyValueStore _propertyValues; - private readonly AvaloniaPropertyValueStore _deferredSetters; private readonly AvaloniaObject _owner; + private readonly IValueSink _sink; + private readonly AvaloniaPropertyValueStore _values; public ValueStore(AvaloniaObject owner) { - _owner = owner; - _propertyValues = new AvaloniaPropertyValueStore(); - _deferredSetters = new AvaloniaPropertyValueStore(); + _sink = _owner = owner; + _values = new AvaloniaPropertyValueStore(); } - public IDisposable AddBinding( - AvaloniaProperty property, - IObservable source, - BindingPriority priority) + public bool IsAnimating(AvaloniaProperty property) { - PriorityValue priorityValue; - - if (_propertyValues.TryGetValue(property, out var v)) + if (_values.TryGetValue(property, out var slot)) { - priorityValue = v as PriorityValue; - - if (priorityValue == null) - { - priorityValue = CreatePriorityValue(property); - priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - _propertyValues.SetValue(property, priorityValue); - } + return slot.Priority < BindingPriority.LocalValue; } - else + + return false; + } + + public bool IsSet(AvaloniaProperty property) + { + if (_values.TryGetValue(property, out var slot)) { - priorityValue = CreatePriorityValue(property); - _propertyValues.AddValue(property, priorityValue); + return slot.GetValue().HasValue; } - return priorityValue.Add(source, (int)priority); + return false; } - public void AddValue(AvaloniaProperty property, object value, int priority) + public bool TryGetValue( + StyledPropertyBase property, + BindingPriority maxPriority, + out T value) { - PriorityValue priorityValue; - - if (_propertyValues.TryGetValue(property, out var v)) + if (_values.TryGetValue(property, out var slot)) { - priorityValue = v as PriorityValue; + var v = ((IValue)slot).GetValue(maxPriority); - if (priorityValue == null) + if (v.HasValue) { - if (priority == (int)BindingPriority.LocalValue) - { - _propertyValues.SetValue(property, Validate(property, value)); - Changed(property, priority, v, value); - return; - } - else - { - priorityValue = CreatePriorityValue(property); - priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - _propertyValues.SetValue(property, priorityValue); - } + value = v.Value; + return true; } } + + value = default!; + return false; + } + + public IDisposable? SetValue(StyledPropertyBase property, T value, BindingPriority priority) + { + if (property.ValidateValue?.Invoke(value) == false) + { + throw new ArgumentException($"{value} is not a valid value for '{property.Name}."); + } + + IDisposable? result = null; + + if (_values.TryGetValue(property, out var slot)) + { + result = SetExisting(slot, property, value, priority); + } + else if (property.HasCoercion) + { + // If the property has any coercion callbacks then always create a PriorityValue. + var entry = new PriorityValue(_owner, property, this); + _values.AddValue(property, entry); + result = entry.SetValue(value, priority); + } else { - if (value == AvaloniaProperty.UnsetValue) - { - return; - } + var change = new AvaloniaPropertyChangedEventArgs(_owner, property, default, value, priority); - if (priority == (int)BindingPriority.LocalValue) + if (priority == BindingPriority.LocalValue) { - _propertyValues.AddValue(property, Validate(property, value)); - Changed(property, priority, AvaloniaProperty.UnsetValue, value); - return; + _values.AddValue(property, new LocalValueEntry(value)); + _sink.ValueChanged(change); } else { - priorityValue = CreatePriorityValue(property); - _propertyValues.AddValue(property, priorityValue); + var entry = new ConstantValueEntry(property, value, priority, this); + _values.AddValue(property, entry); + _sink.ValueChanged(change); + result = entry; } } - priorityValue.SetValue(value, priority); + return result; } - public void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification) + public IDisposable AddBinding( + StyledPropertyBase property, + IObservable> source, + BindingPriority priority) { - _owner.BindingNotificationReceived(property, notification); + if (_values.TryGetValue(property, out var slot)) + { + return BindExisting(slot, property, source, priority); + } + else if (property.HasCoercion) + { + // If the property has any coercion callbacks then always create a PriorityValue. + var entry = new PriorityValue(_owner, property, this); + var binding = entry.AddBinding(source, priority); + _values.AddValue(property, entry); + binding.Start(); + return binding; + } + else + { + var entry = new BindingEntry(_owner, property, source, priority, this); + _values.AddValue(property, entry); + entry.Start(); + return entry; + } } - public void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue) + public void ClearLocalValue(StyledPropertyBase property) { - _owner.PriorityValueChanged(property, priority, oldValue, newValue); - } + if (_values.TryGetValue(property, out var slot)) + { + if (slot is PriorityValue p) + { + p.ClearLocalValue(); + } + else + { + var remove = slot is ConstantValueEntry c ? + c.Priority == BindingPriority.LocalValue : + !(slot is IPriorityValueEntry); - public IDictionary GetSetValues() - { - return _propertyValues.ToDictionary(); + if (remove) + { + var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? value : default; + _values.Remove(property); + _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + _owner, + property, + old, + default, + BindingPriority.Unset)); + } + } + } } - public void LogError(AvaloniaProperty property, Exception e) + public void CoerceValue(StyledPropertyBase property) { - _owner.LogBindingError(property, e); + if (_values.TryGetValue(property, out var slot)) + { + if (slot is PriorityValue p) + { + p.CoerceValue(); + } + } } - public object GetValue(AvaloniaProperty property) + public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property) { - var result = AvaloniaProperty.UnsetValue; - - if (_propertyValues.TryGetValue(property, out var value)) + if (_values.TryGetValue(property, out var slot)) { - result = (value is PriorityValue priorityValue) ? priorityValue.Value : value; + var slotValue = slot.GetValue(); + return new Diagnostics.AvaloniaPropertyValue( + property, + slotValue.HasValue ? slotValue.Value : AvaloniaProperty.UnsetValue, + slot.Priority, + null); } - return result; + return null; } - public bool IsAnimating(AvaloniaProperty property) + void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs change) { - return _propertyValues.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; + _sink.ValueChanged(change); } - public bool IsSet(AvaloniaProperty property) + void IValueSink.Completed( + StyledPropertyBase property, + IPriorityValueEntry entry, + Optional oldValue) { - if (_propertyValues.TryGetValue(property, out var value)) + if (_values.TryGetValue(property, out var slot)) { - return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue; + if (slot == entry) + { + _values.Remove(property); + _sink.Completed(property, entry, oldValue); + } } - - return false; } - public void Revalidate(AvaloniaProperty property) + private IDisposable? SetExisting( + object slot, + StyledPropertyBase property, + T value, + BindingPriority priority) { - if (_propertyValues.TryGetValue(property, out var value)) + IDisposable? result = null; + + if (slot is IPriorityValueEntry e) { - (value as PriorityValue)?.Revalidate(); + var priorityValue = new PriorityValue(_owner, property, this, e); + _values.SetValue(property, priorityValue); + result = priorityValue.SetValue(value, priority); } - } - - public void VerifyAccess() => _owner.VerifyAccess(); - - private PriorityValue CreatePriorityValue(AvaloniaProperty property) - { - var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType()); - Func validate2 = null; - - if (validate != null) + else if (slot is PriorityValue p) { - validate2 = v => validate(_owner, v); + result = p.SetValue(value, priority); + } + else if (slot is LocalValueEntry l) + { + if (priority == BindingPriority.LocalValue) + { + var old = l.GetValue(BindingPriority.LocalValue); + l.SetValue(value); + _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + _owner, + property, + old, + value, + priority)); + } + else + { + var priorityValue = new PriorityValue(_owner, property, this, l); + result = priorityValue.SetValue(value, priority); + _values.SetValue(property, priorityValue); + } + } + else + { + throw new NotSupportedException("Unrecognised value store slot type."); } - return new PriorityValue( - this, - property, - property.PropertyType, - validate2); + return result; } - private object Validate(AvaloniaProperty property, object value) + private IDisposable BindExisting( + object slot, + StyledPropertyBase property, + IObservable> source, + BindingPriority priority) { - var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType()); + PriorityValue priorityValue; - if (validate != null && value != AvaloniaProperty.UnsetValue) + if (slot is IPriorityValueEntry e) { - return validate(_owner, value); + priorityValue = new PriorityValue(_owner, property, this, e); } - - return value; - } - - private DeferredSetter GetDeferredSetter(AvaloniaProperty property) - { - if (_deferredSetters.TryGetValue(property, out var deferredSetter)) + else if (slot is PriorityValue p) { - return (DeferredSetter)deferredSetter; + priorityValue = p; + } + else if (slot is LocalValueEntry l) + { + priorityValue = new PriorityValue(_owner, property, this, l); + } + else + { + throw new NotSupportedException("Unrecognised value store slot type."); } - var newDeferredSetter = new DeferredSetter(); - - _deferredSetters.AddValue(property, newDeferredSetter); - - return newDeferredSetter; - } - - public DeferredSetter GetNonDirectDeferredSetter(AvaloniaProperty property) - { - return GetDeferredSetter(property); - } - - public DeferredSetter GetDirectDeferredSetter(AvaloniaProperty property) - { - return GetDeferredSetter(property); + var binding = priorityValue.AddBinding(source, priority); + _values.SetValue(property, priorityValue); + binding.Start(); + return binding; } } } diff --git a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs index 61303dbbfe..95e59dde2b 100644 --- a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs +++ b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs @@ -12,6 +12,8 @@ namespace Avalonia.Build.Tasks { public bool Execute() { + Enum.TryParse(ReportImportance, true, out MessageImportance outputImportance); + OutputPath = OutputPath ?? AssemblyFile; var outputPdb = GetPdbPath(OutputPath); var input = AssemblyFile; @@ -32,9 +34,12 @@ namespace Avalonia.Build.Tasks } } + var msg = $"CompileAvaloniaXamlTask -> AssemblyFile:{AssemblyFile}, ProjectDirectory:{ProjectDirectory}, OutputPath:{OutputPath}"; + BuildEngine.LogMessage(msg, outputImportance < MessageImportance.Low ? MessageImportance.High : outputImportance); + var res = XamlCompilerTaskExecutor.Compile(BuildEngine, input, File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(), - ProjectDirectory, OutputPath); + ProjectDirectory, OutputPath, VerifyIl, outputImportance); if (!res.Success) return false; if (!res.WrittenFile) @@ -66,7 +71,11 @@ namespace Avalonia.Build.Tasks public string ProjectDirectory { get; set; } public string OutputPath { get; set; } - + + public bool VerifyIl { get; set; } + + public string ReportImportance { get; set; } + public IBuildEngine BuildEngine { get; set; } public ITaskHost HostObject { get; set; } } diff --git a/src/Avalonia.Build.Tasks/Extensions.cs b/src/Avalonia.Build.Tasks/Extensions.cs index 440c6d7489..46c12eaf3d 100644 --- a/src/Avalonia.Build.Tasks/Extensions.cs +++ b/src/Avalonia.Build.Tasks/Extensions.cs @@ -9,14 +9,19 @@ namespace Avalonia.Build.Tasks public static void LogError(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message) { - engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message, + engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message, "", "Avalonia")); } - + public static void LogWarning(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message) { engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message, "", "Avalonia")); } + + public static void LogMessage(this IBuildEngine engine, string message, MessageImportance imp) + { + engine.LogMessageEvent(new BuildMessageEventArgs(message, "", "Avalonia", imp)); + } } } diff --git a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs index 98ebb3e7d1..ae2bf99d1e 100644 --- a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs +++ b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs @@ -22,6 +22,10 @@ namespace Avalonia.Build.Tasks [Required] public ITaskItem[] EmbeddedResources { get; set; } + public string ReportImportance { get; set; } + + private MessageImportance _reportImportance; + class Source { public string Path { get; set; } @@ -29,15 +33,11 @@ namespace Avalonia.Build.Tasks private byte[] _data; private string _sourcePath; - public Source(string file, string root) + public Source(string relativePath, string root) { - file = SPath.GetFullPath(file); root = SPath.GetFullPath(root); - var fileUri = new Uri(file, UriKind.Absolute); - var rootUri = new Uri(root, UriKind.Absolute); - rootUri = new Uri(rootUri.ToString().TrimEnd('/') + '/'); - Path = '/' + rootUri.MakeRelativeUri(fileUri).ToString().TrimStart('/'); - _sourcePath = file; + Path = "/" + relativePath.Replace('\\', '/'); + _sourcePath = SPath.Combine(root, relativePath); Size = (int)new FileInfo(_sourcePath).Length; } @@ -65,7 +65,14 @@ namespace Avalonia.Build.Tasks } } - List BuildResourceSources() => Resources.Select(r => new Source(r.ItemSpec, Root)).ToList(); + List BuildResourceSources() + => Resources.Select(r => + { + + var src = new Source(r.ItemSpec, Root); + BuildEngine.LogMessage($"avares -> name:{src.Path}, path: {src.SystemPath}, size:{src.Size}, ItemSpec:{r.ItemSpec}", _reportImportance); + return src; + }).ToList(); private void Pack(Stream output, List sources) { @@ -100,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 @@ -136,10 +143,14 @@ namespace Avalonia.Build.Tasks sources.Add(new Source("/!AvaloniaResourceXamlInfo", ms.ToArray())); return true; } - + public bool Execute() { - foreach(var r in EmbeddedResources.Where(r=>r.ItemSpec.EndsWith(".xaml")||r.ItemSpec.EndsWith(".paml"))) + Enum.TryParse(ReportImportance, out _reportImportance); + + 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") || 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/Program.cs b/src/Avalonia.Build.Tasks/Program.cs index d356b15408..1909c4c6ec 100644 --- a/src/Avalonia.Build.Tasks/Program.cs +++ b/src/Avalonia.Build.Tasks/Program.cs @@ -28,7 +28,8 @@ namespace Avalonia.Build.Tasks ReferencesFilePath = args[1], OutputPath = args[2], BuildEngine = new ConsoleBuildEngine(), - ProjectDirectory = Directory.GetCurrentDirectory() + ProjectDirectory = Directory.GetCurrentDirectory(), + VerifyIl = true }.Execute() ? 0 : 2; diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 74eaee5e07..886cc1e917 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -25,7 +25,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 { @@ -40,7 +41,7 @@ namespace Avalonia.Build.Tasks } public static CompileResult Compile(IBuildEngine engine, string input, string[] references, string projectDirectory, - string output) + string output, bool verifyIl, MessageImportance logImportance) { var typeSystem = new CecilTypeSystem(references.Concat(new[] {input}), input); var asm = typeSystem.TargetAssemblyDefinition; @@ -74,7 +75,7 @@ namespace Avalonia.Build.Tasks var contextClass = XamlIlContextDefinition.GenerateContextClass(typeSystem.CreateTypeBuilder(contextDef), typeSystem, xamlLanguage); - var compiler = new AvaloniaXamlIlCompiler(compilerConfig, contextClass); + var compiler = new AvaloniaXamlIlCompiler(compilerConfig, contextClass) { EnableIlVerification = verifyIl }; var editorBrowsableAttribute = typeSystem .GetTypeReference(typeSystem.FindType("System.ComponentModel.EditorBrowsableAttribute")) @@ -130,6 +131,8 @@ namespace Avalonia.Build.Tasks { try { + engine.LogMessage($"XAMLIL: {res.Name} -> {res.Uri}", logImportance); + // StreamReader is needed here to handle BOM var xaml = new StreamReader(new MemoryStream(res.FileContents)).ReadToEnd(); var parsed = XDocumentXamlIlParser.Parse(xaml); diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs index 86113da87e..f741d40571 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Collections; using System.Collections.Generic; using System.Globalization; @@ -238,7 +235,7 @@ namespace Avalonia.Collections } else { - return seq.ThenByDescending(o => GetValue(o), InternalComparer); + return seq.ThenBy(o => GetValue(o), InternalComparer); } } diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index b65fd2a8b7..7e893d131e 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -1,5 +1,4 @@ -// (c) Copyright Microsoft Corporation. -// This source is subject to the Microsoft Public License (Ms-PL). +// This source is subject to the Microsoft Public License (Ms-PL). // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. @@ -65,7 +64,7 @@ namespace Avalonia.Controls private const double DATAGRID_minimumColumnHeaderHeight = 4; internal const double DATAGRID_maximumStarColumnWidth = 10000; internal const double DATAGRID_minimumStarColumnWidth = 0.001; - private const double DATAGRID_mouseWheelDelta = 48.0; + private const double DATAGRID_mouseWheelDelta = 72.0; private const double DATAGRID_maxHeadersThickness = 32768; private const double DATAGRID_defaultRowHeight = 22; @@ -149,6 +148,9 @@ namespace Avalonia.Controls private IEnumerable _items; + public event EventHandler HorizontalScroll; + public event EventHandler VerticalScroll; + /// /// Identifies the CanUserReorderColumns dependency property. /// @@ -202,20 +204,12 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(ColumnHeaderHeight), defaultValue: double.NaN, - validate: ValidateColumnHeaderHeight); + validate: IsValidColumnHeaderHeight); - private static double ValidateColumnHeaderHeight(DataGrid grid, double value) + private static bool IsValidColumnHeaderHeight(double value) { - if (value < DATAGRID_minimumColumnHeaderHeight) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(ColumnHeaderHeight), DATAGRID_minimumColumnHeaderHeight); - } - if (value > DATAGRID_maxHeadersThickness) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(ColumnHeaderHeight), DATAGRID_maxHeadersThickness); - } - - return value; + return double.IsNaN(value) || + (value >= DATAGRID_minimumColumnHeaderHeight && value <= DATAGRID_maxHeadersThickness); } /// @@ -273,15 +267,7 @@ namespace Avalonia.Controls set { SetValue(FrozenColumnCountProperty, value); } } - private static int ValidateFrozenColumnCount(DataGrid grid, int value) - { - if (value < 0) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(FrozenColumnCount), 0); - } - - return value; - } + private static bool ValidateFrozenColumnCount(int value) => value >= 0; public static readonly StyledProperty GridLinesVisibilityProperty = AvaloniaProperty.Register(nameof(GridLinesVisibility)); @@ -389,36 +375,22 @@ namespace Avalonia.Controls public bool IsValid { get { return _isValid; } - internal set { SetAndRaise(IsValidProperty, ref _isValid, value); } + internal set + { + SetAndRaise(IsValidProperty, ref _isValid, value); + PseudoClasses.Set(":invalid", !value); + } } public static readonly StyledProperty MaxColumnWidthProperty = AvaloniaProperty.Register( nameof(MaxColumnWidth), defaultValue: DATAGRID_defaultMaxColumnWidth, - validate: ValidateMaxColumnWidth); + validate: IsValidColumnWidth); - private static double ValidateMaxColumnWidth(DataGrid grid, double value) + private static bool IsValidColumnWidth(double value) { - if (double.IsNaN(value)) - { - throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(MaxColumnWidth)); - } - if (value < 0) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MaxColumnWidth), 0); - } - if (grid.MinColumnWidth > value) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MaxColumnWidth), nameof(MinColumnWidth)); - } - - if (value < 0) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(FrozenColumnCount), 0); - } - - return value; + return !double.IsNaN(value) && value > 0; } /// @@ -434,28 +406,11 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(MinColumnWidth), defaultValue: DATAGRID_defaultMinColumnWidth, - validate: ValidateMinColumnWidth); + validate: IsValidMinColumnWidth); - private static double ValidateMinColumnWidth(DataGrid grid, double value) + private static bool IsValidMinColumnWidth(double value) { - if (double.IsNaN(value)) - { - throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(MinColumnWidth)); - } - if (value < 0) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MinColumnWidth), 0); - } - if (double.IsPositiveInfinity(value)) - { - throw DataGridError.DataGrid.ValueCannotBeSetToInfinity(nameof(MinColumnWidth)); - } - if (grid.MaxColumnWidth < value) - { - throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(MinColumnWidth), nameof(MaxColumnWidth)); - } - - return value; + return !double.IsNaN(value) && !double.IsPositiveInfinity(value) && value >= 0; } /// @@ -483,19 +438,12 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(RowHeight), defaultValue: double.NaN, - validate: ValidateRowHeight); - private static double ValidateRowHeight(DataGrid grid, double value) + validate: IsValidRowHeight); + private static bool IsValidRowHeight(double value) { - if (value < DataGridRow.DATAGRIDROW_minimumHeight) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(RowHeight), 0); - } - if (value > DataGridRow.DATAGRIDROW_maximumHeight) - { - throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(RowHeight), DataGridRow.DATAGRIDROW_maximumHeight); - } - - return value; + return double.IsNaN(value) || + (value >= DataGridRow.DATAGRIDROW_minimumHeight && + value <= DataGridRow.DATAGRIDROW_maximumHeight); } /// @@ -511,19 +459,12 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(RowHeaderWidth), defaultValue: double.NaN, - validate: ValidateRowHeaderWidth); - private static double ValidateRowHeaderWidth(DataGrid grid, double value) + validate: IsValidRowHeaderWidth); + private static bool IsValidRowHeaderWidth(double value) { - if (value < DATAGRID_minimumRowHeaderWidth) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(RowHeaderWidth), DATAGRID_minimumRowHeaderWidth); - } - if (value > DATAGRID_maxHeadersThickness) - { - throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(RowHeaderWidth), DATAGRID_maxHeadersThickness); - } - - return value; + return double.IsNaN(value) || + (value >= DATAGRID_minimumRowHeaderWidth && + value <= DATAGRID_maxHeadersThickness); } /// @@ -721,8 +662,6 @@ namespace Avalonia.Controls HorizontalScrollBarVisibilityProperty, VerticalScrollBarVisibilityProperty); - PseudoClass(IsValidProperty, x => !x, ":invalid"); - ItemsProperty.Changed.AddClassHandler((x, e) => x.OnItemsPropertyChanged(e)); CanUserResizeColumnsProperty.Changed.AddClassHandler((x, e) => x.OnCanUserResizeColumnsChanged(e)); ColumnWidthProperty.Changed.AddClassHandler((x, e) => x.OnColumnWidthChanged(e)); @@ -827,7 +766,7 @@ namespace Avalonia.Controls /// /// ItemsProperty property changed handler. /// - /// AvaloniaPropertyChangedEventArgs. + /// The event arguments. private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e) { if (!_areHandlersSuspended) @@ -2305,7 +2244,7 @@ namespace Avalonia.Controls /// Builds the visual tree for the column header when a new template is applied. /// //TODO Validation UI - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { // The template has changed, so we need to refresh the visuals _measured = false; @@ -2537,25 +2476,25 @@ namespace Avalonia.Controls internal bool ProcessDownKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); return ProcessDownKeyInternal(shift, ctrl); } internal bool ProcessEndKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); return ProcessEndKey(shift, ctrl); } internal bool ProcessEnterKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); return ProcessEnterKey(shift, ctrl); } internal bool ProcessHomeKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); return ProcessHomeKey(shift, ctrl); } @@ -2595,25 +2534,25 @@ namespace Avalonia.Controls internal bool ProcessLeftKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); return ProcessLeftKey(shift, ctrl); } internal bool ProcessNextKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); return ProcessNextKey(shift, ctrl); } internal bool ProcessPriorKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); return ProcessPriorKey(shift, ctrl); } internal bool ProcessRightKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); return ProcessRightKey(shift, ctrl); } @@ -2731,7 +2670,7 @@ namespace Avalonia.Controls internal bool ProcessUpKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); return ProcessUpKey(shift, ctrl); } @@ -2742,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 @@ -2759,7 +2698,7 @@ namespace Avalonia.Controls } else if (scrollEventType == ScrollEventType.SmallDecrement) { - if (DoubleUtil.GreaterThan(NegVerticalOffset, 0)) + if (MathUtilities.GreaterThan(NegVerticalOffset, 0)) { DisplayData.PendingVerticalScrollHeight -= NegVerticalOffset; } @@ -2778,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); @@ -2999,7 +2938,7 @@ namespace Avalonia.Controls //TODO: Ensure left button is checked for internal bool UpdateStateOnMouseLeftButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit) { - KeyboardHelper.GetMetaKeyState(pointerPressedEventArgs.InputModifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(pointerPressedEventArgs.KeyModifiers, out bool ctrl, out bool shift); return UpdateStateOnMouseLeftButtonDown(pointerPressedEventArgs, columnIndex, slot, allowEdit, shift, ctrl); } @@ -3407,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); } } @@ -3435,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; @@ -3450,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; @@ -3483,7 +3422,7 @@ namespace Avalonia.Controls if (allowVertScrollbar) { if (cellsHeight > 0 && - DoubleUtil.LessThanOrClose(vertScrollBarWidth, cellsWidth) && + MathUtilities.LessThanOrClose(vertScrollBarWidth, cellsWidth) && DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount) { cellsWidth -= vertScrollBarWidth; @@ -3500,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); @@ -4288,6 +4227,7 @@ namespace Avalonia.Controls private void HorizontalScrollBar_Scroll(object sender, ScrollEventArgs e) { ProcessHorizontalScroll(e.ScrollEventType); + HorizontalScroll?.Invoke(sender, e); } private bool IsColumnOutOfBounds(int columnIndex) @@ -4441,7 +4381,7 @@ namespace Avalonia.Controls private bool ProcessAKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift, out bool alt); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift, out bool alt); if (ctrl && !shift && !alt && SelectionMode == DataGridSelectionMode.Extended) { @@ -4507,10 +4447,10 @@ namespace Avalonia.Controls return ProcessAKey(e); case Key.C: - return ProcessCopyKey(e.Modifiers); + return ProcessCopyKey(e.KeyModifiers); case Key.Insert: - return ProcessCopyKey(e.Modifiers); + return ProcessCopyKey(e.KeyModifiers); } if (focusDataGrid) { @@ -4709,7 +4649,7 @@ namespace Avalonia.Controls private bool ProcessF2Key(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); if (!shift && !ctrl && _editingColumnIndex == -1 && CurrentColumnIndex != -1 && GetRowSelection(CurrentSlot) && @@ -5066,7 +5006,7 @@ namespace Avalonia.Controls private bool ProcessTabKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); return ProcessTabKey(e, shift, ctrl); } @@ -5447,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; } @@ -5620,6 +5560,7 @@ namespace Avalonia.Controls private void VerticalScrollBar_Scroll(object sender, ScrollEventArgs e) { ProcessVerticalScroll(e.ScrollEventType); + VerticalScroll?.Invoke(sender, e); } //TODO: Ensure left button is checked for @@ -5852,7 +5793,7 @@ namespace Avalonia.Controls /// to the Clipboard as text. /// /// Whether or not the DataGrid handled the key press. - private bool ProcessCopyKey(InputModifiers modifiers) + private bool ProcessCopyKey(KeyModifiers modifiers) { KeyboardHelper.GetMetaKeyState(modifiers, out bool ctrl, out bool shift, out bool alt); diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs index 09c3d07a41..8e82bf1a38 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs @@ -55,7 +55,7 @@ namespace Avalonia.Controls binding.Mode = BindingMode.TwoWay; } - if (binding.Converter == null) + if (binding.Converter == null && string.IsNullOrEmpty(binding.StringFormat)) { binding.Converter = DataGridValueConverter.Instance; } diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index e56c534f50..3973f1e86f 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -121,10 +121,8 @@ namespace Avalonia.Controls /// /// Builds the visual tree for the cell control when a new template is applied. /// - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - base.OnTemplateApplied(e); - UpdatePseudoClasses(); _rightGridLine = e.NameScope.Find(DATAGRIDCELL_elementRightGridLine); if (_rightGridLine != null && OwningColumn == null) @@ -164,7 +162,7 @@ namespace Avalonia.Controls if (OwningGrid != null) { OwningGrid.OnCellPointerPressed(new DataGridCellPointerPressedEventArgs(this, OwningRow, OwningColumn, e)); - if (e.MouseButton == MouseButton.Left) + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { if (!e.Handled) //if (!e.Handled && OwningGrid.IsTabStop) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index d1651b2d09..e6cc7e5e40 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -67,7 +67,7 @@ namespace Avalonia.Controls static DataGridColumnHeader() { - AreSeparatorsVisibleProperty.Changed.AddClassHandler((x,e) => x.OnAreSeparatorsVisibleChanged(e)); + AreSeparatorsVisibleProperty.Changed.AddClassHandler((x, e) => x.OnAreSeparatorsVisibleChanged(e)); } /// @@ -103,7 +103,7 @@ namespace Avalonia.Controls { get; set; - } + } internal DataGrid OwningGrid => OwningColumn?.OwningGrid; internal int ColumnIndex @@ -116,19 +116,19 @@ namespace Avalonia.Controls } return OwningColumn.Index; } - } + } internal ListSortDirection? CurrentSortingState { get; private set; - } + } private bool IsMouseOver { get; set; - } + } private bool IsPressed { @@ -158,14 +158,14 @@ namespace Avalonia.Controls && OwningGrid.DataConnection.AllowSort) { var sort = OwningColumn.GetSortDescription(); - if(sort != null) + if (sort != null) { CurrentSortingState = sort.Descending ? ListSortDirection.Descending : ListSortDirection.Ascending; } } - PseudoClasses.Set(":sortascending", + PseudoClasses.Set(":sortascending", CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Ascending); - PseudoClasses.Set(":sortdescending", + PseudoClasses.Set(":sortdescending", CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Descending); } @@ -190,28 +190,28 @@ namespace Avalonia.Controls } } - internal void OnMouseLeftButtonUp_Click(InputModifiers inputModifiers, ref bool handled) + internal void OnMouseLeftButtonUp_Click(KeyModifiers keyModifiers, ref bool handled) { // completed a click without dragging, so we're sorting - InvokeProcessSort(inputModifiers); + InvokeProcessSort(keyModifiers); handled = true; - } + } - internal void InvokeProcessSort(InputModifiers inputModifiers) + internal void InvokeProcessSort(KeyModifiers keyModifiers) { Debug.Assert(OwningGrid != null); - if (OwningGrid.WaitForLostFocus(() => InvokeProcessSort(inputModifiers))) + if (OwningGrid.WaitForLostFocus(() => InvokeProcessSort(keyModifiers))) { return; } if (OwningGrid.CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true)) { - Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(inputModifiers)); + Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(keyModifiers)); } - } + } //TODO GroupSorting - internal void ProcessSort(InputModifiers inputModifiers) + internal void ProcessSort(KeyModifiers keyModifiers) { // if we can sort: // - DataConnection.AllowSort is true, and @@ -233,7 +233,7 @@ namespace Avalonia.Controls DataGridSortDescription newSort; - KeyboardHelper.GetMetaKeyState(inputModifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(keyModifiers, out bool ctrl, out bool shift); DataGridSortDescription sort = OwningColumn.GetSortDescription(); IDataGridCollectionView collectionView = owningGrid.DataConnection.CollectionView; @@ -246,45 +246,49 @@ namespace Avalonia.Controls owningGrid.DataConnection.SortDescriptions.Clear(); } - if (sort != null) + // if ctrl is held down, we only clear the sort directions + if (!ctrl) { - newSort = sort.SwitchSortDirection(); - - // changing direction should not affect sort order, so we replace this column's - // sort description instead of just adding it to the end of the collection - int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort); - if (oldIndex >= 0) + if (sort != null) { - owningGrid.DataConnection.SortDescriptions.Remove(sort); - owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort); + newSort = sort.SwitchSortDirection(); + + // changing direction should not affect sort order, so we replace this column's + // sort description instead of just adding it to the end of the collection + int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort); + if (oldIndex >= 0) + { + owningGrid.DataConnection.SortDescriptions.Remove(sort); + owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort); + } + else + { + owningGrid.DataConnection.SortDescriptions.Add(newSort); + } } else { + string propertyName = OwningColumn.GetSortPropertyName(); + // no-opt if we couldn't find a property to sort on + if (string.IsNullOrEmpty(propertyName)) + { + return; + } + + newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture); owningGrid.DataConnection.SortDescriptions.Add(newSort); } } - else - { - string propertyName = OwningColumn.GetSortPropertyName(); - // no-opt if we couldn't find a property to sort on - if (string.IsNullOrEmpty(propertyName)) - { - return; - } - - newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture); - owningGrid.DataConnection.SortDescriptions.Add(newSort); - } } } - } + } private bool CanReorderColumn(DataGridColumn column) { - return OwningGrid.CanUserReorderColumns + return OwningGrid.CanUserReorderColumns && !(column is DataGridFillerColumn) && (column.CanUserReorderInternal.HasValue && column.CanUserReorderInternal.Value || !column.CanUserReorderInternal.HasValue); - } + } /// /// Determines whether a column can be resized by dragging the border of its header. If star sizing @@ -297,12 +301,12 @@ 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; } return column.ActualCanUserResize; - } + } private static bool TrySetResizeColumn(DataGridColumn column) { @@ -316,7 +320,7 @@ namespace Avalonia.Controls return true; } return false; - } + } //TODO DragDrop @@ -326,7 +330,7 @@ namespace Avalonia.Controls if (OwningGrid != null && OwningGrid.ColumnHeaders != null) { - args.Device.Capture(this); + args.Pointer.Capture(this); _dragMode = DragMode.MouseDown; _frozenColumnsWidth = OwningGrid.ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth(); @@ -371,7 +375,7 @@ namespace Avalonia.Controls { if (_dragMode == DragMode.MouseDown) { - OnMouseLeftButtonUp_Click(args.InputModifiers, ref handled); + OnMouseLeftButtonUp_Click(args.KeyModifiers, ref handled); } else if (_dragMode == DragMode.Reorder) { @@ -391,7 +395,7 @@ namespace Avalonia.Controls SetDragCursor(mousePosition); // Variables that track drag mode states get reset in DataGridColumnHeader_LostMouseCapture - args.Device.Capture(null); + args.Pointer.Capture(null); OnLostMouseCapture(); _dragMode = DragMode.None; handled = true; @@ -449,11 +453,11 @@ namespace Avalonia.Controls OnMouseLeave(); ApplyState(); - } + } private void DataGridColumnHeader_PointerPressed(object sender, PointerPressedEventArgs e) { - if (OwningColumn == null || e.Handled || !IsEnabled || e.MouseButton != MouseButton.Left) + if (OwningColumn == null || e.Handled || !IsEnabled || e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { return; } @@ -577,7 +581,7 @@ namespace Avalonia.Controls { return OwningGrid.Columns.Count - 1; } - } + } /// /// Returns true if the mouse is @@ -723,7 +727,7 @@ namespace Avalonia.Controls Point targetPosition = new Point(0, 0); if (targetColumn == null || targetColumn == OwningGrid.ColumnsInternal.FillerColumn || targetColumn.IsFrozen != OwningColumn.IsFrozen) { - targetColumn = + targetColumn = OwningGrid.ColumnsInternal.GetLastColumn( isVisible: true, isFrozen: OwningColumn.IsFrozen, @@ -741,7 +745,7 @@ namespace Avalonia.Controls handled = true; } - } + } private void OnMouseMove_Resize(ref bool handled, Point mousePositionHeaders) { @@ -764,7 +768,7 @@ namespace Avalonia.Controls handled = true; } - } + } private void SetDragCursor(Point mousePosition) { 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 c9924660be..830eff1102 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -536,10 +536,8 @@ namespace Avalonia.Controls /// /// Builds the visual tree for the column header when a new template is applied. /// - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - base.OnTemplateApplied(e); - RootElement = e.NameScope.Find(DATAGRIDROW_elementRoot); if (RootElement != null) { @@ -786,7 +784,7 @@ namespace Avalonia.Controls private void DataGridRow_PointerPressed(PointerPressedEventArgs e) { - if(e.MouseButton != MouseButton.Left) + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { return; } @@ -881,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/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index 7dafef9d8b..0790fcf5d5 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -68,25 +68,11 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(SublevelIndent), defaultValue: DataGrid.DATAGRID_defaultRowGroupSublevelIndent, - validate: ValidateSublevelIndent); + validate: IsValidSublevelIndent); - private static double ValidateSublevelIndent(DataGridRowGroupHeader header, double value) + private static bool IsValidSublevelIndent(double value) { - // We don't need to revert to the old value if our input is bad because we never read this property value - if (double.IsNaN(value)) - { - throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(SublevelIndent)); - } - else if (double.IsInfinity(value)) - { - throw DataGridError.DataGrid.ValueCannotBeSetToInfinity(nameof(SublevelIndent)); - } - else if (value < 0) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(SublevelIndent), 0); - } - - return value; + return !double.IsNaN(value) && !double.IsInfinity(value) && value >= 0; } /// @@ -182,7 +168,7 @@ namespace Avalonia.Controls private IDisposable _expanderButtonSubscription; - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { _rootElement = e.NameScope.Find(DataGridRow.DATAGRIDROW_elementRoot); @@ -213,8 +199,6 @@ namespace Avalonia.Controls _itemCountElement = e.NameScope.Find(DATAGRIDROWGROUPHEADER_itemCountElement); _propertyNameElement = e.NameScope.Find(DATAGRIDROWGROUPHEADER_propertyNameElement); UpdateTitleElements(); - - base.OnTemplateApplied(e); } internal void ApplyHeaderStatus() @@ -291,7 +275,7 @@ namespace Avalonia.Controls //TODO TabStop private void DataGridRowGroupHeader_PointerPressed(PointerPressedEventArgs e) { - if (OwningGrid != null && e.MouseButton == MouseButton.Left) + if (OwningGrid != null && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { if (OwningGrid.IsDoubleClickRecordsClickOnCall(this) && !e.Handled) { diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs index 5bfe449b63..324227d749 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs @@ -94,10 +94,8 @@ namespace Avalonia.Controls.Primitives /// /// Builds the visual tree for the row header when a new template is applied. /// - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - base.OnTemplateApplied(e); - _rootElement = e.NameScope.Find(DATAGRIDROWHEADER_elementRootName); if (_rootElement != null) { @@ -164,7 +162,7 @@ namespace Avalonia.Controls.Primitives //TODO TabStop private void DataGridRowHeader_PointerPressed(object sender, PointerPressedEventArgs e) { - if(e.MouseButton != MouseButton.Left) + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { return; } 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 b014c699bb..6e0703c90f 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs @@ -269,6 +269,9 @@ namespace Avalonia.Controls.Primitives // Since we didn't know the final widths of the columns until we resized, // we waited until now to measure each cell double leftEdge = 0; + if (autoSizeHeight) + DesiredHeight = 0; + foreach (DataGridColumn column in OwningGrid.ColumnsInternal.GetVisibleColumns()) { DataGridCell cell = OwningRow.Cells[column.Index]; @@ -307,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/Primitives/DataGridFrozenGrid.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs index 060922238d..9feca71cda 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls.Primitives /// public class DataGridFrozenGrid : Grid { - public static readonly AvaloniaProperty IsFrozenProperty = + public static readonly StyledProperty IsFrozenProperty = AvaloniaProperty.RegisterAttached("IsFrozen"); /// diff --git a/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs b/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs index f15442addf..82b01e99bb 100644 --- a/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs @@ -1,11 +1,8 @@ -// 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 System.Reflection; using System.Runtime.CompilerServices; using Avalonia.Metadata; -[assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests")] +[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid.UnitTests")] [assembly: InternalsVisibleTo("Avalonia.DesignerSupport")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] diff --git a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml index 7b6870fec3..17e7ecba43 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml @@ -45,7 +45,7 @@ - + + + + + + + + + + + + + + > + + + + + + + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs new file mode 100644 index 0000000000..ae70b59fde --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Specialized; +using Avalonia.Controls; +using Avalonia.Diagnostics.ViewModels; +using Avalonia.Input; +using Avalonia.LogicalTree; +using Avalonia.Markup.Xaml; +using Avalonia.Threading; + +namespace Avalonia.Diagnostics.Views +{ + internal class ConsoleView : UserControl + { + private readonly ListBox _historyList; + private readonly TextBox _input; + + public ConsoleView() + { + this.InitializeComponent(); + _historyList = this.FindControl("historyList"); + ((ILogical)_historyList).LogicalChildren.CollectionChanged += HistoryChanged; + _input = this.FindControl("input"); + _input.KeyDown += InputKeyDown; + } + + public void FocusInput() => _input.Focus(); + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void HistoryChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems[0] is IControl control) + { + DispatcherTimer.RunOnce(control.BringIntoView, TimeSpan.Zero); + } + } + + private void InputKeyDown(object sender, KeyEventArgs e) + { + var vm = (ConsoleViewModel)DataContext; + + switch (e.Key) + { + case Key.Enter: + vm.Execute(); + e.Handled = true; + break; + case Key.Up: + vm.HistoryUp(); + _input.CaretIndex = _input.Text.Length; + e.Handled = true; + break; + case Key.Down: + vm.HistoryDown(); + _input.CaretIndex = _input.Text.Length; + e.Handled = true; + break; + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml new file mode 100644 index 0000000000..29ce26fcdf --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs new file mode 100644 index 0000000000..c6bd5a18aa --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Avalonia.Diagnostics.Views +{ + internal class ControlDetailsView : UserControl + { + public ControlDetailsView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia.Diagnostics/Views/EventsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml similarity index 92% rename from src/Avalonia.Diagnostics/Views/EventsView.xaml rename to src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml index 406dd433a2..b7f0860e70 100644 --- a/src/Avalonia.Diagnostics/Views/EventsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml @@ -1,9 +1,10 @@  + xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters" + x:Class="Avalonia.Diagnostics.Views.EventsPageView"> - + ("events"); diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml new file mode 100644 index 0000000000..663722acba --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hold Ctrl+Shift over a control to inspect. + + Focused: + + + Pointer Over: + + + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs new file mode 100644 index 0000000000..783709e54b --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs @@ -0,0 +1,66 @@ +using Avalonia.Controls; +using Avalonia.Diagnostics.ViewModels; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Threading; + +namespace Avalonia.Diagnostics.Views +{ + internal class MainView : UserControl + { + private readonly ConsoleView _console; + private readonly GridSplitter _consoleSplitter; + private readonly Grid _rootGrid; + private readonly int _consoleRow; + private double _consoleHeight = -1; + + public MainView() + { + InitializeComponent(); + AddHandler(KeyDownEvent, PreviewKeyDown, RoutingStrategies.Tunnel); + _console = this.FindControl("console"); + _consoleSplitter = this.FindControl("consoleSplitter"); + _rootGrid = this.FindControl("rootGrid"); + _consoleRow = Grid.GetRow(_console); + } + + public void ToggleConsole() + { + var vm = (MainViewModel)DataContext; + + if (_consoleHeight == -1) + { + _consoleHeight = Bounds.Height / 3; + } + + vm.Console.ToggleVisibility(); + _consoleSplitter.IsVisible = vm.Console.IsVisible; + + if (vm.Console.IsVisible) + { + _rootGrid.RowDefinitions[_consoleRow].Height = new GridLength(_consoleHeight, GridUnitType.Pixel); + Dispatcher.UIThread.Post(() => _console.FocusInput(), DispatcherPriority.Background); + } + else + { + _consoleHeight = _rootGrid.RowDefinitions[_consoleRow].Height.Value; + _rootGrid.RowDefinitions[_consoleRow].Height = new GridLength(0, GridUnitType.Pixel); + } + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void PreviewKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Escape) + { + ToggleConsole(); + e.Handled = true; + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml new file mode 100644 index 0000000000..3623e95597 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs new file mode 100644 index 0000000000..3abdb5034a --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq; +using System.Reactive.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Diagnostics.ViewModels; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Markup.Xaml; +using Avalonia.Styling; +using Avalonia.VisualTree; + +namespace Avalonia.Diagnostics.Views +{ + internal class MainWindow : Window, IStyleHost + { + private TopLevel _root; + private IDisposable _keySubscription; + + public MainWindow() + { + InitializeComponent(); + + _keySubscription = InputManager.Instance.Process + .OfType() + .Subscribe(RawKeyDown); + } + + public TopLevel Root + { + get => _root; + set + { + if (_root != value) + { + _root = value; + DataContext = new MainViewModel(value); + } + } + } + + IStyleHost IStyleHost.StylingParent => null; + + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + _keySubscription.Dispose(); + ((MainViewModel)DataContext)?.Dispose(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void RawKeyDown(RawKeyEventArgs e) + { + const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift; + + if (e.Modifiers == modifiers) + { + var point = (Root as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default; + var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible)) + .FirstOrDefault(); + + if (control != null) + { + var vm = (MainViewModel)DataContext; + vm.SelectControl((IControl)control); + } + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml similarity index 61% rename from src/Avalonia.Diagnostics/Views/TreePageView.xaml rename to src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml index ca7314264a..4ddb320175 100644 --- a/src/Avalonia.Diagnostics/Views/TreePageView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml @@ -1,26 +1,29 @@ - - + + - - + + - - + + diff --git a/src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs similarity index 90% rename from src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs rename to src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs index 1326f718de..633d18ddd8 100644 --- a/src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs @@ -1,6 +1,3 @@ -// 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.Controls; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; @@ -12,14 +9,14 @@ using Avalonia.Media; namespace Avalonia.Diagnostics.Views { - public class TreePageView : UserControl + internal class TreePageView : UserControl { private Control _adorner; private TreeView _tree; public TreePageView() { - InitializeComponent(); + this.InitializeComponent(); _tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized; } @@ -39,7 +36,7 @@ namespace Avalonia.Diagnostics.Views _adorner = new Rectangle { Fill = new SolidColorBrush(0x80a0c5e8), - [AdornerLayer.AdornedElementProperty] = node.Visual + [AdornerLayer.AdornedElementProperty] = node.Visual, }; layer.Children.Add(_adorner); diff --git a/src/Avalonia.Diagnostics/VisualTreeDebug.cs b/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs similarity index 92% rename from src/Avalonia.Diagnostics/VisualTreeDebug.cs rename to src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs index 47511e0508..6f699339e7 100644 --- a/src/Avalonia.Diagnostics/VisualTreeDebug.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Text; using Avalonia.Controls; diff --git a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs deleted file mode 100644 index 4b832f7ce6..0000000000 --- a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ /dev/null @@ -1,25 +0,0 @@ -// 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 System.Collections.Generic; -using System.Linq; -using Avalonia.VisualTree; - -namespace Avalonia.Diagnostics.ViewModels -{ - internal class ControlDetailsViewModel : ViewModelBase - { - public ControlDetailsViewModel(IVisual control) - { - if (control is AvaloniaObject avaloniaObject) - { - Properties = AvaloniaPropertyRegistry.Instance.GetRegistered(avaloniaObject) - .Select(x => new PropertyDetails(avaloniaObject, x)) - .OrderBy(x => x.IsAttached) - .ThenBy(x => x.Name); - } - } - - public IEnumerable Properties { get; } - } -} diff --git a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs deleted file mode 100644 index 9f524a21eb..0000000000 --- a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs +++ /dev/null @@ -1,76 +0,0 @@ -// 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 System; -using System.Collections.ObjectModel; -using System.Linq; -using Avalonia.Controls; -using Avalonia.Input; - -namespace Avalonia.Diagnostics.ViewModels -{ - internal class DevToolsViewModel : ViewModelBase - { - private IDevToolViewModel _selectedTool; - private string _focusedControl; - private string _pointerOverElement; - - public DevToolsViewModel(IControl root) - { - Tools = new ObservableCollection - { - new TreePageViewModel(LogicalTreeNode.Create(root), "Logical Tree"), - new TreePageViewModel(VisualTreeNode.Create(root), "Visual Tree"), - new EventsViewModel(root) - }; - - SelectedTool = Tools.First(); - - UpdateFocusedControl(); - - KeyboardDevice.Instance.PropertyChanged += (s, e) => - { - if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement)) - { - UpdateFocusedControl(); - } - }; - - root.GetObservable(TopLevel.PointerOverElementProperty) - .Subscribe(x => PointerOverElement = x?.GetType().Name); - } - - public IDevToolViewModel SelectedTool - { - get => _selectedTool; - set => RaiseAndSetIfChanged(ref _selectedTool, value); - } - - public ObservableCollection Tools { get; } - - public string FocusedControl - { - get => _focusedControl; - private set => RaiseAndSetIfChanged(ref _focusedControl, value); - } - - public string PointerOverElement - { - get => _pointerOverElement; - private set => RaiseAndSetIfChanged(ref _pointerOverElement, value); - } - - public void SelectControl(IControl control) - { - if (SelectedTool is TreePageViewModel tree) - { - tree.SelectControl(control); - } - } - - private void UpdateFocusedControl() - { - FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name; - } - } -} diff --git a/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs deleted file mode 100644 index 0434230a63..0000000000 --- a/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs +++ /dev/null @@ -1,16 +0,0 @@ -// 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. - -namespace Avalonia.Diagnostics.ViewModels -{ - /// - /// View model interface for tool showing up in DevTools - /// - public interface IDevToolViewModel - { - /// - /// Name of a tool. - /// - string Name { get; } - } -} diff --git a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs b/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs deleted file mode 100644 index 523be406c8..0000000000 --- a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs +++ /dev/null @@ -1,58 +0,0 @@ -// 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 System; -using Avalonia.Data; - -namespace Avalonia.Diagnostics.ViewModels -{ - internal class PropertyDetails : ViewModelBase - { - private object _value; - private string _priority; - private string _diagnostic; - - public PropertyDetails(AvaloniaObject o, AvaloniaProperty property) - { - Name = property.IsAttached ? - $"[{property.OwnerType.Name}.{property.Name}]" : - property.Name; - IsAttached = property.IsAttached; - - // TODO: Unsubscribe when view model is deactivated. - o.GetObservable(property).Subscribe(x => - { - var diagnostic = o.GetDiagnostic(property); - Value = diagnostic.Value ?? "(null)"; - Priority = (diagnostic.Priority != BindingPriority.Unset) ? - diagnostic.Priority.ToString() : - diagnostic.Property.Inherits ? - "Inherited" : - "Unset"; - Diagnostic = diagnostic.Diagnostic; - }); - } - - public string Name { get; } - - public bool IsAttached { get; } - - public string Priority - { - get => _priority; - private set => RaiseAndSetIfChanged(ref _priority, value); - } - - public string Diagnostic - { - get => _diagnostic; - private set => RaiseAndSetIfChanged(ref _diagnostic, value); - } - - public object Value - { - get => _value; - private set => RaiseAndSetIfChanged(ref _value, value); - } - } -} diff --git a/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs deleted file mode 100644 index 902eb81bd9..0000000000 --- a/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs +++ /dev/null @@ -1,83 +0,0 @@ -// 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 System; -using System.Collections.Specialized; -using System.Reactive; -using System.Reactive.Linq; -using Avalonia.Collections; -using Avalonia.Styling; -using Avalonia.VisualTree; - -namespace Avalonia.Diagnostics.ViewModels -{ - internal class TreeNode : ViewModelBase - { - private string _classes; - private bool _isExpanded; - - public TreeNode(IVisual visual, TreeNode parent) - { - Parent = parent; - Type = visual.GetType().Name; - Visual = visual; - - if (visual is IStyleable styleable) - { - var classesChanged = Observable.FromEventPattern< - NotifyCollectionChangedEventHandler, - NotifyCollectionChangedEventArgs>( - x => styleable.Classes.CollectionChanged += x, - x => styleable.Classes.CollectionChanged -= x) - .TakeUntil(styleable.StyleDetach); - - classesChanged.Select(_ => Unit.Default) - .StartWith(Unit.Default) - .Subscribe(_ => - { - if (styleable.Classes.Count > 0) - { - Classes = "(" + string.Join(" ", styleable.Classes) + ")"; - } - else - { - Classes = string.Empty; - } - }); - } - } - - public IAvaloniaReadOnlyList Children - { - get; - protected set; - } - - public string Classes - { - get => _classes; - private set => RaiseAndSetIfChanged(ref _classes, value); - } - - public IVisual Visual - { - get; - } - - public bool IsExpanded - { - get => _isExpanded; - set => RaiseAndSetIfChanged(ref _isExpanded, value); - } - - public TreeNode Parent - { - get; - } - - public string Type - { - get; - } - } -} diff --git a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs deleted file mode 100644 index fb867ab55e..0000000000 --- a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs +++ /dev/null @@ -1,75 +0,0 @@ -// 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 System; -using System.Collections.Generic; -using System.Reactive.Linq; -using Avalonia.Controls; -using Avalonia.Diagnostics.ViewModels; -using Avalonia.Media; - -namespace Avalonia.Diagnostics.Views -{ - internal class ControlDetailsView : UserControl - { - private static readonly StyledProperty ViewModelProperty = - AvaloniaProperty.Register(nameof(ViewModel)); - - private SimpleGrid _grid; - - public ControlDetailsView() - { - InitializeComponent(); - this.GetObservable(DataContextProperty) - .Subscribe(x => ViewModel = (ControlDetailsViewModel)x); - } - - public ControlDetailsViewModel ViewModel - { - get => GetValue(ViewModelProperty); - private set - { - SetValue(ViewModelProperty, value); - _grid[GridRepeater.ItemsProperty] = value?.Properties; - } - } - - private void InitializeComponent() - { - Func> pt = PropertyTemplate; - - Content = new ScrollViewer { Content = _grid = new SimpleGrid { [GridRepeater.TemplateProperty] = pt } }; - } - - private IEnumerable PropertyTemplate(object i) - { - var property = (PropertyDetails)i; - - var margin = new Thickness(2); - - yield return new TextBlock - { - Margin = margin, - Text = property.Name, - TextWrapping = TextWrapping.NoWrap, - [!ToolTip.TipProperty] = property.GetObservable(nameof(property.Diagnostic)).ToBinding() - }; - - yield return new TextBlock - { - Margin = margin, - TextWrapping = TextWrapping.NoWrap, - [!TextBlock.TextProperty] = property.GetObservable(nameof(property.Value)) - .Select(v => v?.ToString()) - .ToBinding() - }; - - yield return new TextBlock - { - Margin = margin, - TextWrapping = TextWrapping.NoWrap, - [!TextBlock.TextProperty] = property.GetObservable((nameof(property.Priority))).ToBinding() - }; - } - } -} diff --git a/src/Avalonia.Diagnostics/Views/GridRepeater.cs b/src/Avalonia.Diagnostics/Views/GridRepeater.cs deleted file mode 100644 index b0ff26c7b6..0000000000 --- a/src/Avalonia.Diagnostics/Views/GridRepeater.cs +++ /dev/null @@ -1,51 +0,0 @@ -// 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 System; -using System.Collections; -using System.Collections.Generic; -using Avalonia.Controls; - -namespace Avalonia.Diagnostics.Views -{ - internal static class GridRepeater - { - public static readonly AttachedProperty ItemsProperty = - AvaloniaProperty.RegisterAttached("Items", typeof(GridRepeater)); - - public static readonly AttachedProperty>> TemplateProperty = - AvaloniaProperty.RegisterAttached>>("Template", - typeof(GridRepeater)); - - static GridRepeater() - { - ItemsProperty.Changed.Subscribe(ItemsChanged); - } - - private static void ItemsChanged(AvaloniaPropertyChangedEventArgs e) - { - var grid = (SimpleGrid)e.Sender; - var items = (IEnumerable)e.NewValue; - var template = grid.GetValue(TemplateProperty); - - grid.Children.Clear(); - - if (items != null) - { - int count = 0; - int cols = 3; - - foreach (var item in items) - { - foreach (var control in template(item)) - { - grid.Children.Add(control); - SimpleGrid.SetColumn(control, count % cols); - SimpleGrid.SetRow(control, count / cols); - ++count; - } - } - } - } - } -} diff --git a/src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs b/src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs deleted file mode 100644 index 24b2f29463..0000000000 --- a/src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -// 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 System; -using System.ComponentModel; -using System.Reactive.Linq; -using System.Reflection; - -namespace Avalonia.Diagnostics.Views -{ - internal static class PropertyChangedExtensions - { - public static IObservable GetObservable(this INotifyPropertyChanged source, string propertyName) - { - Contract.Requires(source != null); - Contract.Requires(propertyName != null); - - var property = source.GetType().GetTypeInfo().GetDeclaredProperty(propertyName); - - if (property == null) - { - throw new ArgumentException($"Property '{propertyName}' not found on '{source}."); - } - - return Observable.FromEventPattern( - e => source.PropertyChanged += e, - e => source.PropertyChanged -= e) - .Where(e => e.EventArgs.PropertyName == propertyName) - .Select(_ => (T)property.GetValue(source)) - .StartWith((T)property.GetValue(source)); - } - } -} diff --git a/src/Avalonia.Diagnostics/Views/SimpleGrid.cs b/src/Avalonia.Diagnostics/Views/SimpleGrid.cs deleted file mode 100644 index 4fc77666e1..0000000000 --- a/src/Avalonia.Diagnostics/Views/SimpleGrid.cs +++ /dev/null @@ -1,146 +0,0 @@ -// 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 System.Collections.Generic; -using Avalonia.Controls; - -namespace Avalonia.Diagnostics.Views -{ - /// - /// A simple grid control that lays out columns with a equal width and rows to their desired - /// size. - /// - /// - /// This is used in the devtools because our performance sucks. - /// - public class SimpleGrid : Panel - { - private readonly List _columnWidths = new List(); - private readonly List _rowHeights = new List(); - private double _totalWidth; - private double _totalHeight; - - /// - /// Defines the Column attached property. - /// - public static readonly AttachedProperty ColumnProperty = - AvaloniaProperty.RegisterAttached("Column"); - - /// - /// Defines the Row attached property. - /// - public static readonly AttachedProperty RowProperty = - AvaloniaProperty.RegisterAttached("Row"); - - /// - /// Gets the value of the Column attached property for a control. - /// - /// The control. - /// The control's column. - public static int GetColumn(IControl control) - { - return control.GetValue(ColumnProperty); - } - - /// - /// Gets the value of the Row attached property for a control. - /// - /// The control. - /// The control's row. - public static int GetRow(IControl control) - { - return control.GetValue(RowProperty); - } - - /// - /// Sets the value of the Column attached property for a control. - /// - /// The control. - /// The column value. - public static void SetColumn(IControl control, int value) - { - control.SetValue(ColumnProperty, value); - } - - - /// - /// Sets the value of the Row attached property for a control. - /// - /// The control. - /// The row value. - public static void SetRow(IControl control, int value) - { - control.SetValue(RowProperty, value); - } - - protected override Size MeasureOverride(Size availableSize) - { - _columnWidths.Clear(); - _rowHeights.Clear(); - _totalWidth = 0; - _totalHeight = 0; - - foreach (var child in Children) - { - var column = GetColumn(child); - var row = GetRow(child); - - child.Measure(availableSize); - - var desired = child.DesiredSize; - UpdateCell(_columnWidths, column, desired.Width, ref _totalWidth); - UpdateCell(_rowHeights, row, desired.Height, ref _totalHeight); - } - - return new Size(_totalWidth, _totalHeight); - } - - protected override Size ArrangeOverride(Size finalSize) - { - var columnWidth = finalSize.Width / _columnWidths.Count; - - foreach (var child in Children) - { - var column = GetColumn(child); - var row = GetRow(child); - var rect = new Rect(column * columnWidth, GetRowTop(row), columnWidth, _rowHeights[row]); - child.Arrange(rect); - } - - return new Size(finalSize.Width, _totalHeight); - } - - private double UpdateCell(IList cells, int cell, double value, ref double total) - { - while (cells.Count < cell + 1) - { - cells.Add(0); - } - - var existing = cells[cell]; - - if (value > existing) - { - cells[cell] = value; - total += value - existing; - return value; - } - else - { - return existing; - } - } - - private double GetRowTop(int row) - { - var result = 0.0; - - for (var i = 0; i < row; ++i) - { - result += _rowHeights[i]; - } - - return result; - } - } -} diff --git a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml new file mode 100644 index 0000000000..e51cc2f3ce --- /dev/null +++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs b/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs index b967b40c0d..7f29407ed5 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs +++ b/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs @@ -34,7 +34,7 @@ namespace Avalonia.Dialogs return; } - var isQuickLink = _quickLinksRoot.IsLogicalParentOf(e.Source as Control); + var isQuickLink = _quickLinksRoot.IsLogicalAncestorOf(e.Source as Control); if (e.ClickCount == 2 || isQuickLink) { if (model.ItemType == ManagedFileChooserItemType.File) @@ -81,7 +81,7 @@ namespace Avalonia.Dialogs if (indexOfPreselected > 1) { - _filesView.ScrollIntoView(model.Items[indexOfPreselected - 1]); + _filesView.ScrollIntoView(indexOfPreselected - 1); } } } diff --git a/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs b/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs index 03e05e7a75..28d40f13b9 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs +++ b/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs @@ -14,6 +14,7 @@ namespace Avalonia.Dialogs { internal class ManagedFileChooserViewModel : InternalViewModelBase { + private readonly ManagedFileDialogOptions _options; public event Action CancelRequested; public event Action CompleteRequested; @@ -103,8 +104,9 @@ namespace Avalonia.Dialogs QuickLinks.AddRange(quickSources.GetAllItems().Select(i => new ManagedFileChooserItemViewModel(i))); } - public ManagedFileChooserViewModel(FileSystemDialog dialog) + public ManagedFileChooserViewModel(FileSystemDialog dialog, ManagedFileDialogOptions options) { + _options = options; _disposables = new CompositeDisposable(); var quickSources = AvaloniaLocator.Current @@ -134,7 +136,7 @@ namespace Avalonia.Dialogs : dialog is OpenFolderDialog ? "Select directory" : throw new ArgumentException(nameof(dialog))); - var directory = dialog.InitialDirectory; + var directory = dialog.Directory; if (directory == null || !Directory.Exists(directory)) { @@ -202,15 +204,22 @@ namespace Avalonia.Dialogs } else { - var invalidItems = SelectedItems.Where(i => i.ItemType == ManagedFileChooserItemType.Folder).ToList(); - foreach (var item in invalidItems) + if (!_options.AllowDirectorySelection) { - SelectedItems.Remove(item); + var invalidItems = SelectedItems.Where(i => i.ItemType == ManagedFileChooserItemType.Folder) + .ToList(); + foreach (var item in invalidItems) + SelectedItems.Remove(item); } if (!_selectingDirectory) { - FileName = SelectedItems.FirstOrDefault()?.DisplayName; + var selectedItem = SelectedItems.FirstOrDefault(); + + if (selectedItem != null) + { + FileName = selectedItem.DisplayName; + } } } } diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs index 771d2b1b5e..f9e62d905b 100644 --- a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs +++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs @@ -1,25 +1,23 @@ -using System; using System.Linq; using System.Threading.Tasks; -using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Platform; -using Avalonia.Dialogs; -using Avalonia.Platform; namespace Avalonia.Dialogs { public static class ManagedFileDialogExtensions { - class ManagedSystemDialogImpl : ISystemDialogImpl where T : Window, new() + private class ManagedSystemDialogImpl : ISystemDialogImpl where T : Window, new() { - async Task Show(SystemDialog d, IWindowImpl parent) + async Task Show(SystemDialog d, Window parent, ManagedFileDialogOptions options = null) { - var model = new ManagedFileChooserViewModel((FileSystemDialog)d); + var model = new ManagedFileChooserViewModel((FileSystemDialog)d, + options ?? new ManagedFileDialogOptions()); var dialog = new T { Content = new ManagedFileChooser(), + Title = d.Title, DataContext = model }; @@ -39,15 +37,20 @@ namespace Avalonia.Dialogs return result; } - public async Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + public async Task ShowFileDialogAsync(FileDialog dialog, Window parent) { return await Show(dialog, parent); } - public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) { return (await Show(dialog, parent))?.FirstOrDefault(); } + + public async Task ShowFileDialogAsync(FileDialog dialog, Window parent, ManagedFileDialogOptions options) + { + return await Show(dialog, parent, options); + } } public static TAppBuilder UseManagedSystemDialogs(this TAppBuilder builder) @@ -65,5 +68,14 @@ namespace Avalonia.Dialogs AvaloniaLocator.CurrentMutable.Bind().ToSingleton>()); return builder; } + + public static Task ShowManagedAsync(this OpenFileDialog dialog, Window parent, + ManagedFileDialogOptions options = null) => ShowManagedAsync(dialog, parent, options); + + public static Task ShowManagedAsync(this OpenFileDialog dialog, Window parent, + ManagedFileDialogOptions options = null) where TWindow : Window, new() + { + return new ManagedSystemDialogImpl().ShowFileDialogAsync(dialog, parent, options); + } } } diff --git a/src/Avalonia.Dialogs/ManagedFileDialogOptions.cs b/src/Avalonia.Dialogs/ManagedFileDialogOptions.cs new file mode 100644 index 0000000000..56a3eb9122 --- /dev/null +++ b/src/Avalonia.Dialogs/ManagedFileDialogOptions.cs @@ -0,0 +1,7 @@ +namespace Avalonia.Dialogs +{ + public class ManagedFileDialogOptions + { + public bool AllowDirectorySelection { get; set; } + } +} diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 90239b5a49..e93ca64d3a 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.IO; using System.Reactive.Disposables; using System.Threading.Tasks; using Avalonia.Controls; @@ -184,7 +185,7 @@ namespace Avalonia.FreeDesktop private static string[] AllProperties = new[] { - "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display" + "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data" }; object GetProperty((NativeMenuItemBase item, NativeMenu menu) i, string name) @@ -210,7 +211,7 @@ namespace Avalonia.FreeDesktop return null; if (item.Menu != null && item.Menu.Items.Count == 0) return false; - if (item.Enabled == false) + if (item.IsEnabled == false) return false; return null; } @@ -234,6 +235,30 @@ namespace Avalonia.FreeDesktop return new[] { lst.ToArray() }; } + if (name == "toggle-type") + { + if (item.ToggleType == NativeMenuItemToggleType.CheckBox) + return "checkmark"; + if (item.ToggleType == NativeMenuItemToggleType.Radio) + return "radio"; + } + + if (name == "toggle-state") + { + if (item.ToggleType != NativeMenuItemToggleType.None) + return item.IsChecked ? 1 : 0; + } + + if (name == "icon-data") + { + if (item.Icon != null) + { + var ms = new MemoryStream(); + item.Icon.Save(ms); + return ms.ToArray(); + } + } + if (name == "children-display") return menu != null ? "submenu" : null; } @@ -319,10 +344,10 @@ namespace Avalonia.FreeDesktop { var item = GetMenu(id).item; - if (item is NativeMenuItem menuItem) + if (item is NativeMenuItem menuItem && item is INativeMenuItemExporterEventsImplBridge bridge) { - if (menuItem?.Enabled == true) - menuItem.RaiseClick(); + if (menuItem?.IsEnabled == true) + bridge?.RaiseClicked(); } } } diff --git a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs index 8081528e55..f9737b461d 100644 --- a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs @@ -47,7 +47,8 @@ namespace Avalonia.FreeDesktop var fProcMounts = File.ReadAllLines(ProcMountsDir) .Select(x => x.Split(' ')) - .Select(x => (x[0], x[1])); + .Select(x => (x[0], x[1])) + .Where(x => !x.Item2.StartsWith("/snap/", StringComparison.InvariantCultureIgnoreCase)); var labelDirEnum = Directory.Exists(DevByLabelDir) ? new DirectoryInfo(DevByLabelDir).GetFiles() : Enumerable.Empty(); diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index 9e4b2b84e0..96f0bb59b3 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Linq; @@ -140,7 +137,7 @@ namespace Avalonia.Input /// The event args. protected virtual void OnPreviewKeyDown(object sender, KeyEventArgs e) { - if (e.Key == Key.LeftAlt) + if (e.Key == Key.LeftAlt || e.Key == Key.RightAlt) { _altIsDown = true; @@ -182,7 +179,7 @@ namespace Avalonia.Input { bool menuIsOpen = MainMenu?.IsOpen == true; - if ((e.Modifiers & InputModifiers.Alt) != 0 || menuIsOpen) + if ((e.KeyModifiers & KeyModifiers.Alt) != 0 || menuIsOpen) { // If any other key is pressed with the Alt key held down, or the main menu is open, // find all controls who have registered that access key. @@ -218,6 +215,7 @@ namespace Avalonia.Input switch (e.Key) { case Key.LeftAlt: + case Key.RightAlt: _altIsDown = false; if (_ignoreAltUp) diff --git a/src/Avalonia.Input/Cursors.cs b/src/Avalonia.Input/Cursors.cs index f72ccf1850..920b598eac 100644 --- a/src/Avalonia.Input/Cursors.cs +++ b/src/Avalonia.Input/Cursors.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Platform; diff --git a/src/Avalonia.Input/DragDrop.cs b/src/Avalonia.Input/DragDrop.cs index d39659cee3..723d577964 100644 --- a/src/Avalonia.Input/DragDrop.cs +++ b/src/Avalonia.Input/DragDrop.cs @@ -23,7 +23,7 @@ namespace Avalonia.Input /// public static readonly RoutedEvent DropEvent = RoutedEvent.Register("Drop", RoutingStrategies.Bubble, typeof(DragDrop)); - public static readonly AvaloniaProperty AllowDropProperty = AvaloniaProperty.RegisterAttached("AllowDrop", typeof(DragDrop), inherits: true); + public static readonly AttachedProperty AllowDropProperty = AvaloniaProperty.RegisterAttached("AllowDrop", typeof(DragDrop), inherits: true); /// /// Gets a value indicating whether the given element can be used as the target of a drag-and-drop operation. diff --git a/src/Avalonia.Input/DragDropDevice.cs b/src/Avalonia.Input/DragDropDevice.cs index 0b9f09b224..bcd962bc31 100644 --- a/src/Avalonia.Input/DragDropDevice.cs +++ b/src/Avalonia.Input/DragDropDevice.cs @@ -11,7 +11,7 @@ namespace Avalonia.Input private Interactive _lastTarget = null; - private Interactive GetTarget(IInputElement root, Point local) + private Interactive GetTarget(IInputRoot root, Point local) { var target = root.InputHitTest(local)?.GetSelfAndVisualAncestors()?.OfType()?.FirstOrDefault(); if (target != null && DragDrop.GetAllowDrop(target)) @@ -19,7 +19,7 @@ namespace Avalonia.Input return null; } - private DragDropEffects RaiseDragEvent(Interactive target, IInputElement inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data, InputModifiers modifiers) + private DragDropEffects RaiseDragEvent(Interactive target, IInputRoot inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data, KeyModifiers modifiers) { if (target == null) return DragDropEffects.None; @@ -38,13 +38,13 @@ namespace Avalonia.Input return args.DragEffects; } - private DragDropEffects DragEnter(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) + private DragDropEffects DragEnter(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, KeyModifiers modifiers) { _lastTarget = GetTarget(inputRoot, point); return RaiseDragEvent(_lastTarget, inputRoot, point, DragDrop.DragEnterEvent, effects, data, modifiers); } - private DragDropEffects DragOver(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) + private DragDropEffects DragOver(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, KeyModifiers modifiers) { var target = GetTarget(inputRoot, point); @@ -77,7 +77,7 @@ namespace Avalonia.Input } } - private DragDropEffects Drop(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) + private DragDropEffects Drop(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, KeyModifiers modifiers) { try { @@ -100,16 +100,16 @@ namespace Avalonia.Input switch (e.Type) { case RawDragEventType.DragEnter: - e.Effects = DragEnter(e.InputRoot, e.Location, e.Data, e.Effects, e.Modifiers); + e.Effects = DragEnter(e.Root, e.Location, e.Data, e.Effects, e.KeyModifiers); break; case RawDragEventType.DragOver: - e.Effects = DragOver(e.InputRoot, e.Location, e.Data, e.Effects, e.Modifiers); + e.Effects = DragOver(e.Root, e.Location, e.Data, e.Effects, e.KeyModifiers); break; case RawDragEventType.DragLeave: - DragLeave(e.InputRoot); + DragLeave(e.Root); break; case RawDragEventType.Drop: - e.Effects = Drop(e.InputRoot, e.Location, e.Data, e.Effects, e.Modifiers); + e.Effects = Drop(e.Root, e.Location, e.Data, e.Effects, e.KeyModifiers); break; } } diff --git a/src/Avalonia.Input/DragEventArgs.cs b/src/Avalonia.Input/DragEventArgs.cs index dc0b76b225..22ca8358ff 100644 --- a/src/Avalonia.Input/DragEventArgs.cs +++ b/src/Avalonia.Input/DragEventArgs.cs @@ -13,8 +13,11 @@ namespace Avalonia.Input public IDataObject Data { get; private set; } + [Obsolete("Use KeyModifiers")] public InputModifiers Modifiers { get; private set; } + public KeyModifiers KeyModifiers { get; private set; } + public Point GetPosition(IVisual relativeTo) { var point = new Point(0, 0); @@ -32,13 +35,27 @@ namespace Avalonia.Input return point; } + [Obsolete("Use constructor taking KeyModifiers")] public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, InputModifiers modifiers) : base(routedEvent) { - this.Data = data; - this._target = target; - this._targetLocation = targetLocation; - this.Modifiers = modifiers; + Data = data; + _target = target; + _targetLocation = targetLocation; + Modifiers = modifiers; + KeyModifiers = (KeyModifiers)(((int)modifiers) & 0xF); + } + + public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers) + : base(routedEvent) + { + Data = data; + _target = target; + _targetLocation = targetLocation; + KeyModifiers = keyModifiers; +#pragma warning disable CS0618 // Type or member is obsolete + Modifiers = (InputModifiers)keyModifiers; +#pragma warning restore CS0618 // Type or member is obsolete } } diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index 80e18cb7bf..66355da8b9 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/src/Avalonia.Input/FocusManager.cs @@ -1,9 +1,7 @@ -// 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 System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using Avalonia.Interactivity; using Avalonia.VisualTree; @@ -17,8 +15,8 @@ namespace Avalonia.Input /// /// The focus scopes in which the focus is currently defined. /// - private readonly Dictionary _focusScopes = - new Dictionary(); + private readonly ConditionalWeakTable _focusScopes = + new ConditionalWeakTable(); /// /// Initializes a new instance of the class. @@ -55,11 +53,11 @@ namespace Avalonia.Input /// /// The control to focus. /// The method by which focus was changed. - /// Any input modifiers active at the time of focus. + /// Any key modifiers active at the time of focus. public void Focus( IInputElement control, NavigationMethod method = NavigationMethod.Unspecified, - InputModifiers modifiers = InputModifiers.None) + KeyModifiers keyModifiers = KeyModifiers.None) { if (control != null) { @@ -69,7 +67,7 @@ namespace Avalonia.Input if (scope != null) { Scope = scope; - SetFocusedElement(scope, control, method, modifiers); + SetFocusedElement(scope, control, method, keyModifiers); } } else if (Current != null) @@ -97,7 +95,7 @@ namespace Avalonia.Input /// The focus scope. /// The element to focus. May be null. /// The method by which focus was changed. - /// Any input modifiers active at the time of focus. + /// Any key modifiers active at the time of focus. /// /// If the specified scope is the current then the keyboard focus /// will change. @@ -106,15 +104,26 @@ namespace Avalonia.Input IFocusScope scope, IInputElement element, NavigationMethod method = NavigationMethod.Unspecified, - InputModifiers modifiers = InputModifiers.None) + KeyModifiers keyModifiers = KeyModifiers.None) { Contract.Requires(scope != null); - _focusScopes[scope] = element; + if (_focusScopes.TryGetValue(scope, out IInputElement existingElement)) + { + if (element != existingElement) + { + _focusScopes.Remove(scope); + _focusScopes.Add(scope, element); + } + } + else + { + _focusScopes.Add(scope, element); + } if (Scope == scope) { - KeyboardDevice.Instance?.SetFocusedElement(element, method, modifiers); + KeyboardDevice.Instance?.SetFocusedElement(element, method, keyModifiers); } } @@ -159,7 +168,7 @@ namespace Avalonia.Input { var scope = control as IFocusScope; - if (scope != null) + if (scope != null && control.VisualRoot?.IsVisible == true) { yield return scope; } @@ -177,21 +186,22 @@ namespace Avalonia.Input private static void OnPreviewPointerPressed(object sender, RoutedEventArgs e) { var ev = (PointerPressedEventArgs)e; + var visual = (IVisual)sender; - if (sender == e.Source && ev.MouseButton == MouseButton.Left) + if (sender == e.Source && ev.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) { - var element = (ev.Device?.Captured as IInputElement) ?? (e.Source as IInputElement); + IVisual element = ev.Pointer?.Captured ?? e.Source as IInputElement; - if (element == null || !CanFocus(element)) + while (element != null) { - element = element.GetSelfAndVisualAncestors() - .OfType() - .FirstOrDefault(CanFocus); - } + if (element is IInputElement inputElement && CanFocus(inputElement)) + { + Instance?.Focus(inputElement, NavigationMethod.Pointer, ev.KeyModifiers); - if (element != null) - { - Instance?.Focus(element, NavigationMethod.Pointer, ev.InputModifiers); + break; + } + + element = element.VisualParent; } } } diff --git a/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs b/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs index 91b224e65a..112abb1a4e 100644 --- a/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs +++ b/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs @@ -111,9 +111,7 @@ namespace Avalonia.Input.GestureRecognizers _pointerGrabs.Remove(e.Pointer); foreach (var r in _recognizers) { - if(e.Handled) - break; - r.PointerCaptureLost(e); + r.PointerCaptureLost(e.Pointer); } } @@ -121,6 +119,11 @@ namespace Avalonia.Input.GestureRecognizers { pointer.Capture(_inputElement); _pointerGrabs[pointer] = recognizer; + foreach (var r in _recognizers) + { + if (r != recognizer) + r.PointerCaptureLost(pointer); + } } } diff --git a/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs index b8ba9e529c..c1d9ae5304 100644 --- a/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs +++ b/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs @@ -6,7 +6,7 @@ namespace Avalonia.Input.GestureRecognizers void PointerPressed(PointerPressedEventArgs e); void PointerReleased(PointerReleasedEventArgs e); void PointerMoved(PointerEventArgs e); - void PointerCaptureLost(PointerCaptureLostEventArgs e); + void PointerCaptureLost(IPointer pointer); } public interface IGestureRecognizerActionsDispatcher diff --git a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs index 4f3c7c0bba..e022401c8e 100644 --- a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -116,9 +116,9 @@ namespace Avalonia.Input.GestureRecognizers } } - public void PointerCaptureLost(PointerCaptureLostEventArgs e) + public void PointerCaptureLost(IPointer pointer) { - if (e.Pointer == _tracking) EndGesture(); + if (pointer == _tracking) EndGesture(); } void EndGesture() @@ -148,6 +148,7 @@ namespace Avalonia.Input.GestureRecognizers EndGesture(); else { + _tracking = null; var savedGestureId = _gestureId; var st = Stopwatch.StartNew(); var lastTime = TimeSpan.Zero; diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index a5bd4feb64..0efc20b196 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -1,8 +1,6 @@ -// 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 System; using Avalonia.Interactivity; +using Avalonia.VisualTree; namespace Avalonia.Input { @@ -74,12 +72,13 @@ namespace Avalonia.Input if (ev.Route == RoutingStrategies.Bubble) { var e = (PointerPressedEventArgs)ev; + var visual = (IVisual)ev.Source; if (e.ClickCount <= 1) { s_lastPress = new WeakReference(e.Source); } - else if (s_lastPress != null && e.ClickCount == 2 && e.MouseButton != MouseButton.Right) + else if (s_lastPress != null && e.ClickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) { if (s_lastPress.TryGetTarget(out var target) && target == e.Source) { diff --git a/src/Avalonia.Input/GotFocusEventArgs.cs b/src/Avalonia.Input/GotFocusEventArgs.cs index 01e978a55c..9d958823fe 100644 --- a/src/Avalonia.Input/GotFocusEventArgs.cs +++ b/src/Avalonia.Input/GotFocusEventArgs.cs @@ -1,6 +1,4 @@ -// 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 System; using Avalonia.Interactivity; namespace Avalonia.Input @@ -18,6 +16,16 @@ namespace Avalonia.Input /// /// Gets or sets any input modifiers active at the time of focus. /// - public InputModifiers InputModifiers { get; set; } + [Obsolete("Use KeyModifiers")] + public InputModifiers InputModifiers + { + get => (InputModifiers)KeyModifiers; + set => KeyModifiers = (KeyModifiers)((int)value & 0xF); + } + + /// + /// Gets or sets any key modifiers active at the time of focus. + /// + public KeyModifiers KeyModifiers { get; set; } } } diff --git a/src/Avalonia.Input/IAccessKeyHandler.cs b/src/Avalonia.Input/IAccessKeyHandler.cs index 4ad4ed4c8a..3e6510320f 100644 --- a/src/Avalonia.Input/IAccessKeyHandler.cs +++ b/src/Avalonia.Input/IAccessKeyHandler.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Input { /// diff --git a/src/Avalonia.Input/ICloseable.cs b/src/Avalonia.Input/ICloseable.cs index b25e29360a..3d6b95ddf4 100644 --- a/src/Avalonia.Input/ICloseable.cs +++ b/src/Avalonia.Input/ICloseable.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Input diff --git a/src/Avalonia.Input/ICustomKeyboardNavigation.cs b/src/Avalonia.Input/ICustomKeyboardNavigation.cs index ff4c2094b3..3d2927c632 100644 --- a/src/Avalonia.Input/ICustomKeyboardNavigation.cs +++ b/src/Avalonia.Input/ICustomKeyboardNavigation.cs @@ -1,7 +1,4 @@ -// 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. - - + namespace Avalonia.Input { /// diff --git a/src/Avalonia.Input/IFocusManager.cs b/src/Avalonia.Input/IFocusManager.cs index 29a22b86b2..9122cc428d 100644 --- a/src/Avalonia.Input/IFocusManager.cs +++ b/src/Avalonia.Input/IFocusManager.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Input { /// @@ -23,11 +20,11 @@ namespace Avalonia.Input /// /// The control to focus. /// The method by which focus was changed. - /// Any input modifiers active at the time of focus. + /// Any key modifiers active at the time of focus. void Focus( - IInputElement control, + IInputElement control, NavigationMethod method = NavigationMethod.Unspecified, - InputModifiers modifiers = InputModifiers.None); + KeyModifiers keyModifiers = KeyModifiers.None); /// /// Notifies the focus manager of a change in focus scope. diff --git a/src/Avalonia.Input/IFocusScope.cs b/src/Avalonia.Input/IFocusScope.cs index 97b97e0157..56f558040e 100644 --- a/src/Avalonia.Input/IFocusScope.cs +++ b/src/Avalonia.Input/IFocusScope.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Input { public interface IFocusScope diff --git a/src/Avalonia.Input/IInputDevice.cs b/src/Avalonia.Input/IInputDevice.cs index 72fa2ab9bf..ab0fae65df 100644 --- a/src/Avalonia.Input/IInputDevice.cs +++ b/src/Avalonia.Input/IInputDevice.cs @@ -1,6 +1,3 @@ -// 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.Input.Raw; namespace Avalonia.Input diff --git a/src/Avalonia.Input/IInputElement.cs b/src/Avalonia.Input/IInputElement.cs index 9247fb48a9..c30d74c965 100644 --- a/src/Avalonia.Input/IInputElement.cs +++ b/src/Avalonia.Input/IInputElement.cs @@ -1,5 +1,3 @@ -// 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 System; using System.Collections.Generic; using Avalonia.Interactivity; @@ -63,7 +61,7 @@ namespace Avalonia.Input event EventHandler PointerReleased; /// - /// Occurs when the mouse wheen is scrolled over the control. + /// Occurs when the mouse wheel is scrolled over the control. /// event EventHandler PointerWheelChanged; diff --git a/src/Avalonia.Input/IInputManager.cs b/src/Avalonia.Input/IInputManager.cs index cdd72a7f3e..80b71d3e47 100644 --- a/src/Avalonia.Input/IInputManager.cs +++ b/src/Avalonia.Input/IInputManager.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Input.Raw; diff --git a/src/Avalonia.Input/IInputRoot.cs b/src/Avalonia.Input/IInputRoot.cs index 6b3e1e6bc5..eeb7e4323e 100644 --- a/src/Avalonia.Input/IInputRoot.cs +++ b/src/Avalonia.Input/IInputRoot.cs @@ -1,6 +1,3 @@ -// 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 JetBrains.Annotations; namespace Avalonia.Input diff --git a/src/Avalonia.Input/IKeyboardDevice.cs b/src/Avalonia.Input/IKeyboardDevice.cs index 1a82f7d671..ba7e0484ee 100644 --- a/src/Avalonia.Input/IKeyboardDevice.cs +++ b/src/Avalonia.Input/IKeyboardDevice.cs @@ -1,6 +1,3 @@ -// 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 System; using System.ComponentModel; @@ -48,6 +45,8 @@ namespace Avalonia.Input LeftMouseButton = 16, RightMouseButton = 32, MiddleMouseButton = 64, + XButton1MouseButton = 128, + XButton2MouseButton = 256, KeyboardMask = Alt | Control | Shift | Meta } @@ -64,6 +63,6 @@ namespace Avalonia.Input void SetFocusedElement( IInputElement element, NavigationMethod method, - InputModifiers modifiers); + KeyModifiers modifiers); } } diff --git a/src/Avalonia.Input/IKeyboardNavigationHandler.cs b/src/Avalonia.Input/IKeyboardNavigationHandler.cs index db3a3cf114..88d00b3b50 100644 --- a/src/Avalonia.Input/IKeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/IKeyboardNavigationHandler.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Input { /// @@ -22,10 +19,10 @@ namespace Avalonia.Input /// /// The current element. /// The direction to move. - /// Any input modifiers active at the time of focus. + /// Any key modifiers active at the time of focus. void Move( IInputElement element, NavigationDirection direction, - InputModifiers modifiers = InputModifiers.None); + KeyModifiers keyModifiers = KeyModifiers.None); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Input/IMainMenu.cs b/src/Avalonia.Input/IMainMenu.cs index a3373191a8..781b176f5c 100644 --- a/src/Avalonia.Input/IMainMenu.cs +++ b/src/Avalonia.Input/IMainMenu.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Interactivity; using Avalonia.VisualTree; diff --git a/src/Avalonia.Input/IMouseDevice.cs b/src/Avalonia.Input/IMouseDevice.cs index a641544f7a..272d1eb8d7 100644 --- a/src/Avalonia.Input/IMouseDevice.cs +++ b/src/Avalonia.Input/IMouseDevice.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Input diff --git a/src/Avalonia.Input/INavigableContainer.cs b/src/Avalonia.Input/INavigableContainer.cs index df434bca70..c52f3ac404 100644 --- a/src/Avalonia.Input/INavigableContainer.cs +++ b/src/Avalonia.Input/INavigableContainer.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Input { /// diff --git a/src/Avalonia.Input/IPointerDevice.cs b/src/Avalonia.Input/IPointerDevice.cs index de775d90f2..bf001dda15 100644 --- a/src/Avalonia.Input/IPointerDevice.cs +++ b/src/Avalonia.Input/IPointerDevice.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.VisualTree; diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 535b930f8b..407b28b665 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -1,9 +1,8 @@ -// 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 System; using System.Collections.Generic; using System.Linq; +using Avalonia.Controls; +using Avalonia.Data; using Avalonia.Input.GestureRecognizers; using Avalonia.Interactivity; using Avalonia.VisualTree; @@ -181,10 +180,11 @@ namespace Avalonia.Input PointerReleasedEvent.AddClassHandler((x, e) => x.OnPointerReleased(e)); PointerCaptureLostEvent.AddClassHandler((x, e) => x.OnPointerCaptureLost(e)); PointerWheelChangedEvent.AddClassHandler((x, e) => x.OnPointerWheelChanged(e)); + } - PseudoClass(IsEffectivelyEnabledProperty, x => !x, ":disabled"); - PseudoClass(IsFocusedProperty, ":focus"); - PseudoClass(IsPointerOverProperty, ":pointerover"); + public InputElement() + { + UpdatePseudoClasses(IsFocused, IsPointerOver); } /// @@ -342,7 +342,7 @@ namespace Avalonia.Input } /// - /// Gets or sets a value indicating whether the control is focused. + /// Gets a value indicating whether the control is focused. /// public bool IsFocused { @@ -360,7 +360,7 @@ namespace Avalonia.Input } /// - /// Gets or sets a value indicating whether the pointer is currently over the control. + /// Gets a value indicating whether the pointer is currently over the control. /// public bool IsPointerOver { @@ -372,7 +372,11 @@ namespace Avalonia.Input public bool IsEffectivelyEnabled { get => _isEffectivelyEnabled; - private set => SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value); + private set + { + SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value); + PseudoClasses.Set(":disabled", !value); + } } public List KeyBindings { get; } = new List(); @@ -522,6 +526,20 @@ namespace Avalonia.Input { } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == IsFocusedProperty) + { + UpdatePseudoClasses(change.NewValue.GetValueOrDefault(), null); + } + else if (change.Property == IsPointerOverProperty) + { + UpdatePseudoClasses(null, change.NewValue.GetValueOrDefault()); + } + } + /// /// Updates the property value according to the parent /// control's enabled state and . @@ -578,5 +596,18 @@ namespace Avalonia.Input child?.UpdateIsEffectivelyEnabled(this); } } + + private void UpdatePseudoClasses(bool? isFocused, bool? isPointerOver) + { + if (isFocused.HasValue) + { + PseudoClasses.Set(":focus", isFocused.Value); + } + + if (isPointerOver.HasValue) + { + PseudoClasses.Set(":pointerover", isPointerOver.Value); + } + } } } diff --git a/src/Avalonia.Input/InputExtensions.cs b/src/Avalonia.Input/InputExtensions.cs index f83c41e266..4babe711f2 100644 --- a/src/Avalonia.Input/InputExtensions.cs +++ b/src/Avalonia.Input/InputExtensions.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Linq; @@ -38,7 +35,9 @@ namespace Avalonia.Input /// The topmost at the specified position. public static IInputElement InputHitTest(this IInputElement element, Point p) { - return element.GetInputElementsAt(p).FirstOrDefault(); + Contract.Requires(element != null); + + return element.GetVisualAt(p, s_hitTestDelegate) as IInputElement; } private static bool IsHitTestVisible(IVisual visual) diff --git a/src/Avalonia.Input/InputManager.cs b/src/Avalonia.Input/InputManager.cs index 4fbdfd56c0..129b9cf474 100644 --- a/src/Avalonia.Input/InputManager.cs +++ b/src/Avalonia.Input/InputManager.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Reactive.Subjects; using Avalonia.Input.Raw; diff --git a/src/Avalonia.Input/Key.cs b/src/Avalonia.Input/Key.cs index d51b990fd4..33e1e5df45 100644 --- a/src/Avalonia.Input/Key.cs +++ b/src/Avalonia.Input/Key.cs @@ -1,5 +1,3 @@ -// 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. #if AVALONIA_REMOTE_PROTOCOL namespace Avalonia.Remote.Protocol.Input #else diff --git a/src/Avalonia.Input/KeyEventArgs.cs b/src/Avalonia.Input/KeyEventArgs.cs index 8aef81503f..267376262b 100644 --- a/src/Avalonia.Input/KeyEventArgs.cs +++ b/src/Avalonia.Input/KeyEventArgs.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Interactivity; diff --git a/src/Avalonia.Input/KeyGesture.cs b/src/Avalonia.Input/KeyGesture.cs index 490c31bef9..ad447794bc 100644 --- a/src/Avalonia.Input/KeyGesture.cs +++ b/src/Avalonia.Input/KeyGesture.cs @@ -1,9 +1,8 @@ -// 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 System; +using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; +using System.Text; namespace Avalonia.Input { @@ -108,19 +107,43 @@ namespace Avalonia.Input public override string ToString() { - var parts = new List(); + var s = new StringBuilder(); - foreach (var flag in Enum.GetValues(typeof(KeyModifiers)).Cast()) + static void Plus(StringBuilder s) { - if (KeyModifiers.HasFlag(flag) && flag != KeyModifiers.None) + if (s.Length > 0) { - parts.Add(flag.ToString()); + s.Append("+"); } } - parts.Add(Key.ToString()); + if (KeyModifiers.HasFlagCustom(KeyModifiers.Control)) + { + s.Append("Ctrl"); + } + + if (KeyModifiers.HasFlagCustom(KeyModifiers.Shift)) + { + Plus(s); + s.Append("Shift"); + } + + if (KeyModifiers.HasFlagCustom(KeyModifiers.Alt)) + { + Plus(s); + s.Append("Alt"); + } + + if (KeyModifiers.HasFlagCustom(KeyModifiers.Meta)) + { + Plus(s); + s.Append("Cmd"); + } + + Plus(s); + s.Append(Key); - return string.Join(" + ", parts); + return s.ToString(); } public bool Matches(KeyEventArgs keyEvent) => ResolveNumPadOperationKey(keyEvent.Key) == Key && keyEvent.KeyModifiers == KeyModifiers; @@ -141,7 +164,9 @@ namespace Avalonia.Input return KeyModifiers.Control; } - if (modifier.Equals("cmd".AsSpan(), StringComparison.OrdinalIgnoreCase)) + if (modifier.Equals("cmd".AsSpan(), StringComparison.OrdinalIgnoreCase) || + modifier.Equals("win".AsSpan(), StringComparison.OrdinalIgnoreCase) || + modifier.Equals("⌘".AsSpan(), StringComparison.OrdinalIgnoreCase)) { return KeyModifiers.Meta; } diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index a02c580ad2..0321b0bdf3 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -1,6 +1,3 @@ -// 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 System.ComponentModel; using System.Runtime.CompilerServices; using Avalonia.Input.Raw; @@ -38,7 +35,7 @@ namespace Avalonia.Input public void SetFocusedElement( IInputElement element, NavigationMethod method, - InputModifiers modifiers) + KeyModifiers keyModifiers) { if (element != FocusedElement) { @@ -56,7 +53,7 @@ namespace Avalonia.Input { RoutedEvent = InputElement.GotFocusEvent, NavigationMethod = method, - InputModifiers = modifiers, + KeyModifiers = keyModifiers, }); } } @@ -70,66 +67,60 @@ namespace Avalonia.Input { if(e.Handled) return; - IInputElement element = FocusedElement; - if (element != null) - { - var keyInput = e as RawKeyEventArgs; + var element = FocusedElement ?? e.Root; - if (keyInput != null) + if (e is RawKeyEventArgs keyInput) + { + switch (keyInput.Type) { - switch (keyInput.Type) - { - case RawKeyEventType.KeyDown: - case RawKeyEventType.KeyUp: - var routedEvent = keyInput.Type == RawKeyEventType.KeyDown - ? InputElement.KeyDownEvent - : InputElement.KeyUpEvent; - - KeyEventArgs ev = new KeyEventArgs - { - RoutedEvent = routedEvent, - Device = this, - Key = keyInput.Key, - KeyModifiers = KeyModifiersUtils.ConvertToKey(keyInput.Modifiers), - Source = element, - }; - - IVisual currentHandler = element; - while (currentHandler != null && !ev.Handled && keyInput.Type == RawKeyEventType.KeyDown) - { - var bindings = (currentHandler as IInputElement)?.KeyBindings; - if(bindings!=null) - foreach (var binding in bindings) - { - if(ev.Handled) - break; - binding.TryHandle(ev); - } - currentHandler = currentHandler.VisualParent; - } - - element.RaiseEvent(ev); - e.Handled = ev.Handled; - break; - } + case RawKeyEventType.KeyDown: + case RawKeyEventType.KeyUp: + var routedEvent = keyInput.Type == RawKeyEventType.KeyDown + ? InputElement.KeyDownEvent + : InputElement.KeyUpEvent; + + KeyEventArgs ev = new KeyEventArgs + { + RoutedEvent = routedEvent, + Device = this, + Key = keyInput.Key, + KeyModifiers = KeyModifiersUtils.ConvertToKey(keyInput.Modifiers), + Source = element, + }; + + IVisual currentHandler = element; + while (currentHandler != null && !ev.Handled && keyInput.Type == RawKeyEventType.KeyDown) + { + var bindings = (currentHandler as IInputElement)?.KeyBindings; + if (bindings != null) + foreach (var binding in bindings) + { + if (ev.Handled) + break; + binding.TryHandle(ev); + } + currentHandler = currentHandler.VisualParent; + } + + element.RaiseEvent(ev); + e.Handled = ev.Handled; + break; } + } - var text = e as RawTextInputEventArgs; - - if (text != null) + if (e is RawTextInputEventArgs text) + { + var ev = new TextInputEventArgs() { - var ev = new TextInputEventArgs() - { - Device = this, - Text = text.Text, - Source = element, - RoutedEvent = InputElement.TextInputEvent - }; - - element.RaiseEvent(ev); - e.Handled = ev.Handled; - } + Device = this, + Text = text.Text, + Source = element, + RoutedEvent = InputElement.TextInputEvent + }; + + element.RaiseEvent(ev); + e.Handled = ev.Handled; } } } diff --git a/src/Avalonia.Input/KeyboardNavigation.cs b/src/Avalonia.Input/KeyboardNavigation.cs index 0277876e24..722215f8b7 100644 --- a/src/Avalonia.Input/KeyboardNavigation.cs +++ b/src/Avalonia.Input/KeyboardNavigation.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Input { /// @@ -33,6 +30,19 @@ namespace Avalonia.Input "TabOnceActiveElement", typeof(KeyboardNavigation)); + + /// + /// Defines the IsTabStop attached property. + /// + /// + /// The IsTabStop attached property determines whether the control is focusable by tab navigation. + /// + public static readonly AttachedProperty IsTabStopProperty = + AvaloniaProperty.RegisterAttached( + "IsTabStop", + typeof(KeyboardNavigation), + true); + /// /// Gets the for a container. /// @@ -72,5 +82,25 @@ namespace Avalonia.Input { element.SetValue(TabOnceActiveElementProperty, value); } + + /// + /// Sets the for a container. + /// + /// The container. + /// Value indicating whether the container is a tab stop. + public static void SetIsTabStop(InputElement element, bool value) + { + element.SetValue(IsTabStopProperty, value); + } + + /// + /// Gets the for a container. + /// + /// The container. + /// Whether the container is a tab stop. + public static bool GetIsTabStop(InputElement element) + { + return element.GetValue(IsTabStopProperty); + } } } diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs index 32faf98603..c425eeeedb 100644 --- a/src/Avalonia.Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Linq; using Avalonia.Input.Navigation; @@ -94,11 +91,11 @@ namespace Avalonia.Input /// /// The current element. /// The direction to move. - /// Any input modifiers active at the time of focus. + /// Any key modifiers active at the time of focus. public void Move( IInputElement element, NavigationDirection direction, - InputModifiers modifiers = InputModifiers.None) + KeyModifiers keyModifiers = KeyModifiers.None) { Contract.Requires(element != null); @@ -109,7 +106,7 @@ namespace Avalonia.Input var method = direction == NavigationDirection.Next || direction == NavigationDirection.Previous ? NavigationMethod.Tab : NavigationMethod.Directional; - FocusManager.Instance.Focus(next, method, modifiers); + FocusManager.Instance.Focus(next, method, keyModifiers); } } @@ -124,9 +121,9 @@ namespace Avalonia.Input if (current != null && e.Key == Key.Tab) { - var direction = (e.Modifiers & InputModifiers.Shift) == 0 ? + var direction = (e.KeyModifiers & KeyModifiers.Shift) == 0 ? NavigationDirection.Next : NavigationDirection.Previous; - Move(current, direction, e.Modifiers); + Move(current, direction, e.KeyModifiers); e.Handled = true; } } diff --git a/src/Avalonia.Input/KeyboardNavigationMode.cs b/src/Avalonia.Input/KeyboardNavigationMode.cs index c6ad7ecbaf..41e778bf49 100644 --- a/src/Avalonia.Input/KeyboardNavigationMode.cs +++ b/src/Avalonia.Input/KeyboardNavigationMode.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Input { /// diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index c84596b913..188ddd9835 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Linq; using System.Reactive.Linq; @@ -14,13 +11,14 @@ namespace Avalonia.Input /// /// Represents a mouse device. /// - public class MouseDevice : IMouseDevice + public class MouseDevice : IMouseDevice, IDisposable { private int _clickCount; private Rect _lastClickRect; private ulong _lastClickTime; private readonly Pointer _pointer; + private bool _disposed; public MouseDevice(Pointer pointer = null) { @@ -119,6 +117,10 @@ namespace Avalonia.Input rv++; if (props.IsRightButtonPressed) rv++; + if (props.IsXButton1Pressed) + rv++; + if (props.IsXButton2Pressed) + rv++; return rv; } @@ -126,7 +128,9 @@ namespace Avalonia.Input { Contract.Requires(e != null); - var mouse = (IMouseDevice)e.Device; + var mouse = (MouseDevice)e.Device; + if(mouse._disposed) + return; Position = e.Root.PointToScreen(e.Position); var props = CreateProperties(e); @@ -139,6 +143,8 @@ namespace Avalonia.Input case RawPointerEventType.LeftButtonDown: case RawPointerEventType.RightButtonDown: case RawPointerEventType.MiddleButtonDown: + case RawPointerEventType.XButton1Down: + case RawPointerEventType.XButton2Down: if (ButtonCount(props) > 1) e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); else @@ -148,6 +154,8 @@ namespace Avalonia.Input case RawPointerEventType.LeftButtonUp: case RawPointerEventType.RightButtonUp: case RawPointerEventType.MiddleButtonUp: + case RawPointerEventType.XButton1Up: + case RawPointerEventType.XButton2Up: if (ButtonCount(props) != 0) e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); else @@ -183,12 +191,20 @@ namespace Avalonia.Input kind = PointerUpdateKind.MiddleButtonPressed; if (args.Type == RawPointerEventType.RightButtonDown) kind = PointerUpdateKind.RightButtonPressed; + if (args.Type == RawPointerEventType.XButton1Down) + kind = PointerUpdateKind.XButton1Pressed; + if (args.Type == RawPointerEventType.XButton2Down) + kind = PointerUpdateKind.XButton2Pressed; if (args.Type == RawPointerEventType.LeftButtonUp) kind = PointerUpdateKind.LeftButtonReleased; if (args.Type == RawPointerEventType.MiddleButtonUp) kind = PointerUpdateKind.MiddleButtonReleased; if (args.Type == RawPointerEventType.RightButtonUp) kind = PointerUpdateKind.RightButtonReleased; + if (args.Type == RawPointerEventType.XButton1Up) + kind = PointerUpdateKind.XButton1Released; + if (args.Type == RawPointerEventType.XButton2Up) + kind = PointerUpdateKind.XButton2Released; return new PointerPointProperties(args.InputModifiers, kind); } @@ -441,5 +457,11 @@ namespace Avalonia.Input el = (IInputElement)el.VisualParent; } } + + public void Dispose() + { + _disposed = true; + _pointer?.Dispose(); + } } } diff --git a/src/Avalonia.Input/Navigation/FocusExtensions.cs b/src/Avalonia.Input/Navigation/FocusExtensions.cs index 794dc63f84..83bcd9187a 100644 --- a/src/Avalonia.Input/Navigation/FocusExtensions.cs +++ b/src/Avalonia.Input/Navigation/FocusExtensions.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Input.Navigation { /// diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index 18db2a9173..dd50ea438a 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Linq; @@ -80,7 +77,8 @@ namespace Avalonia.Input.Navigation /// The element. /// The tab direction. Must be Next or Previous. /// The element's focusable descendants. - private static IEnumerable GetFocusableDescendants(IInputElement element, NavigationDirection direction) + private static IEnumerable GetFocusableDescendants(IInputElement element, + NavigationDirection direction) { var mode = KeyboardNavigation.GetTabNavigation((InputElement)element); @@ -116,7 +114,7 @@ namespace Avalonia.Input.Navigation } else { - if (child.CanFocus()) + if (child.CanFocus() && KeyboardNavigation.GetIsTabStop((InputElement)child)) { yield return child; } @@ -125,7 +123,10 @@ namespace Avalonia.Input.Navigation { foreach (var descendant in GetFocusableDescendants(child, direction)) { - yield return descendant; + if (KeyboardNavigation.GetIsTabStop((InputElement)descendant)) + { + yield return descendant; + } } } } @@ -170,7 +171,9 @@ namespace Avalonia.Input.Navigation { element = navigable.GetControl(direction, element, false); - if (element != null && element.CanFocus()) + if (element != null && + element.CanFocus() && + KeyboardNavigation.GetIsTabStop((InputElement) element)) { break; } @@ -236,26 +239,22 @@ namespace Avalonia.Input.Navigation return customNext.next; } - if (sibling.CanFocus()) + if (sibling.CanFocus() && KeyboardNavigation.GetIsTabStop((InputElement) sibling)) { return sibling; } - else + + next = direction == NavigationDirection.Next ? + GetFocusableDescendants(sibling, direction).FirstOrDefault() : + GetFocusableDescendants(sibling, direction).LastOrDefault(); + + if (next != null) { - next = direction == NavigationDirection.Next ? - GetFocusableDescendants(sibling, direction).FirstOrDefault() : - GetFocusableDescendants(sibling, direction).LastOrDefault(); - if(next != null) - { - return next; - } + return next; } } - if (next == null) - { - next = GetFirstInNextContainer(element, parent, direction); - } + next = GetFirstInNextContainer(element, parent, direction); } else { @@ -267,7 +266,8 @@ namespace Avalonia.Input.Navigation return next; } - private static (bool handled, IInputElement next) GetCustomNext(IInputElement element, NavigationDirection direction) + private static (bool handled, IInputElement next) GetCustomNext(IInputElement element, + NavigationDirection direction) { if (element is ICustomKeyboardNavigation custom) { diff --git a/src/Avalonia.Input/NavigationDirection.cs b/src/Avalonia.Input/NavigationDirection.cs index 406890b767..9b9af0b0a6 100644 --- a/src/Avalonia.Input/NavigationDirection.cs +++ b/src/Avalonia.Input/NavigationDirection.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Input { /// @@ -100,12 +97,12 @@ namespace Avalonia.Input /// public static NavigationDirection? ToNavigationDirection( this Key key, - InputModifiers modifiers = InputModifiers.None) + KeyModifiers modifiers = KeyModifiers.None) { switch (key) { case Key.Tab: - return (modifiers & InputModifiers.Shift) != 0 ? + return (modifiers & KeyModifiers.Shift) == 0 ? NavigationDirection.Next : NavigationDirection.Previous; case Key.Up: return NavigationDirection.Up; diff --git a/src/Avalonia.Input/NavigationMethod.cs b/src/Avalonia.Input/NavigationMethod.cs index a8185da52a..ff46be6c4f 100644 --- a/src/Avalonia.Input/NavigationMethod.cs +++ b/src/Avalonia.Input/NavigationMethod.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Input { /// diff --git a/src/Avalonia.Input/Platform/IClipboard.cs b/src/Avalonia.Input/Platform/IClipboard.cs index 4afb69f509..eb880904eb 100644 --- a/src/Avalonia.Input/Platform/IClipboard.cs +++ b/src/Avalonia.Input/Platform/IClipboard.cs @@ -1,6 +1,3 @@ -// 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 System.Threading.Tasks; namespace Avalonia.Input.Platform @@ -12,5 +9,11 @@ namespace Avalonia.Input.Platform Task SetTextAsync(string text); Task ClearAsync(); + + Task SetDataObjectAsync(IDataObject data); + + Task GetFormatsAsync(); + + Task GetDataAsync(string format); } } diff --git a/src/Avalonia.Input/Platform/IStandardCursorFactory.cs b/src/Avalonia.Input/Platform/IStandardCursorFactory.cs index 393ba6b40a..51845cf03e 100644 --- a/src/Avalonia.Input/Platform/IStandardCursorFactory.cs +++ b/src/Avalonia.Input/Platform/IStandardCursorFactory.cs @@ -1,6 +1,3 @@ -// 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.Input; namespace Avalonia.Platform diff --git a/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs b/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs index a758a328be..053f894755 100644 --- a/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs +++ b/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs @@ -4,14 +4,14 @@ namespace Avalonia.Input.Platform { public class PlatformHotkeyConfiguration { - public PlatformHotkeyConfiguration() : this(InputModifiers.Control) + public PlatformHotkeyConfiguration() : this(KeyModifiers.Control) { } - public PlatformHotkeyConfiguration(InputModifiers commandModifiers, - InputModifiers selectionModifiers = InputModifiers.Shift, - InputModifiers wholeWordTextActionModifiers = InputModifiers.Control) + public PlatformHotkeyConfiguration(KeyModifiers commandModifiers, + KeyModifiers selectionModifiers = KeyModifiers.Shift, + KeyModifiers wholeWordTextActionModifiers = KeyModifiers.Control) { CommandModifiers = commandModifiers; SelectionModifiers = selectionModifiers; @@ -75,9 +75,9 @@ namespace Avalonia.Input.Platform }; } - public InputModifiers CommandModifiers { get; set; } - public InputModifiers WholeWordTextActionModifiers { get; set; } - public InputModifiers SelectionModifiers { get; set; } + public KeyModifiers CommandModifiers { get; set; } + public KeyModifiers WholeWordTextActionModifiers { get; set; } + public KeyModifiers SelectionModifiers { get; set; } public List Copy { get; set; } public List Cut { get; set; } public List Paste { get; set; } diff --git a/src/Avalonia.Input/Pointer.cs b/src/Avalonia.Input/Pointer.cs index 80d803abb1..00222e92cf 100644 --- a/src/Avalonia.Input/Pointer.cs +++ b/src/Avalonia.Input/Pointer.cs @@ -37,7 +37,7 @@ namespace Avalonia.Input { if (Captured != null) Captured.DetachedFromVisualTree -= OnCaptureDetached; - var oldCapture = control; + var oldCapture = Captured; Captured = control; PlatformCapture(control); if (oldCapture != null) @@ -55,9 +55,11 @@ namespace Avalonia.Input Captured.DetachedFromVisualTree += OnCaptureDetached; } - IInputElement GetNextCapture(IVisual parent) => - parent as IInputElement ?? parent.GetVisualAncestors().OfType().FirstOrDefault(); - + IInputElement GetNextCapture(IVisual parent) + { + return parent as IInputElement ?? parent.FindAncestorOfType(); + } + private void OnCaptureDetached(object sender, VisualTreeAttachmentEventArgs e) { Capture(GetNextCapture(e.Parent)); diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index e12a20f7a2..9cc42ffa69 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Input.Raw; using Avalonia.Interactivity; @@ -131,10 +128,10 @@ namespace Avalonia.Input _obsoleteClickCount = obsoleteClickCount; } - [Obsolete("Use DoubleTapped or DoubleRightTapped event instead")] + [Obsolete("Use DoubleTapped event or Gestures.DoubleRightTapped attached event")] public int ClickCount => _obsoleteClickCount; - [Obsolete("Use PointerUpdateKind")] + [Obsolete("Use PointerPressedEventArgs.GetCurrentPoint(this).Properties")] public MouseButton MouseButton => Properties.PointerUpdateKind.GetMouseButton(); } @@ -156,7 +153,7 @@ namespace Avalonia.Input /// public MouseButton InitialPressMouseButton { get; } - [Obsolete("Either use GetCurrentPoint(this).Properties.PointerUpdateKind or InitialPressMouseButton, see https://github.com/AvaloniaUI/Avalonia/wiki/Pointer-events-in-0.9 for more details", true)] + [Obsolete("Use InitialPressMouseButton")] public MouseButton MouseButton => InitialPressMouseButton; } diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index e9f3c02b7f..a316e0d964 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -18,18 +18,24 @@ namespace Avalonia.Input public bool IsLeftButtonPressed { get; } public bool IsMiddleButtonPressed { get; } public bool IsRightButtonPressed { get; } + public bool IsXButton1Pressed { get; } + public bool IsXButton2Pressed { get; } + public PointerUpdateKind PointerUpdateKind { get; } + private PointerPointProperties() - { - + { } public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind) { PointerUpdateKind = kind; - IsLeftButtonPressed = modifiers.HasFlag(RawInputModifiers.LeftMouseButton); - IsMiddleButtonPressed = modifiers.HasFlag(RawInputModifiers.MiddleMouseButton); - IsRightButtonPressed = modifiers.HasFlag(RawInputModifiers.RightMouseButton); + + IsLeftButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.LeftMouseButton); + IsMiddleButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.MiddleMouseButton); + IsRightButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.RightMouseButton); + IsXButton1Pressed = modifiers.HasFlagCustom(RawInputModifiers.XButton1MouseButton); + IsXButton2Pressed = modifiers.HasFlagCustom(RawInputModifiers.XButton2MouseButton); // The underlying input source might be reporting the previous state, // so make sure that we reflect the current state @@ -46,6 +52,14 @@ namespace Avalonia.Input IsRightButtonPressed = true; if (kind == PointerUpdateKind.RightButtonReleased) IsRightButtonPressed = false; + if (kind == PointerUpdateKind.XButton1Pressed) + IsXButton1Pressed = true; + if (kind == PointerUpdateKind.XButton1Released) + IsXButton1Pressed = false; + if (kind == PointerUpdateKind.XButton2Pressed) + IsXButton2Pressed = true; + if (kind == PointerUpdateKind.XButton2Released) + IsXButton2Pressed = false; } public static PointerPointProperties None { get; } = new PointerPointProperties(); @@ -56,9 +70,13 @@ namespace Avalonia.Input LeftButtonPressed, MiddleButtonPressed, RightButtonPressed, + XButton1Pressed, + XButton2Pressed, LeftButtonReleased, MiddleButtonReleased, RightButtonReleased, + XButton1Released, + XButton2Released, Other } diff --git a/src/Avalonia.Input/PointerWheelEventArgs.cs b/src/Avalonia.Input/PointerWheelEventArgs.cs index 3d7f93ddde..e5701dcf23 100644 --- a/src/Avalonia.Input/PointerWheelEventArgs.cs +++ b/src/Avalonia.Input/PointerWheelEventArgs.cs @@ -1,6 +1,3 @@ -// 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.Interactivity; using Avalonia.VisualTree; diff --git a/src/Avalonia.Input/Properties/AssemblyInfo.cs b/src/Avalonia.Input/Properties/AssemblyInfo.cs index 3a8d358931..433f821ca3 100644 --- a/src/Avalonia.Input/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Input/Properties/AssemblyInfo.cs @@ -1,6 +1,3 @@ -// 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 System.Reflection; using Avalonia.Metadata; diff --git a/src/Avalonia.Input/Raw/RawDragEvent.cs b/src/Avalonia.Input/Raw/RawDragEvent.cs index 1480f92f03..6e9ce20ff1 100644 --- a/src/Avalonia.Input/Raw/RawDragEvent.cs +++ b/src/Avalonia.Input/Raw/RawDragEvent.cs @@ -1,24 +1,29 @@ -namespace Avalonia.Input.Raw +using System; + +namespace Avalonia.Input.Raw { public class RawDragEvent : RawInputEventArgs { - public IInputElement InputRoot { get; } public Point Location { get; set; } public IDataObject Data { get; } public DragDropEffects Effects { get; set; } public RawDragEventType Type { get; } + [Obsolete("Use KeyModifiers")] public InputModifiers Modifiers { get; } + public KeyModifiers KeyModifiers { get; } public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type, - IInputElement inputRoot, Point location, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers) - :base(inputDevice, 0) + IInputRoot root, Point location, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers) + :base(inputDevice, 0, root) { Type = type; - InputRoot = inputRoot; Location = location; Data = data; Effects = effects; + KeyModifiers = KeyModifiersUtils.ConvertToKey(modifiers); +#pragma warning disable CS0618 // Type or member is obsolete Modifiers = (InputModifiers)modifiers; +#pragma warning restore CS0618 // Type or member is obsolete } } } diff --git a/src/Avalonia.Input/Raw/RawInputEventArgs.cs b/src/Avalonia.Input/Raw/RawInputEventArgs.cs index 78c1b58624..b85563b24a 100644 --- a/src/Avalonia.Input/Raw/RawInputEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawInputEventArgs.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Input.Raw @@ -21,12 +18,14 @@ namespace Avalonia.Input.Raw /// /// The associated device. /// The event timestamp. - public RawInputEventArgs(IInputDevice device, ulong timestamp) + /// The root from which the event originates. + public RawInputEventArgs(IInputDevice device, ulong timestamp, IInputRoot root) { Contract.Requires(device != null); Device = device; Timestamp = timestamp; + Root = root; } /// @@ -34,6 +33,11 @@ namespace Avalonia.Input.Raw /// public IInputDevice Device { get; } + /// + /// Gets the root from which the event originates. + /// + public IInputRoot Root { get; } + /// /// Gets or sets a value indicating whether the event was handled. /// diff --git a/src/Avalonia.Input/Raw/RawKeyEventArgs.cs b/src/Avalonia.Input/Raw/RawKeyEventArgs.cs index cd8b2eacf7..6ed073d006 100644 --- a/src/Avalonia.Input/Raw/RawKeyEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawKeyEventArgs.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Input.Raw { public enum RawKeyEventType @@ -14,9 +11,10 @@ namespace Avalonia.Input.Raw public RawKeyEventArgs( IKeyboardDevice device, ulong timestamp, + IInputRoot root, RawKeyEventType type, Key key, RawInputModifiers modifiers) - : base(device, timestamp) + : base(device, timestamp, root) { Key = key; Type = type; diff --git a/src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs b/src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs index 516234de7e..a752275980 100644 --- a/src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Input.Raw { diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index 88f6daf11f..62a1dd5d84 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Input.Raw @@ -14,6 +11,10 @@ namespace Avalonia.Input.Raw RightButtonUp, MiddleButtonDown, MiddleButtonUp, + XButton1Down, + XButton1Up, + XButton2Down, + XButton2Up, Move, Wheel, NonClientLeftButtonDown, @@ -44,22 +45,16 @@ namespace Avalonia.Input.Raw RawPointerEventType type, Point position, RawInputModifiers inputModifiers) - : base(device, timestamp) + : base(device, timestamp, root) { Contract.Requires(device != null); Contract.Requires(root != null); - Root = root; Position = position; Type = type; InputModifiers = inputModifiers; } - /// - /// Gets the root from which the event originates. - /// - public IInputRoot Root { get; } - /// /// Gets the mouse position, in client DIPs. /// @@ -68,7 +63,7 @@ namespace Avalonia.Input.Raw /// /// Gets the type of the event. /// - public RawPointerEventType Type { get; private set; } + public RawPointerEventType Type { get; set; } /// /// Gets the input modifiers. diff --git a/src/Avalonia.Input/Raw/RawSizeEventArgs.cs b/src/Avalonia.Input/Raw/RawSizeEventArgs.cs index 0a3074a859..b6a364eab9 100644 --- a/src/Avalonia.Input/Raw/RawSizeEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawSizeEventArgs.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Input.Raw diff --git a/src/Avalonia.Input/Raw/RawTextInputEventArgs.cs b/src/Avalonia.Input/Raw/RawTextInputEventArgs.cs index 0d1e5d2422..48c882197f 100644 --- a/src/Avalonia.Input/Raw/RawTextInputEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawTextInputEventArgs.cs @@ -1,15 +1,17 @@ -// 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. - namespace Avalonia.Input.Raw { public class RawTextInputEventArgs : RawInputEventArgs { - public string Text { get; set; } - - public RawTextInputEventArgs(IKeyboardDevice device, ulong timestamp, string text) : base(device, timestamp) + public RawTextInputEventArgs( + IKeyboardDevice device, + ulong timestamp, + IInputRoot root, + string text) + : base(device, timestamp, root) { Text = text; } + + public string Text { get; set; } } } diff --git a/src/Avalonia.Input/TextInputEventArgs.cs b/src/Avalonia.Input/TextInputEventArgs.cs index b7203b54d5..6e763d3b56 100644 --- a/src/Avalonia.Input/TextInputEventArgs.cs +++ b/src/Avalonia.Input/TextInputEventArgs.cs @@ -1,6 +1,3 @@ -// 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.Interactivity; namespace Avalonia.Input diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index b231c9fff4..d6ad836f37 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/src/Avalonia.Input/TouchDevice.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Avalonia.Input.Raw; @@ -11,10 +12,11 @@ namespace Avalonia.Input /// This class is supposed to be used on per-toplevel basis, don't use a shared one /// /// - public class TouchDevice : IInputDevice + public class TouchDevice : IInputDevice, IDisposable { - Dictionary _pointers = new Dictionary(); - + private readonly Dictionary _pointers = new Dictionary(); + private bool _disposed; + KeyModifiers GetKeyModifiers(RawInputModifiers modifiers) => (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask); @@ -28,6 +30,8 @@ namespace Avalonia.Input public void ProcessRawEvent(RawInputEventArgs ev) { + if(_disposed) + return; var args = (RawTouchEventArgs)ev; if (!_pointers.TryGetValue(args.TouchPointId, out var pointer)) { @@ -82,6 +86,17 @@ namespace Avalonia.Input } + + public void Dispose() + { + if(_disposed) + return; + var values = _pointers.Values.ToList(); + _pointers.Clear(); + _disposed = true; + foreach (var p in values) + p.Dispose(); + } } } diff --git a/src/Avalonia.Input/VectorEventArgs.cs b/src/Avalonia.Input/VectorEventArgs.cs index 11aa84e16b..000fd52f69 100644 --- a/src/Avalonia.Input/VectorEventArgs.cs +++ b/src/Avalonia.Input/VectorEventArgs.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Interactivity; diff --git a/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj b/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj index 66f1e8cc26..730ca2bd6e 100644 --- a/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj +++ b/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj @@ -1,6 +1,8 @@  netstandard2.0 + Enable + CS8600;CS8602;CS8603 @@ -9,6 +11,4 @@ - - - + \ No newline at end of file diff --git a/src/Avalonia.Interactivity/EventRoute.cs b/src/Avalonia.Interactivity/EventRoute.cs new file mode 100644 index 0000000000..85ba33d7ba --- /dev/null +++ b/src/Avalonia.Interactivity/EventRoute.cs @@ -0,0 +1,200 @@ +using System; +using Avalonia.Collections.Pooled; + +namespace Avalonia.Interactivity +{ + /// + /// Holds the route for a routed event and supports raising an event on that route. + /// + public class EventRoute : IDisposable + { + private readonly RoutedEvent _event; + private PooledList? _route; + + /// + /// Initializes a new instance of the class. + /// + /// The routed event to be raised. + public EventRoute(RoutedEvent e) + { + e = e ?? throw new ArgumentNullException(nameof(e)); + + _event = e; + _route = null; + } + + /// + /// Gets a value indicating whether the route has any handlers. + /// + public bool HasHandlers => _route?.Count > 0; + + /// + /// Adds a handler to the route. + /// + /// The target on which the event should be raised. + /// The handler for the event. + /// The routing strategies to listen to. + /// + /// If true the handler will be raised even when the routed event is marked as handled. + /// + /// + /// An optional adapter which if supplied, will be called with + /// and the parameters for the event. This adapter can be used to avoid calling + /// `DynamicInvoke` on the handler. + /// + public void Add( + IInteractive target, + Delegate handler, + RoutingStrategies routes, + bool handledEventsToo = false, + Action? adapter = null) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + handler = handler ?? throw new ArgumentNullException(nameof(handler)); + + _route ??= new PooledList(16); + _route.Add(new RouteItem(target, handler, adapter, routes, handledEventsToo)); + } + + /// + /// Adds a class handler to the route. + /// + /// The target on which the event should be raised. + public void AddClassHandler(IInteractive target) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + + _route ??= new PooledList(16); + _route.Add(new RouteItem(target, null, null, 0, false)); + } + + /// + /// Raises an event along the route. + /// + /// The event source. + /// The event args. + public void RaiseEvent(IInteractive source, RoutedEventArgs e) + { + source = source ?? throw new ArgumentNullException(nameof(source)); + e = e ?? throw new ArgumentNullException(nameof(e)); + + e.Source = source; + + if (_event.RoutingStrategies == RoutingStrategies.Direct) + { + e.Route = RoutingStrategies.Direct; + RaiseEventImpl(e); + _event.InvokeRouteFinished(e); + } + else + { + if (_event.RoutingStrategies.HasFlagCustom(RoutingStrategies.Tunnel)) + { + e.Route = RoutingStrategies.Tunnel; + RaiseEventImpl(e); + _event.InvokeRouteFinished(e); + } + + if (_event.RoutingStrategies.HasFlagCustom(RoutingStrategies.Bubble)) + { + e.Route = RoutingStrategies.Bubble; + RaiseEventImpl(e); + _event.InvokeRouteFinished(e); + } + } + } + + /// + /// Disposes of the event route. + /// + public void Dispose() + { + _route?.Dispose(); + _route = null; + } + + private void RaiseEventImpl(RoutedEventArgs e) + { + if (_route is null) + { + return; + } + + if (e.Source is null) + { + throw new ArgumentException("Event source may not be null", nameof(e)); + } + + IInteractive? lastTarget = null; + var start = 0; + var end = _route.Count; + var step = 1; + + if (e.Route == RoutingStrategies.Tunnel) + { + start = end - 1; + step = end = -1; + } + + for (var i = start; i != end; i += step) + { + var entry = _route[i]; + + // If we've got to a new control then call any RoutedEvent.Raised listeners. + if (entry.Target != lastTarget) + { + if (!e.Handled) + { + _event.InvokeRaised(entry.Target, e); + } + + // If this is a direct event and we've already raised events then we're finished. + if (e.Route == RoutingStrategies.Direct && lastTarget is object) + { + return; + } + + lastTarget = entry.Target; + } + + // Raise the event handler. + if (entry.Handler is object && + entry.Routes.HasFlagCustom(e.Route) && + (!e.Handled || entry.HandledEventsToo)) + { + if (entry.Adapter is object) + { + entry.Adapter(entry.Handler, entry.Target, e); + } + else + { + entry.Handler.DynamicInvoke(entry.Target, e); + } + } + } + } + + private readonly struct RouteItem + { + public RouteItem( + IInteractive target, + Delegate? handler, + Action? adapter, + RoutingStrategies routes, + bool handledEventsToo) + { + Target = target; + Handler = handler; + Adapter = adapter; + Routes = routes; + HandledEventsToo = handledEventsToo; + } + + public IInteractive Target { get; } + public Delegate? Handler { get; } + public Action? Adapter { get; } + public RoutingStrategies Routes { get; } + public bool HandledEventsToo { get; } + } + } +} diff --git a/src/Avalonia.Interactivity/EventSubscription.cs b/src/Avalonia.Interactivity/EventSubscription.cs deleted file mode 100644 index e8fb1bfaf1..0000000000 --- a/src/Avalonia.Interactivity/EventSubscription.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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 System; - -namespace Avalonia.Interactivity -{ - internal delegate void HandlerInvokeSignature(Delegate baseHandler, object sender, RoutedEventArgs args); - - internal class EventSubscription - { - public HandlerInvokeSignature InvokeAdapter { get; set; } - - public Delegate Handler { get; set; } - - public RoutingStrategies Routes { get; set; } - - public bool AlsoIfHandled { get; set; } - } -} diff --git a/src/Avalonia.Interactivity/IInteractive.cs b/src/Avalonia.Interactivity/IInteractive.cs index 47046b58e2..5c01f870ab 100644 --- a/src/Avalonia.Interactivity/IInteractive.cs +++ b/src/Avalonia.Interactivity/IInteractive.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Interactivity @@ -13,7 +10,7 @@ namespace Avalonia.Interactivity /// /// Gets the interactive parent of the object for bubbling and tunneling events. /// - IInteractive InteractiveParent { get; } + IInteractive? InteractiveParent { get; } /// /// Adds a handler for the specified routed event. @@ -23,7 +20,7 @@ namespace Avalonia.Interactivity /// The routing strategies to listen to. /// Whether handled events should also be listened for. /// A disposable that terminates the event subscription. - IDisposable AddHandler( + void AddHandler( RoutedEvent routedEvent, Delegate handler, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, @@ -38,7 +35,7 @@ namespace Avalonia.Interactivity /// The routing strategies to listen to. /// Whether handled events should also be listened for. /// A disposable that terminates the event subscription. - IDisposable AddHandler( + void AddHandler( RoutedEvent routedEvent, EventHandler handler, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, @@ -60,6 +57,13 @@ namespace Avalonia.Interactivity void RemoveHandler(RoutedEvent routedEvent, EventHandler handler) where TEventArgs : RoutedEventArgs; + /// + /// Adds the object's handlers for a routed event to an event route. + /// + /// The event. + /// The event route. + void AddToEventRoute(RoutedEvent routedEvent, EventRoute route); + /// /// Raises a routed event. /// diff --git a/src/Avalonia.Interactivity/Interactive.cs b/src/Avalonia.Interactivity/Interactive.cs index 27ece25183..bb7f0a8401 100644 --- a/src/Avalonia.Interactivity/Interactive.cs +++ b/src/Avalonia.Interactivity/Interactive.cs @@ -1,10 +1,5 @@ -// 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 System; using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; using Avalonia.Layout; using Avalonia.VisualTree; @@ -15,16 +10,12 @@ namespace Avalonia.Interactivity /// public class Interactive : Layoutable, IInteractive { - private Dictionary> _eventHandlers; - - private static readonly Dictionary s_invokeHandlerCache = new Dictionary(); + private Dictionary>? _eventHandlers; /// /// Gets the interactive parent of the object for bubbling and tunneling events. /// - IInteractive IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive; - - private Dictionary> EventHandlers => _eventHandlers ?? (_eventHandlers = new Dictionary>()); + IInteractive? IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive; /// /// Adds a handler for the specified routed event. @@ -33,24 +24,18 @@ namespace Avalonia.Interactivity /// The handler. /// The routing strategies to listen to. /// Whether handled events should also be listened for. - /// A disposable that terminates the event subscription. - public IDisposable AddHandler( + public void AddHandler( RoutedEvent routedEvent, Delegate handler, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, bool handledEventsToo = false) { - Contract.Requires(routedEvent != null); - Contract.Requires(handler != null); + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + handler = handler ?? throw new ArgumentNullException(nameof(handler)); - var subscription = new EventSubscription - { - Handler = handler, - Routes = routes, - AlsoIfHandled = handledEventsToo, - }; + var subscription = new EventSubscription(handler, routes, handledEventsToo); - return AddEventSubscription(routedEvent, subscription); + AddEventSubscription(routedEvent, subscription); } /// @@ -61,44 +46,26 @@ namespace Avalonia.Interactivity /// The handler. /// The routing strategies to listen to. /// Whether handled events should also be listened for. - /// A disposable that terminates the event subscription. - public IDisposable AddHandler( + public void AddHandler( RoutedEvent routedEvent, EventHandler handler, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, bool handledEventsToo = false) where TEventArgs : RoutedEventArgs { - Contract.Requires(routedEvent != null); - Contract.Requires(handler != null); - - // EventHandler delegate is not covariant, this forces us to create small wrapper - // that will cast our type erased instance and invoke it. - Type eventArgsType = routedEvent.EventArgsType; + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + handler = handler ?? throw new ArgumentNullException(nameof(handler)); - if (!s_invokeHandlerCache.TryGetValue(eventArgsType, out var invokeAdapter)) + static void InvokeAdapter(Delegate baseHandler, object sender, RoutedEventArgs args) { - void InvokeAdapter(Delegate baseHandler, object sender, RoutedEventArgs args) - { - var typedHandler = (EventHandler)baseHandler; - var typedArgs = (TEventArgs)args; + var typedHandler = (EventHandler)baseHandler; + var typedArgs = (TEventArgs)args; - typedHandler(sender, typedArgs); - } - - invokeAdapter = InvokeAdapter; - - s_invokeHandlerCache.Add(eventArgsType, invokeAdapter); + typedHandler(sender, typedArgs); } - var subscription = new EventSubscription - { - InvokeAdapter = invokeAdapter, - Handler = handler, - Routes = routes, - AlsoIfHandled = handledEventsToo, - }; + var subscription = new EventSubscription(handler, routes, handledEventsToo, (baseHandler, sender, args) => InvokeAdapter(baseHandler, sender, args)); - return AddEventSubscription(routedEvent, subscription); + AddEventSubscription(routedEvent, subscription); } /// @@ -108,14 +75,19 @@ namespace Avalonia.Interactivity /// The handler. public void RemoveHandler(RoutedEvent routedEvent, Delegate handler) { - Contract.Requires(routedEvent != null); - Contract.Requires(handler != null); - - List subscriptions = null; + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + handler = handler ?? throw new ArgumentNullException(nameof(handler)); - if (_eventHandlers?.TryGetValue(routedEvent, out subscriptions) == true) + if (_eventHandlers is object && + _eventHandlers.TryGetValue(routedEvent, out var subscriptions)) { - subscriptions.RemoveAll(x => x.Handler == handler); + for (var i = subscriptions.Count - 1; i >= 0; i--) + { + if (subscriptions[i].Handler == handler) + { + subscriptions.RemoveAt(i); + } + } } } @@ -137,191 +109,115 @@ namespace Avalonia.Interactivity /// The event args. public void RaiseEvent(RoutedEventArgs e) { - Contract.Requires(e != null); - - e.Source = e.Source ?? this; - - if (e.RoutedEvent.RoutingStrategies == RoutingStrategies.Direct) - { - e.Route = RoutingStrategies.Direct; - RaiseEventImpl(e); - e.RoutedEvent.InvokeRouteFinished(e); - } - - if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Tunnel) != 0) - { - TunnelEvent(e); - e.RoutedEvent.InvokeRouteFinished(e); - } + e = e ?? throw new ArgumentNullException(nameof(e)); - if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Bubble) != 0) + if (e.RoutedEvent == null) { - BubbleEvent(e); - e.RoutedEvent.InvokeRouteFinished(e); + throw new ArgumentException("Cannot raise an event whose RoutedEvent is null."); } - } - /// - /// Bubbles an event. - /// - /// The event args. - private void BubbleEvent(RoutedEventArgs e) - { - Contract.Requires(e != null); - - e.Route = RoutingStrategies.Bubble; - - var traverser = HierarchyTraverser.Create(e); - - traverser.Traverse(this); + using var route = BuildEventRoute(e.RoutedEvent); + route.RaiseEvent(this, e); } - /// - /// Tunnels an event. - /// - /// The event args. - private void TunnelEvent(RoutedEventArgs e) + void IInteractive.AddToEventRoute(RoutedEvent routedEvent, EventRoute route) { - Contract.Requires(e != null); - - e.Route = RoutingStrategies.Tunnel; - - var traverser = HierarchyTraverser.Create(e); + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + route = route ?? throw new ArgumentNullException(nameof(route)); - traverser.Traverse(this); + if (_eventHandlers != null && + _eventHandlers.TryGetValue(routedEvent, out var subscriptions)) + { + foreach (var sub in subscriptions) + { + route.Add(this, sub.Handler, sub.Routes, sub.HandledEventsToo, sub.InvokeAdapter); + } + } } /// - /// Carries out the actual invocation of an event on this object. + /// Builds an event route for a routed event. /// - /// The event args. - private void RaiseEventImpl(RoutedEventArgs e) + /// The routed event. + /// An describing the route. + /// + /// Usually, calling is sufficent to raise a routed + /// event, however there are situations in which the construction of the event args is expensive + /// and should be avoided if there are no handlers for an event. In these cases you can call + /// this method to build the event route and check the + /// property to see if there are any handlers registered on the route. If there are, call + /// to raise the event. + /// + protected EventRoute BuildEventRoute(RoutedEvent e) { - Contract.Requires(e != null); + e = e ?? throw new ArgumentNullException(nameof(e)); - e.RoutedEvent.InvokeRaised(this, e); + var result = new EventRoute(e); + var hasClassHandlers = e.HasRaisedSubscriptions; - List subscriptions = null; - - if (_eventHandlers?.TryGetValue(e.RoutedEvent, out subscriptions) == true) + if (e.RoutingStrategies.HasFlagCustom(RoutingStrategies.Bubble) || + e.RoutingStrategies.HasFlagCustom(RoutingStrategies.Tunnel)) { - foreach (var sub in subscriptions.ToList()) - { - bool correctRoute = (e.Route & sub.Routes) != 0; - bool notFinished = !e.Handled || sub.AlsoIfHandled; + IInteractive? element = this; - if (correctRoute && notFinished) + while (element != null) + { + if (hasClassHandlers) { - if (sub.InvokeAdapter != null) - { - sub.InvokeAdapter(sub.Handler, this, e); - } - else - { - sub.Handler.DynamicInvoke(this, e); - } + result.AddClassHandler(element); } + + element.AddToEventRoute(e, result); + element = element.InteractiveParent; } } - } - - private List GetEventSubscriptions(RoutedEvent routedEvent) - { - if (!EventHandlers.TryGetValue(routedEvent, out var subscriptions)) + else { - subscriptions = new List(); - EventHandlers.Add(routedEvent, subscriptions); - } - - return subscriptions; - } - - private IDisposable AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription) - { - List subscriptions = GetEventSubscriptions(routedEvent); - - subscriptions.Add(subscription); - - return new UnsubscribeDisposable(subscriptions, subscription); - } - - private sealed class UnsubscribeDisposable : IDisposable - { - private readonly List _subscriptions; - private readonly EventSubscription _subscription; + if (hasClassHandlers) + { + result.AddClassHandler(this); + } - public UnsubscribeDisposable(List subscriptions, EventSubscription subscription) - { - _subscriptions = subscriptions; - _subscription = subscription; + ((IInteractive)this).AddToEventRoute(e, result); } - public void Dispose() - { - _subscriptions.Remove(_subscription); - } + return result; } - private interface ITraverse + private void AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription) { - void Execute(IInteractive target, RoutedEventArgs e); - } + _eventHandlers ??= new Dictionary>(); - private struct NopTraverse : ITraverse - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Execute(IInteractive target, RoutedEventArgs e) + if (!_eventHandlers.TryGetValue(routedEvent, out var subscriptions)) { + subscriptions = new List(); + _eventHandlers.Add(routedEvent, subscriptions); } - } - private struct RaiseEventTraverse : ITraverse - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Execute(IInteractive target, RoutedEventArgs e) - { - ((Interactive)target).RaiseEventImpl(e); - } + subscriptions.Add(subscription); } - /// - /// Traverses interactive hierarchy allowing for raising events. - /// - /// Called before parent is traversed. - /// Called after parent has been traversed. - private struct HierarchyTraverser - where TPreTraverse : struct, ITraverse - where TPostTraverse : struct, ITraverse + private readonly struct EventSubscription { - private TPreTraverse _preTraverse; - private TPostTraverse _postTraverse; - private readonly RoutedEventArgs _args; - - private HierarchyTraverser(TPreTraverse preTraverse, TPostTraverse postTraverse, RoutedEventArgs args) - { - _preTraverse = preTraverse; - _postTraverse = postTraverse; - _args = args; - } - - public static HierarchyTraverser Create(RoutedEventArgs args) - { - return new HierarchyTraverser(new TPreTraverse(), new TPostTraverse(), args); + public EventSubscription( + Delegate handler, + RoutingStrategies routes, + bool handledEventsToo, + Action? invokeAdapter = null) + { + Handler = handler; + Routes = routes; + HandledEventsToo = handledEventsToo; + InvokeAdapter = invokeAdapter; } - public void Traverse(IInteractive target) - { - _preTraverse.Execute(target, _args); + public Action? InvokeAdapter { get; } - IInteractive parent = target.InteractiveParent; + public Delegate Handler { get; } - if (parent != null) - { - Traverse(parent); - } + public RoutingStrategies Routes { get; } - _postTraverse.Execute(target, _args); - } + public bool HandledEventsToo { get; } } } } diff --git a/src/Avalonia.Interactivity/InteractiveExtensions.cs b/src/Avalonia.Interactivity/InteractiveExtensions.cs index 07e4029240..ee6b623b5f 100644 --- a/src/Avalonia.Interactivity/InteractiveExtensions.cs +++ b/src/Avalonia.Interactivity/InteractiveExtensions.cs @@ -1,9 +1,5 @@ -// 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 System; -using System.Collections.Generic; -using System.Linq; +using System.Reactive.Disposables; using System.Reactive.Linq; namespace Avalonia.Interactivity @@ -13,6 +9,28 @@ namespace Avalonia.Interactivity /// public static class InteractiveExtensions { + /// + /// Adds a handler for the specified routed event and returns a disposable that can terminate the event subscription. + /// + /// The type of the event's args. + /// Target for adding given event handler. + /// The routed event. + /// The handler. + /// The routing strategies to listen to. + /// Whether handled events should also be listened for. + /// A disposable that terminates the event subscription. + public static IDisposable AddDisposableHandler(this IInteractive o, RoutedEvent routedEvent, + EventHandler handler, + RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, + bool handledEventsToo = false) where TEventArgs : RoutedEventArgs + { + o.AddHandler(routedEvent, handler, routes, handledEventsToo); + + return Disposable.Create( + (instance: o, handler, routedEvent), + state => state.instance.RemoveHandler(state.routedEvent, state.handler)); + } + /// /// Gets an observable for a . /// @@ -30,7 +48,10 @@ namespace Avalonia.Interactivity bool handledEventsToo = false) where TEventArgs : RoutedEventArgs { - return Observable.Create(x => o.AddHandler( + o = o ?? throw new ArgumentNullException(nameof(o)); + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + + return Observable.Create(x => o.AddDisposableHandler( routedEvent, (_, e) => x.OnNext(e), routes, diff --git a/src/Avalonia.Interactivity/RoutedEvent.cs b/src/Avalonia.Interactivity/RoutedEvent.cs index bc5dec9a90..3f38a61048 100644 --- a/src/Avalonia.Interactivity/RoutedEvent.cs +++ b/src/Avalonia.Interactivity/RoutedEvent.cs @@ -1,9 +1,5 @@ -// 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 System; using System.Reactive.Subjects; -using System.Reflection; namespace Avalonia.Interactivity { @@ -26,10 +22,14 @@ namespace Avalonia.Interactivity Type eventArgsType, Type ownerType) { - Contract.Requires(name != null); - Contract.Requires(eventArgsType != null); - Contract.Requires(ownerType != null); - Contract.Requires(typeof(RoutedEventArgs).GetTypeInfo().IsAssignableFrom(eventArgsType.GetTypeInfo())); + name = name ?? throw new ArgumentNullException(nameof(name)); + eventArgsType = eventArgsType ?? throw new ArgumentNullException(nameof(name)); + ownerType = ownerType ?? throw new ArgumentNullException(nameof(name)); + + if (!typeof(RoutedEventArgs).IsAssignableFrom(eventArgsType)) + { + throw new InvalidCastException("eventArgsType must be derived from RoutedEventArgs."); + } EventArgsType = eventArgsType; Name = name; @@ -45,6 +45,8 @@ namespace Avalonia.Interactivity public RoutingStrategies RoutingStrategies { get; } + public bool HasRaisedSubscriptions => _raised.HasObservers; + public IObservable<(object, RoutedEventArgs)> Raised => _raised; public IObservable RouteFinished => _routeFinished; @@ -53,7 +55,7 @@ namespace Avalonia.Interactivity RoutingStrategies routingStrategy) where TEventArgs : RoutedEventArgs { - Contract.Requires(name != null); + name = name ?? throw new ArgumentNullException(nameof(name)); var routedEvent = new RoutedEvent(name, routingStrategy, typeof(TOwner)); RoutedEventRegistry.Instance.Register(typeof(TOwner), routedEvent); @@ -66,7 +68,7 @@ namespace Avalonia.Interactivity Type ownerType) where TEventArgs : RoutedEventArgs { - Contract.Requires(name != null); + name = name ?? throw new ArgumentNullException(nameof(name)); var routedEvent = new RoutedEvent(name, routingStrategy, ownerType); RoutedEventRegistry.Instance.Register(ownerType, routedEvent); @@ -109,8 +111,6 @@ namespace Avalonia.Interactivity public RoutedEvent(string name, RoutingStrategies routingStrategies, Type ownerType) : base(name, routingStrategies, typeof(TEventArgs), ownerType) { - Contract.Requires(name != null); - Contract.Requires(ownerType != null); } [Obsolete("Use overload taking Action.")] diff --git a/src/Avalonia.Interactivity/RoutedEventArgs.cs b/src/Avalonia.Interactivity/RoutedEventArgs.cs index 05bbf7b6a3..ccbb41b7dc 100644 --- a/src/Avalonia.Interactivity/RoutedEventArgs.cs +++ b/src/Avalonia.Interactivity/RoutedEventArgs.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Interactivity @@ -11,12 +8,12 @@ namespace Avalonia.Interactivity { } - public RoutedEventArgs(RoutedEvent routedEvent) + public RoutedEventArgs(RoutedEvent? routedEvent) { RoutedEvent = routedEvent; } - public RoutedEventArgs(RoutedEvent routedEvent, IInteractive source) + public RoutedEventArgs(RoutedEvent? routedEvent, IInteractive? source) { RoutedEvent = routedEvent; Source = source; @@ -24,10 +21,10 @@ namespace Avalonia.Interactivity public bool Handled { get; set; } - public RoutedEvent RoutedEvent { get; set; } + public RoutedEvent? RoutedEvent { get; set; } public RoutingStrategies Route { get; set; } - public IInteractive Source { get; set; } + public IInteractive? Source { get; set; } } } diff --git a/src/Avalonia.Interactivity/RoutedEventRegistry.cs b/src/Avalonia.Interactivity/RoutedEventRegistry.cs index 34c970a806..409e993b56 100644 --- a/src/Avalonia.Interactivity/RoutedEventRegistry.cs +++ b/src/Avalonia.Interactivity/RoutedEventRegistry.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; @@ -32,8 +29,8 @@ namespace Avalonia.Interactivity /// public void Register(Type type, RoutedEvent @event) { - Contract.Requires(type != null); - Contract.Requires(@event != null); + type = type ?? throw new ArgumentNullException(nameof(type)); + @event = @event ?? throw new ArgumentNullException(nameof(@event)); if (!_registeredRoutedEvents.TryGetValue(type, out var list)) { @@ -66,7 +63,7 @@ namespace Avalonia.Interactivity /// All routed events registered with the provided type. public IReadOnlyList GetRegistered(Type type) { - Contract.Requires(type != null); + type = type ?? throw new ArgumentNullException(nameof(type)); if (_registeredRoutedEvents.TryGetValue(type, out var events)) { diff --git a/src/Avalonia.Layout/AttachedLayout.cs b/src/Avalonia.Layout/AttachedLayout.cs index 5622731a7c..047c01343f 100644 --- a/src/Avalonia.Layout/AttachedLayout.cs +++ b/src/Avalonia.Layout/AttachedLayout.cs @@ -12,7 +12,7 @@ namespace Avalonia.Layout /// public abstract class AttachedLayout : AvaloniaObject { - internal string LayoutId { get; set; } + public string LayoutId { get; set; } /// /// Occurs when the measurement state (layout) has been invalidated. @@ -46,7 +46,23 @@ namespace Avalonia.Layout /// to provide the behavior for /// this method in a derived class. /// - public abstract void InitializeForContext(LayoutContext context); + public void InitializeForContext(LayoutContext context) + { + if (this is VirtualizingLayout virtualizingLayout) + { + var virtualizingContext = GetVirtualizingLayoutContext(context); + virtualizingLayout.InitializeForContextCore(virtualizingContext); + } + else if (this is NonVirtualizingLayout nonVirtualizingLayout) + { + var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context); + nonVirtualizingLayout.InitializeForContextCore(nonVirtualizingContext); + } + else + { + throw new NotSupportedException(); + } + } /// /// Removes any state the layout previously stored on the ILayoutable container. @@ -55,7 +71,23 @@ namespace Avalonia.Layout /// The context object that facilitates communication between the layout and its host /// container. /// - public abstract void UninitializeForContext(LayoutContext context); + public void UninitializeForContext(LayoutContext context) + { + if (this is VirtualizingLayout virtualizingLayout) + { + var virtualizingContext = GetVirtualizingLayoutContext(context); + virtualizingLayout.UninitializeForContextCore(virtualizingContext); + } + else if (this is NonVirtualizingLayout nonVirtualizingLayout) + { + var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context); + nonVirtualizingLayout.UninitializeForContextCore(nonVirtualizingContext); + } + else + { + throw new NotSupportedException(); + } + } /// /// Suggests a DesiredSize for a container element. A container element that supports @@ -73,7 +105,23 @@ namespace Avalonia.Layout /// if scrolling or other resize behavior is possible in that particular container. /// /// - public abstract Size Measure(LayoutContext context, Size availableSize); + public Size Measure(LayoutContext context, Size availableSize) + { + if (this is VirtualizingLayout virtualizingLayout) + { + var virtualizingContext = GetVirtualizingLayoutContext(context); + return virtualizingLayout.MeasureOverride(virtualizingContext, availableSize); + } + else if (this is NonVirtualizingLayout nonVirtualizingLayout) + { + var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context); + return nonVirtualizingLayout.MeasureOverride(nonVirtualizingContext, availableSize); + } + else + { + throw new NotSupportedException(); + } + } /// /// Positions child elements and determines a size for a container UIElement. Container @@ -88,7 +136,23 @@ namespace Avalonia.Layout /// The final size that the container computes for the child in layout. /// /// The actual size that is used after the element is arranged in layout. - public abstract Size Arrange(LayoutContext context, Size finalSize); + public Size Arrange(LayoutContext context, Size finalSize) + { + if (this is VirtualizingLayout virtualizingLayout) + { + var virtualizingContext = GetVirtualizingLayoutContext(context); + return virtualizingLayout.ArrangeOverride(virtualizingContext, finalSize); + } + else if (this is NonVirtualizingLayout nonVirtualizingLayout) + { + var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context); + return nonVirtualizingLayout.ArrangeOverride(nonVirtualizingContext, finalSize); + } + else + { + throw new NotSupportedException(); + } + } /// /// Invalidates the measurement state (layout) for all ILayoutable containers that reference @@ -102,5 +166,37 @@ namespace Avalonia.Layout /// occurs asynchronously. /// protected void InvalidateArrange() => ArrangeInvalidated?.Invoke(this, EventArgs.Empty); + + private VirtualizingLayoutContext GetVirtualizingLayoutContext(LayoutContext context) + { + if (context is VirtualizingLayoutContext virtualizingContext) + { + return virtualizingContext; + } + else if (context is NonVirtualizingLayoutContext nonVirtualizingContext) + { + return nonVirtualizingContext.GetVirtualizingContextAdapter(); + } + else + { + throw new NotSupportedException(); + } + } + + private NonVirtualizingLayoutContext GetNonVirtualizingLayoutContext(LayoutContext context) + { + if (context is NonVirtualizingLayoutContext nonVirtualizingContext) + { + return nonVirtualizingContext; + } + else if (context is VirtualizingLayoutContext virtualizingContext) + { + return virtualizingContext.GetNonVirtualizingContextAdapter(); + } + else + { + throw new NotSupportedException(); + } + } } } diff --git a/src/Avalonia.Layout/EffectiveViewportChangedEventArgs.cs b/src/Avalonia.Layout/EffectiveViewportChangedEventArgs.cs new file mode 100644 index 0000000000..1cdc775b13 --- /dev/null +++ b/src/Avalonia.Layout/EffectiveViewportChangedEventArgs.cs @@ -0,0 +1,24 @@ +using System; + +namespace Avalonia.Layout +{ + /// + /// Provides data for the event. + /// + public class EffectiveViewportChangedEventArgs : EventArgs + { + public EffectiveViewportChangedEventArgs(Rect effectiveViewport) + { + EffectiveViewport = effectiveViewport; + } + + /// + /// Gets the representing the effective viewport. + /// + /// + /// The viewport is expressed in coordinates relative to the control that the event is + /// raised on. + /// + public Rect EffectiveViewport { get; } + } +} diff --git a/src/Avalonia.Layout/ElementManager.cs b/src/Avalonia.Layout/ElementManager.cs index 1748a3be03..70805ba31c 100644 --- a/src/Avalonia.Layout/ElementManager.cs +++ b/src/Avalonia.Layout/ElementManager.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using Avalonia.Layout.Utils; +using Avalonia.Logging; namespace Avalonia.Layout { @@ -78,6 +79,7 @@ namespace Avalonia.Layout { // Sentinel. Create the element now since we need it. int dataIndex = GetDataIndexFromRealizedRangeIndex(realizedIndex); + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "Creating element for sentinal with data index {Index}", dataIndex); element = _context.GetOrCreateElementAt( dataIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); @@ -232,6 +234,8 @@ namespace Avalonia.Layout { Insert(0, dataIndex, element); } + + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Created element for index {index}", layoutId, dataIndex); } } diff --git a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs index 615ce725bd..cd7f725f18 100644 --- a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Specialized; +using Avalonia.Logging; namespace Avalonia.Layout { @@ -72,7 +73,9 @@ namespace Avalonia.Layout bool isWrapping, double minItemSpacing, double lineSpacing, + int maxItemsPerLine, ScrollOrientation orientation, + bool disableVirtualization, string layoutId) { _orientation.ScrollOrientation = orientation; @@ -80,6 +83,9 @@ namespace Avalonia.Layout // If minor size is infinity, there is only one line and no need to align that line. _scrollOrientationSameAsFlow = double.IsInfinity(_orientation.Minor(availableSize)); var realizationRect = RealizationRect; + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: MeasureLayout Realization({Rect})", + layoutId, + realizationRect); var suggestedAnchorIndex = _context.RecommendedAnchorIndex; if (_elementManager.IsIndexValidInData(suggestedAnchorIndex)) @@ -94,14 +100,15 @@ namespace Avalonia.Layout _elementManager.OnBeginMeasure(orientation); int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId); - Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId); - Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId); + Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId); + Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId); if (isWrapping && IsReflowRequired()) { + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Reflow Pass", layoutId); var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); _orientation.SetMinorStart(ref firstElementBounds, 0); _elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds); - Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, layoutId); + Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId); } RaiseLineArranged(); @@ -115,10 +122,12 @@ namespace Avalonia.Layout public Size Arrange( Size finalSize, VirtualizingLayoutContext context, + bool isWrapping, LineAlignment lineAlignment, string layoutId) { - ArrangeVirtualizingLayout(finalSize, lineAlignment, layoutId); + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: ArrangeLayout", layoutId); + ArrangeVirtualizingLayout(finalSize, lineAlignment, isWrapping, layoutId); return new Size( Math.Max(finalSize.Width, _lastExtent.Width), @@ -181,6 +190,7 @@ namespace Avalonia.Layout if (isAnchorSuggestionValid) { + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Using suggested anchor {Anchor}", layoutId, suggestedAnchorIndex); anchorIndex = _algorithmCallbacks.Algorithm_GetAnchorForTargetElement( suggestedAnchorIndex, availableSize, @@ -220,6 +230,9 @@ namespace Avalonia.Layout } else if (needAnchorColumnRevaluation || !isRealizationWindowConnected) { + if (needAnchorColumnRevaluation) { Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: NeedAnchorColumnReevaluation", layoutId); } + if (!isRealizationWindowConnected) { Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Disconnected Window", layoutId); } + // The anchor is based on the realization window because a connected ItemsRepeater might intersect the realization window // but not the visible window. In that situation, we still need to produce a valid anchor. var anchorInfo = _algorithmCallbacks.Algorithm_GetAnchorForRealizationRect(availableSize, context); @@ -228,6 +241,7 @@ namespace Avalonia.Layout } else { + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Connected Window - picking first realized element as anchor", layoutId); // No suggestion - just pick first in realized range anchorIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0); var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); @@ -235,12 +249,14 @@ namespace Avalonia.Layout } } + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Picked anchor: {Anchor}", layoutId, anchorIndex); _firstRealizedDataIndexInsideRealizationWindow = _lastRealizedDataIndexInsideRealizationWindow = anchorIndex; if (_elementManager.IsIndexValidInData(anchorIndex)) { if (!_elementManager.IsDataIndexRealized(anchorIndex)) { // Disconnected, throw everything and create new anchor + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId} Disconnected Window - throwing away all realized elements", layoutId); _elementManager.ClearRealizedRange(); var anchor = _context.GetOrCreateElementAt(anchorIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); @@ -251,9 +267,17 @@ namespace Avalonia.Layout var desiredSize = MeasureElement(anchorElement, anchorIndex, availableSize, _context); var layoutBounds = new Rect(anchorPosition.X, anchorPosition.Y, desiredSize.Width, desiredSize.Height); _elementManager.SetLayoutBoundsForDataIndex(anchorIndex, layoutBounds); + + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Layout bounds of anchor {anchor} are ({Bounds})", + layoutId, + anchorIndex, + layoutBounds); } else { + // Throw everything away + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId} Anchor index is not valid - throwing away all realized elements", + layoutId); _elementManager.ClearRealizedRange(); } @@ -270,22 +294,30 @@ namespace Avalonia.Layout Size availableSize, double minItemSpacing, double lineSpacing, + int maxItemsPerLine, + bool disableVirtualization, string layoutId) { if (anchorIndex != -1) { int step = (direction == GenerateDirection.Forward) ? 1 : -1; + + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Generating {Direction} from anchor {Anchor}", + layoutId, + direction, + anchorIndex); + int previousIndex = anchorIndex; int currentIndex = anchorIndex + step; var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex); var lineOffset = _orientation.MajorStart(anchorBounds); var lineMajorSize = _orientation.MajorSize(anchorBounds); - int countInLine = 1; + var countInLine = 1; int count = 0; bool lineNeedsReposition = false; while (_elementManager.IsIndexValidInData(currentIndex) && - ShouldContinueFillingUpSpace(previousIndex, direction)) + (disableVirtualization || ShouldContinueFillingUpSpace(previousIndex, direction))) { // Ensure layout element. _elementManager.EnsureElementRealized(direction == GenerateDirection.Forward, currentIndex, layoutId); @@ -301,7 +333,7 @@ namespace Avalonia.Layout if (direction == GenerateDirection.Forward) { double remainingSpace = _orientation.Minor(availableSize) - (_orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing + _orientation.Minor(desiredSize)); - if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) + if (countInLine >= maxItemsPerLine || _algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) { // No more space in this row. wrap to next row. _orientation.SetMinorStart(ref currentBounds, 0); @@ -339,7 +371,7 @@ namespace Avalonia.Layout { // Backward double remainingSpace = _orientation.MinorStart(previousElementBounds) - (_orientation.Minor(desiredSize) + minItemSpacing); - if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) + if (countInLine >= maxItemsPerLine || _algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) { // Does not fit, wrap to the previous row var availableSizeMinor = _orientation.Minor(availableSize); @@ -360,6 +392,10 @@ namespace Avalonia.Layout _orientation.SetMajorStart(ref bounds, previousLineOffset - lineMajorSize - lineSpacing); _orientation.SetMajorSize(ref bounds, lineMajorSize); _elementManager.SetLayoutBoundsForDataIndex(dataIndex, bounds); + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Corrected Layout bounds of element {Index} are ({Bounds})", + layoutId, + dataIndex, + bounds); } } } @@ -382,6 +418,11 @@ namespace Avalonia.Layout } _elementManager.SetLayoutBoundsForDataIndex(currentIndex, currentBounds); + + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Layout bounds of element {Index} are ({Bounds}).", + layoutId, + currentIndex, + currentBounds); previousIndex = currentIndex; currentIndex += step; } @@ -389,20 +430,17 @@ namespace Avalonia.Layout // If we did not reach the top or bottom of the extent, we realized one // extra item before we knew we were outside the realization window. Do not // account for that element in the indicies inside the realization window. - if (count > 0) + if (direction == GenerateDirection.Forward) { - if (direction == GenerateDirection.Forward) - { - int dataCount = _context.ItemCount; - _lastRealizedDataIndexInsideRealizationWindow = previousIndex == dataCount - 1 ? dataCount - 1 : previousIndex - 1; - _lastRealizedDataIndexInsideRealizationWindow = Math.Max(0, _lastRealizedDataIndexInsideRealizationWindow); - } - else - { - int dataCount = _context.ItemCount; - _firstRealizedDataIndexInsideRealizationWindow = previousIndex == 0 ? 0 : previousIndex + 1; - _firstRealizedDataIndexInsideRealizationWindow = Math.Min(dataCount - 1, _firstRealizedDataIndexInsideRealizationWindow); - } + int dataCount = _context.ItemCount; + _lastRealizedDataIndexInsideRealizationWindow = previousIndex == dataCount - 1 ? dataCount - 1 : previousIndex - 1; + _lastRealizedDataIndexInsideRealizationWindow = Math.Max(0, _lastRealizedDataIndexInsideRealizationWindow); + } + else + { + int dataCount = _context.ItemCount; + _firstRealizedDataIndexInsideRealizationWindow = previousIndex == 0 ? 0 : previousIndex + 1; + _firstRealizedDataIndexInsideRealizationWindow = Math.Min(dataCount - 1, _firstRealizedDataIndexInsideRealizationWindow); } _elementManager.DiscardElementsOutsideWindow(direction == GenerateDirection.Forward, currentIndex); @@ -503,6 +541,7 @@ namespace Avalonia.Layout lastDataIndex, lastBounds); + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId} Extent: ({Bounds})", layoutId, extent); return extent; } @@ -544,6 +583,7 @@ namespace Avalonia.Layout private void ArrangeVirtualizingLayout( Size finalSize, LineAlignment lineAlignment, + bool isWrapping, string layoutId) { // Walk through the realized elements one line at a time and @@ -563,7 +603,7 @@ namespace Avalonia.Layout if (_orientation.MajorStart(currentBounds) != currentLineOffset) { spaceAtLineEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds); - PerformLineAlignment(i - countInLine, countInLine, spaceAtLineStart, spaceAtLineEnd, currentLineSize, lineAlignment, layoutId); + PerformLineAlignment(i - countInLine, countInLine, spaceAtLineStart, spaceAtLineEnd, currentLineSize, lineAlignment, isWrapping, finalSize, layoutId); spaceAtLineStart = _orientation.MinorStart(currentBounds); countInLine = 0; currentLineOffset = _orientation.MajorStart(currentBounds); @@ -580,7 +620,7 @@ namespace Avalonia.Layout if (countInLine > 0) { var spaceAtEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds); - PerformLineAlignment(realizedElementCount - countInLine, countInLine, spaceAtLineStart, spaceAtEnd, currentLineSize, lineAlignment, layoutId); + PerformLineAlignment(realizedElementCount - countInLine, countInLine, spaceAtLineStart, spaceAtEnd, currentLineSize, lineAlignment, isWrapping, finalSize, layoutId); } } } @@ -594,6 +634,8 @@ namespace Avalonia.Layout double spaceAtLineEnd, double lineSize, LineAlignment lineAlignment, + bool isWrapping, + Size finalSize, string layoutId) { for (int rangeIndex = lineStartIndex; rangeIndex < lineStartIndex + countInLine; ++rangeIndex) @@ -659,7 +701,20 @@ namespace Avalonia.Layout } bounds = bounds.Translate(-_lastExtent.Position); + + if (!isWrapping) + { + _orientation.SetMinorSize( + ref bounds, + Math.Max(_orientation.MinorSize(bounds), _orientation.Minor(finalSize))); + } + var element = _elementManager.GetAt(rangeIndex); + + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Arranging element {Index} at ({Bounds})", + layoutId, + _elementManager.GetDataIndexFromRealizedRangeIndex(rangeIndex), + bounds); element.Arrange(bounds); } } diff --git a/src/Avalonia.Layout/ILayoutManager.cs b/src/Avalonia.Layout/ILayoutManager.cs index a255fb155c..614670a53b 100644 --- a/src/Avalonia.Layout/ILayoutManager.cs +++ b/src/Avalonia.Layout/ILayoutManager.cs @@ -1,14 +1,19 @@ -// 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 System; +#nullable enable namespace Avalonia.Layout { /// /// Manages measuring and arranging of controls. /// - public interface ILayoutManager + public interface ILayoutManager : IDisposable { + /// + /// Raised when the layout manager completes a layout pass. + /// + event EventHandler LayoutUpdated; + /// /// Notifies the layout manager that a control requires a measure. /// @@ -30,6 +35,15 @@ namespace Avalonia.Layout /// void ExecuteLayoutPass(); + /// + /// Executes the initial layout pass on a layout root. + /// + /// + /// You should not usually need to call this method explictly, the layout root will call + /// it to carry out the initial layout of the control. + /// + void ExecuteInitialLayoutPass(); + /// /// Executes the initial layout pass on a layout root. /// @@ -38,6 +52,19 @@ namespace Avalonia.Layout /// You should not usually need to call this method explictly, the layout root will call /// it to carry out the initial layout of the control. /// + [Obsolete("Call ExecuteInitialLayoutPass without parameter")] void ExecuteInitialLayoutPass(ILayoutRoot root); + + /// + /// Registers a control as wanting to receive effective viewport notifications. + /// + /// The control. + void RegisterEffectiveViewportListener(ILayoutable control); + + /// + /// Registers a control as no longer wanting to receive effective viewport notifications. + /// + /// The control. + void UnregisterEffectiveViewportListener(ILayoutable control); } } diff --git a/src/Avalonia.Layout/ILayoutRoot.cs b/src/Avalonia.Layout/ILayoutRoot.cs index 700b6a8600..e2f16b338a 100644 --- a/src/Avalonia.Layout/ILayoutRoot.cs +++ b/src/Avalonia.Layout/ILayoutRoot.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Layout { /// @@ -13,11 +10,6 @@ namespace Avalonia.Layout /// Size ClientSize { get; } - /// - /// The maximum client size available. - /// - Size MaxClientSize { get; } - /// /// The scaling factor to use in layout. /// diff --git a/src/Avalonia.Layout/ILayoutable.cs b/src/Avalonia.Layout/ILayoutable.cs index d670889565..54d3ba6a11 100644 --- a/src/Avalonia.Layout/ILayoutable.cs +++ b/src/Avalonia.Layout/ILayoutable.cs @@ -1,8 +1,7 @@ -// 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.VisualTree; +#nullable enable + namespace Avalonia.Layout { /// @@ -112,5 +111,12 @@ namespace Avalonia.Layout /// /// The child control. void ChildDesiredSizeChanged(ILayoutable control); + + /// + /// Used by the to notify the control that its effective + /// viewport is changed. + /// + /// The viewport information. + void EffectiveViewportChanged(EffectiveViewportChangedEventArgs e); } } diff --git a/src/Avalonia.Layout/LayoutContextAdapter.cs b/src/Avalonia.Layout/LayoutContextAdapter.cs new file mode 100644 index 0000000000..695866df94 --- /dev/null +++ b/src/Avalonia.Layout/LayoutContextAdapter.cs @@ -0,0 +1,45 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; + +namespace Avalonia.Layout +{ + internal class LayoutContextAdapter : VirtualizingLayoutContext + { + private readonly NonVirtualizingLayoutContext _nonVirtualizingContext; + + public LayoutContextAdapter(NonVirtualizingLayoutContext nonVirtualizingContext) + { + _nonVirtualizingContext = nonVirtualizingContext; + } + + protected override object LayoutStateCore + { + get => _nonVirtualizingContext.LayoutState; + set => _nonVirtualizingContext.LayoutState = value; + } + + protected override Point LayoutOriginCore + { + get => default; + set + { + if (value != default) + { + throw new InvalidOperationException("LayoutOrigin must be at (0,0) when RealizationRect is infinite sized."); + } + } + } + + protected override Rect RealizationRectCore() => new Rect(Size.Infinity); + + protected override int ItemCountCore() => _nonVirtualizingContext.Children.Count; + protected override object GetItemAtCore(int index) => _nonVirtualizingContext.Children[index]; + protected override ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options) => + _nonVirtualizingContext.Children[index]; + protected override void RecycleElementCore(ILayoutable element) { } + } +} diff --git a/src/Avalonia.Layout/LayoutHelper.cs b/src/Avalonia.Layout/LayoutHelper.cs index d77cc269f9..3708748ad1 100644 --- a/src/Avalonia.Layout/LayoutHelper.cs +++ b/src/Avalonia.Layout/LayoutHelper.cs @@ -1,7 +1,6 @@ -// 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 System; +using Avalonia.Utilities; +using Avalonia.VisualTree; namespace Avalonia.Layout { @@ -21,13 +20,11 @@ namespace Avalonia.Layout /// The control's size. public static Size ApplyLayoutConstraints(ILayoutable control, Size constraints) { - double width = (control.Width > 0) ? control.Width : constraints.Width; - double height = (control.Height > 0) ? control.Height : constraints.Height; - width = Math.Min(width, control.MaxWidth); - width = Math.Max(width, control.MinWidth); - height = Math.Min(height, control.MaxHeight); - height = Math.Max(height, control.MinHeight); - return new Size(width, height); + var minmax = new MinMax(control); + + return new Size( + MathUtilities.Clamp(constraints.Width, minmax.MinWidth, minmax.MaxWidth), + MathUtilities.Clamp(constraints.Height, minmax.MinHeight, minmax.MaxHeight)); } public static Size MeasureChild(ILayoutable control, Size availableSize, Thickness padding, @@ -58,5 +55,123 @@ namespace Avalonia.Layout return availableSize; } + + /// + /// Invalidates measure for given control and all visual children recursively. + /// + public static void InvalidateSelfAndChildrenMeasure(ILayoutable control) + { + void InnerInvalidateMeasure(IVisual target) + { + if (target is ILayoutable targetLayoutable) + { + targetLayoutable.InvalidateMeasure(); + } + + var visualChildren = target.VisualChildren; + var visualChildrenCount = visualChildren.Count; + + for (int i = 0; i < visualChildrenCount; i++) + { + IVisual child = visualChildren[i]; + + InnerInvalidateMeasure(child); + } + } + + InnerInvalidateMeasure(control); + } + + /// + /// Rounds a size to integer values for layout purposes, compensating for high DPI screen + /// coordinates. + /// + /// Input size. + /// DPI along x-dimension. + /// DPI along y-dimension. + /// Value of size that will be rounded under screen 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 the UseLayoutRounding property and should not be used as a general rounding + /// utility. + /// + public static Size RoundLayoutSize(Size size, double dpiScaleX, double dpiScaleY) + { + return new Size(RoundLayoutValue(size.Width, dpiScaleX), RoundLayoutValue(size.Height, dpiScaleY)); + } + + /// + /// 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 the 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.IsOne(dpiScale)) + { + 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; + } + + + /// + /// Calculates the min and max height for a control. Ported from WPF. + /// + private readonly struct MinMax + { + public MinMax(ILayoutable e) + { + MaxHeight = e.MaxHeight; + MinHeight = e.MinHeight; + double l = e.Height; + + double height = (double.IsNaN(l) ? double.PositiveInfinity : l); + MaxHeight = Math.Max(Math.Min(height, MaxHeight), MinHeight); + + height = (double.IsNaN(l) ? 0 : l); + MinHeight = Math.Max(Math.Min(MaxHeight, height), MinHeight); + + MaxWidth = e.MaxWidth; + MinWidth = e.MinWidth; + l = e.Width; + + double width = (double.IsNaN(l) ? double.PositiveInfinity : l); + MaxWidth = Math.Max(Math.Min(width, MaxWidth), MinWidth); + + width = (double.IsNaN(l) ? 0 : l); + MinWidth = Math.Max(Math.Min(MaxWidth, width), MinWidth); + } + + public double MinWidth { get; } + public double MaxWidth { get; } + public double MinHeight { get; } + public double MaxHeight { get; } + } } } diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index 855f123748..792de774d1 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -1,29 +1,49 @@ -// 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 System; +using System.Buffers; +using System.Collections.Generic; using System.Diagnostics; using Avalonia.Logging; using Avalonia.Threading; +using Avalonia.VisualTree; + +#nullable enable namespace Avalonia.Layout { /// /// Manages measuring and arranging of controls. /// - public class LayoutManager : ILayoutManager + public class LayoutManager : ILayoutManager, IDisposable { + private const int MaxPasses = 3; + private readonly ILayoutRoot _owner; private readonly LayoutQueue _toMeasure = new LayoutQueue(v => !v.IsMeasureValid); private readonly LayoutQueue _toArrange = new LayoutQueue(v => !v.IsArrangeValid); + private readonly Action _executeLayoutPass; + private List? _effectiveViewportChangedListeners; + private bool _disposed; private bool _queued; private bool _running; + public LayoutManager(ILayoutRoot owner) + { + _owner = owner ?? throw new ArgumentNullException(nameof(owner)); + _executeLayoutPass = ExecuteLayoutPass; + } + + public virtual event EventHandler? LayoutUpdated; + /// - public void InvalidateMeasure(ILayoutable control) + public virtual void InvalidateMeasure(ILayoutable control) { - Contract.Requires(control != null); + control = control ?? throw new ArgumentNullException(nameof(control)); Dispatcher.UIThread.VerifyAccess(); + if (_disposed) + { + return; + } + if (!control.IsAttachedToVisualTree) { #if DEBUG @@ -34,17 +54,27 @@ namespace Avalonia.Layout #endif } + if (control.VisualRoot != _owner) + { + throw new ArgumentException("Attempt to call InvalidateMeasure on wrong LayoutManager."); + } + _toMeasure.Enqueue(control); _toArrange.Enqueue(control); QueueLayoutPass(); } /// - public void InvalidateArrange(ILayoutable control) + public virtual void InvalidateArrange(ILayoutable control) { - Contract.Requires(control != null); + control = control ?? throw new ArgumentNullException(nameof(control)); Dispatcher.UIThread.VerifyAccess(); + if (_disposed) + { + return; + } + if (!control.IsAttachedToVisualTree) { #if DEBUG @@ -55,30 +85,37 @@ namespace Avalonia.Layout #endif } + if (control.VisualRoot != _owner) + { + throw new ArgumentException("Attempt to call InvalidateArrange on wrong LayoutManager."); + } + _toArrange.Enqueue(control); QueueLayoutPass(); } /// - public void ExecuteLayoutPass() + public virtual void ExecuteLayoutPass() { - const int MaxPasses = 3; - Dispatcher.UIThread.VerifyAccess(); + if (_disposed) + { + return; + } + if (!_running) { _running = true; - Stopwatch stopwatch = null; + Stopwatch? stopwatch = null; const LogEventLevel timingLogLevel = LogEventLevel.Information; - bool captureTiming = Logger.IsEnabled(timingLogLevel); + bool captureTiming = Logger.IsEnabled(timingLogLevel, LogArea.Layout); if (captureTiming) { - Logger.TryGet(timingLogLevel)?.Log( - LogArea.Layout, + Logger.TryGet(timingLogLevel, LogArea.Layout)?.Log( this, "Started layout pass. To measure: {Measure} To arrange: {Arrange}", _toMeasure.Count, @@ -91,43 +128,49 @@ namespace Avalonia.Layout _toMeasure.BeginLoop(MaxPasses); _toArrange.BeginLoop(MaxPasses); - try + for (var pass = 0; pass < MaxPasses; ++pass) { - for (var pass = 0; pass < MaxPasses; ++pass) - { - ExecuteMeasurePass(); - ExecuteArrangePass(); + InnerLayoutPass(); - if (_toMeasure.Count == 0) - { - break; - } + if (!RaiseEffectiveViewportChanged()) + { + break; } } - finally - { - _running = false; - } _toMeasure.EndLoop(); _toArrange.EndLoop(); if (captureTiming) { - stopwatch.Stop(); + stopwatch!.Stop(); - Logger.TryGet(timingLogLevel)?.Log(LogArea.Layout, this, "Layout pass finished in {Time}", stopwatch.Elapsed); + Logger.TryGet(timingLogLevel, LogArea.Layout)?.Log(this, "Layout pass finished in {Time}", stopwatch.Elapsed); } } _queued = false; + LayoutUpdated?.Invoke(this, EventArgs.Empty); } /// - public void ExecuteInitialLayoutPass(ILayoutRoot root) + public virtual void ExecuteInitialLayoutPass() { - Measure(root); - Arrange(root); + if (_disposed) + { + return; + } + + try + { + _running = true; + Measure(_owner); + Arrange(_owner); + } + finally + { + _running = false; + } // Running the initial layout pass may have caused some control to be invalidated // so run a full layout pass now (this usually due to scrollbars; its not known @@ -136,6 +179,67 @@ namespace Avalonia.Layout ExecuteLayoutPass(); } + [Obsolete("Call ExecuteInitialLayoutPass without parameter")] + public void ExecuteInitialLayoutPass(ILayoutRoot root) + { + if (root != _owner) + { + throw new ArgumentException("ExecuteInitialLayoutPass called with incorrect root."); + } + + ExecuteInitialLayoutPass(); + } + + public void Dispose() + { + _disposed = true; + _toMeasure.Dispose(); + _toArrange.Dispose(); + } + + void ILayoutManager.RegisterEffectiveViewportListener(ILayoutable control) + { + _effectiveViewportChangedListeners ??= new List(); + _effectiveViewportChangedListeners.Add(new EffectiveViewportChangedListener( + control, + CalculateEffectiveViewport(control))); + } + + void ILayoutManager.UnregisterEffectiveViewportListener(ILayoutable control) + { + if (_effectiveViewportChangedListeners is object) + { + for (var i = _effectiveViewportChangedListeners.Count - 1; i >= 0; --i) + { + if (_effectiveViewportChangedListeners[i].Listener == control) + { + _effectiveViewportChangedListeners.RemoveAt(i); + } + } + } + } + + private void InnerLayoutPass() + { + try + { + for (var pass = 0; pass < MaxPasses; ++pass) + { + ExecuteMeasurePass(); + ExecuteArrangePass(); + + if (_toMeasure.Count == 0) + { + break; + } + } + } + finally + { + _running = false; + } + } + private void ExecuteMeasurePass() { while (_toMeasure.Count > 0) @@ -215,9 +319,101 @@ namespace Avalonia.Layout { if (!_queued && !_running) { - Dispatcher.UIThread.Post(ExecuteLayoutPass, DispatcherPriority.Layout); + Dispatcher.UIThread.Post(_executeLayoutPass, DispatcherPriority.Layout); _queued = true; } } + + private bool RaiseEffectiveViewportChanged() + { + var startCount = _toMeasure.Count + _toArrange.Count; + + if (_effectiveViewportChangedListeners is object) + { + var count = _effectiveViewportChangedListeners.Count; + var pool = ArrayPool.Shared; + var listeners = pool.Rent(count); + + _effectiveViewportChangedListeners.CopyTo(listeners); + + try + { + for (var i = 0; i < count; ++i) + { + var l = _effectiveViewportChangedListeners[i]; + + if (!l.Listener.IsAttachedToVisualTree) + { + continue; + } + + var viewport = CalculateEffectiveViewport(l.Listener); + + if (viewport != l.Viewport) + { + l.Listener.EffectiveViewportChanged(new EffectiveViewportChangedEventArgs(viewport)); + _effectiveViewportChangedListeners[i] = new EffectiveViewportChangedListener(l.Listener, viewport); + } + } + } + finally + { + pool.Return(listeners, clearArray: true); + } + } + + return startCount != _toMeasure.Count + _toMeasure.Count; + } + + private Rect CalculateEffectiveViewport(IVisual control) + { + var viewport = new Rect(0, 0, double.PositiveInfinity, double.PositiveInfinity); + CalculateEffectiveViewport(control, control, ref viewport); + return viewport; + } + + private void CalculateEffectiveViewport(IVisual target, IVisual control, ref Rect viewport) + { + // Recurse until the top level control. + if (control.VisualParent is object) + { + CalculateEffectiveViewport(target, control.VisualParent, ref viewport); + } + else + { + viewport = new Rect(control.Bounds.Size); + } + + // Apply the control clip bounds if it's not the target control. We don't apply it to + // the target control because it may itself be clipped to bounds and if so the viewport + // we calculate would be of no use. + if (control != target && control.ClipToBounds) + { + viewport = control.Bounds.Intersect(viewport); + } + + // Translate the viewport into this control's coordinate space. + viewport = viewport.Translate(-control.Bounds.Position); + + if (control != target && control.RenderTransform is object) + { + var origin = control.RenderTransformOrigin.ToPixels(control.Bounds.Size); + var offset = Matrix.CreateTranslation(origin); + var renderTransform = (-offset) * control.RenderTransform.Value.Invert() * (offset); + viewport = viewport.TransformToAABB(renderTransform); + } + } + + private readonly struct EffectiveViewportChangedListener + { + public EffectiveViewportChangedListener(ILayoutable listener, Rect viewport) + { + Listener = listener; + Viewport = viewport; + } + + public ILayoutable Listener { get; } + public Rect Viewport { get; } + } } } diff --git a/src/Avalonia.Layout/LayoutQueue.cs b/src/Avalonia.Layout/LayoutQueue.cs index eb0e4bd9f3..1a9eb6b785 100644 --- a/src/Avalonia.Layout/LayoutQueue.cs +++ b/src/Avalonia.Layout/LayoutQueue.cs @@ -1,11 +1,10 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; namespace Avalonia.Layout { - internal class LayoutQueue : IReadOnlyCollection + internal class LayoutQueue : IReadOnlyCollection, IDisposable { private struct Info { @@ -18,9 +17,11 @@ namespace Avalonia.Layout _shouldEnqueue = shouldEnqueue; } - private Func _shouldEnqueue; - private Queue _inner = new Queue(); - private Dictionary _loopQueueInfo = new Dictionary(); + private readonly Func _shouldEnqueue; + private readonly Queue _inner = new Queue(); + private readonly Dictionary _loopQueueInfo = new Dictionary(); + private readonly List> _notFinalizedBuffer = new List>(); + private int _maxEnqueueCountPerLoop = 1; public int Count => _inner.Count; @@ -60,20 +61,35 @@ namespace Avalonia.Layout public void EndLoop() { - var notfinalized = _loopQueueInfo.Where(v => v.Value.Count >= _maxEnqueueCountPerLoop).ToArray(); + foreach (KeyValuePair info in _loopQueueInfo) + { + if (info.Value.Count >= _maxEnqueueCountPerLoop) + { + _notFinalizedBuffer.Add(info); + } + } _loopQueueInfo.Clear(); - //prevent layout cycle but add to next layout the non arranged/measured items that might have caused cycle - //one more time as a final attempt - foreach (var item in notfinalized) + // Prevent layout cycle but add to next layout the non arranged/measured items that might have caused cycle + // one more time as a final attempt. + foreach (var item in _notFinalizedBuffer) { if (_shouldEnqueue(item.Key)) { - _loopQueueInfo[item.Key] = new Info() { Active = true, Count = item.Value.Count + 1 }; + _loopQueueInfo[item.Key] = new Info() { Active = true, Count = 0 }; _inner.Enqueue(item.Key); } } + + _notFinalizedBuffer.Clear(); + } + + public void Dispose() + { + _inner.Clear(); + _loopQueueInfo.Clear(); + _notFinalizedBuffer.Clear(); } } } diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index b0757a823d..e62e22f8ec 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -1,11 +1,9 @@ -// 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 System; -using System.Linq; using Avalonia.Logging; using Avalonia.VisualTree; +#nullable enable + namespace Avalonia.Layout { /// @@ -134,6 +132,8 @@ namespace Avalonia.Layout private bool _measuring; private Size? _previousMeasure; private Rect? _previousArrange; + private EventHandler? _effectiveViewportChanged; + private EventHandler? _layoutUpdated; /// /// Initializes static members of the class. @@ -153,10 +153,57 @@ namespace Avalonia.Layout VerticalAlignmentProperty); } + /// + /// Occurs when the element's effective viewport changes. + /// + public event EventHandler? EffectiveViewportChanged + { + add + { + if (_effectiveViewportChanged is null && VisualRoot is ILayoutRoot r) + { + r.LayoutManager.RegisterEffectiveViewportListener(this); + } + + _effectiveViewportChanged += value; + } + + remove + { + _effectiveViewportChanged -= value; + + if (_effectiveViewportChanged is null && VisualRoot is ILayoutRoot r) + { + r.LayoutManager.UnregisterEffectiveViewportListener(this); + } + } + } + /// /// Occurs when a layout pass completes for the control. /// - public event EventHandler LayoutUpdated; + public event EventHandler? LayoutUpdated + { + add + { + if (_layoutUpdated is null && VisualRoot is ILayoutRoot r) + { + r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated; + } + + _layoutUpdated += value; + } + + remove + { + _layoutUpdated -= value; + + if (_layoutUpdated is null && VisualRoot is ILayoutRoot r) + { + r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated; + } + } + } /// /// Gets or sets the width of the element. @@ -329,7 +376,7 @@ namespace Avalonia.Layout DesiredSize = desiredSize; _previousMeasure = availableSize; - Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Layout, this, "Measure requested {DesiredSize}", DesiredSize); + Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Measure requested {DesiredSize}", DesiredSize); if (DesiredSize != previousDesiredSize) { @@ -356,24 +403,14 @@ namespace Avalonia.Layout if (!IsArrangeValid || _previousArrange != rect) { - Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Layout, this, "Arrange to {Rect} ", rect); + Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Arrange to {Rect} ", rect); IsArrangeValid = true; ArrangeCore(rect); _previousArrange = rect; - - LayoutUpdated?.Invoke(this, EventArgs.Empty); } } - - /// - /// Called by InvalidateMeasure - /// - protected virtual void OnMeasureInvalidated() - { - } - /// /// Invalidates the measurement of the control and queues a new layout pass. /// @@ -381,7 +418,7 @@ namespace Avalonia.Layout { if (IsMeasureValid) { - Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Layout, this, "Invalidated measure"); + Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Invalidated measure"); IsMeasureValid = false; IsArrangeValid = false; @@ -402,7 +439,7 @@ namespace Avalonia.Layout { if (IsArrangeValid) { - Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Layout, this, "Invalidated arrange"); + Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Invalidated arrange"); IsArrangeValid = false; (VisualRoot as ILayoutRoot)?.LayoutManager?.InvalidateArrange(this); @@ -419,6 +456,11 @@ namespace Avalonia.Layout } } + void ILayoutable.EffectiveViewportChanged(EffectiveViewportChangedEventArgs e) + { + _effectiveViewportChanged?.Invoke(this, e); + } + /// /// Marks a property as affecting the control's measurement. /// @@ -508,6 +550,7 @@ namespace Avalonia.Layout { var margin = Margin; + ApplyStyling(); ApplyTemplate(); var constrained = LayoutHelper.ApplyLayoutConstraints( @@ -518,17 +561,25 @@ namespace Avalonia.Layout var width = measured.Width; var height = measured.Height; - if (!double.IsNaN(Width)) { - width = Width; + double widthCache = Width; + + if (!double.IsNaN(widthCache)) + { + width = widthCache; + } } width = Math.Min(width, MaxWidth); width = Math.Max(width, MinWidth); - if (!double.IsNaN(Height)) { - height = Height; + double heightCache = Height; + + if (!double.IsNaN(heightCache)) + { + height = heightCache; + } } height = Math.Min(height, MaxHeight); @@ -540,8 +591,8 @@ namespace Avalonia.Layout if (UseLayoutRounding) { var scale = GetLayoutScale(); - width = Math.Ceiling(width * scale) / scale; - height = Math.Ceiling(height * scale) / scale; + width = LayoutHelper.RoundLayoutValue(width, scale); + height = LayoutHelper.RoundLayoutValue(height, scale); } return NonNegative(new Size(width, height).Inflate(margin)); @@ -562,11 +613,19 @@ namespace Avalonia.Layout double width = 0; double height = 0; - foreach (ILayoutable child in this.GetVisualChildren()) + var visualChildren = VisualChildren; + var visualCount = visualChildren.Count; + + for (var i = 0; i < visualCount; i++) { - child.Measure(availableSize); - width = Math.Max(width, child.DesiredSize.Width); - height = Math.Max(height, child.DesiredSize.Height); + IVisual visual = visualChildren[i]; + + if (visual is ILayoutable layoutable) + { + layoutable.Measure(availableSize); + width = Math.Max(width, layoutable.DesiredSize.Width); + height = Math.Max(height, layoutable.DesiredSize.Height); + } } return new Size(width, height); @@ -594,6 +653,7 @@ namespace Avalonia.Layout var verticalAlignment = VerticalAlignment; var size = availableSizeMinusMargins; var scale = GetLayoutScale(); + var useLayoutRounding = UseLayoutRounding; if (horizontalAlignment != HorizontalAlignment.Stretch) { @@ -607,14 +667,10 @@ namespace Avalonia.Layout size = LayoutHelper.ApplyLayoutConstraints(this, size); - if (UseLayoutRounding) + if (useLayoutRounding) { - size = new Size( - Math.Ceiling(size.Width * scale) / scale, - Math.Ceiling(size.Height * scale) / scale); - availableSizeMinusMargins = new Size( - Math.Ceiling(availableSizeMinusMargins.Width * scale) / scale, - Math.Ceiling(availableSizeMinusMargins.Height * scale) / scale); + size = LayoutHelper.RoundLayoutSize(size, scale, scale); + availableSizeMinusMargins = LayoutHelper.RoundLayoutSize(availableSizeMinusMargins, scale, scale); } size = ArrangeOverride(size).Constrain(size); @@ -641,10 +697,10 @@ namespace Avalonia.Layout break; } - if (UseLayoutRounding) + if (useLayoutRounding) { - originX = Math.Floor(originX * scale) / scale; - originY = Math.Floor(originY * scale) / scale; + originX = LayoutHelper.RoundLayoutValue(originX, scale); + originY = LayoutHelper.RoundLayoutValue(originY, scale); } Bounds = new Rect(originX, originY, size.Width, size.Height); @@ -658,25 +714,88 @@ namespace Avalonia.Layout /// The actual size used. protected virtual Size ArrangeOverride(Size finalSize) { - foreach (ILayoutable child in this.GetVisualChildren().OfType()) + var arrangeRect = new Rect(finalSize); + + var visualChildren = VisualChildren; + var visualCount = visualChildren.Count; + + for (var i = 0; i < visualCount; i++) { - child.Arrange(new Rect(finalSize)); + IVisual visual = visualChildren[i]; + + if (visual is ILayoutable layoutable) + { + layoutable.Arrange(arrangeRect); + } } return finalSize; } - /// - protected override sealed void OnVisualParentChanged(IVisual oldParent, IVisual newParent) + protected sealed override void InvalidateStyles() { - foreach (ILayoutable i in this.GetSelfAndVisualDescendants()) + base.InvalidateStyles(); + InvalidateMeasure(); + } + + protected override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTreeCore(e); + + if (e.Root is ILayoutRoot r) { - i.InvalidateMeasure(); + if (_layoutUpdated is object) + { + r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated; + } + + if (_effectiveViewportChanged is object) + { + r.LayoutManager.RegisterEffectiveViewportListener(this); + } } + } + + protected override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTreeCore(e); + + if (e.Root is ILayoutRoot r) + { + if (_layoutUpdated is object) + { + r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated; + } + + if (_effectiveViewportChanged is object) + { + r.LayoutManager.UnregisterEffectiveViewportListener(this); + } + } + } + + /// + /// Called by InvalidateMeasure + /// + protected virtual void OnMeasureInvalidated() + { + } + + /// + protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent) + { + LayoutHelper.InvalidateSelfAndChildrenMeasure(this); base.OnVisualParentChanged(oldParent, newParent); } + /// + /// Called when the layout manager raises a LayoutUpdated event. + /// + /// The sender. + /// The event args. + private void LayoutManagedLayoutUpdated(object sender, EventArgs e) => _layoutUpdated?.Invoke(this, e); + /// /// Tests whether any of a 's properties include negative values, /// a NaN or Infinity. diff --git a/src/Avalonia.Layout/NonVirtualizingLayout.cs b/src/Avalonia.Layout/NonVirtualizingLayout.cs index fba91e66c7..fb6b0dd4c9 100644 --- a/src/Avalonia.Layout/NonVirtualizingLayout.cs +++ b/src/Avalonia.Layout/NonVirtualizingLayout.cs @@ -17,30 +17,6 @@ namespace Avalonia.Layout /// public abstract class NonVirtualizingLayout : AttachedLayout { - /// - public sealed override void InitializeForContext(LayoutContext context) - { - InitializeForContextCore((VirtualizingLayoutContext)context); - } - - /// - public sealed override void UninitializeForContext(LayoutContext context) - { - UninitializeForContextCore((VirtualizingLayoutContext)context); - } - - /// - public sealed override Size Measure(LayoutContext context, Size availableSize) - { - return MeasureOverride((VirtualizingLayoutContext)context, availableSize); - } - - /// - public sealed override Size Arrange(LayoutContext context, Size finalSize) - { - return ArrangeOverride((VirtualizingLayoutContext)context, finalSize); - } - /// /// When overridden in a derived class, initializes any per-container state the layout /// requires when it is attached to an ILayoutable container. @@ -49,7 +25,7 @@ namespace Avalonia.Layout /// The context object that facilitates communication between the layout and its host /// container. /// - protected virtual void InitializeForContextCore(VirtualizingLayoutContext context) + protected internal virtual void InitializeForContextCore(LayoutContext context) { } @@ -61,7 +37,7 @@ namespace Avalonia.Layout /// The context object that facilitates communication between the layout and its host /// container. /// - protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context) + protected internal virtual void UninitializeForContextCore(LayoutContext context) { } @@ -83,7 +59,9 @@ namespace Avalonia.Layout /// of the allocated sizes for child objects or based on other considerations such as a /// fixed container size. /// - protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize); + protected internal abstract Size MeasureOverride( + NonVirtualizingLayoutContext context, + Size availableSize); /// /// When implemented in a derived class, provides the behavior for the "Arrange" pass of @@ -98,6 +76,8 @@ namespace Avalonia.Layout /// its children. /// /// The actual size that is used after the element is arranged in layout. - protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize; + protected internal virtual Size ArrangeOverride( + NonVirtualizingLayoutContext context, + Size finalSize) => finalSize; } } diff --git a/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs b/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs new file mode 100644 index 0000000000..cef551f32e --- /dev/null +++ b/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs @@ -0,0 +1,31 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System.Collections.Generic; + +namespace Avalonia.Layout +{ + /// + /// Represents the base class for layout context types that do not support virtualization. + /// + public abstract class NonVirtualizingLayoutContext : LayoutContext + { + private VirtualizingLayoutContext _contextAdapter; + + /// + /// Gets the collection of child controls from the container that provides the context. + /// + public IReadOnlyList Children => ChildrenCore; + + /// + /// Implements the behavior for getting the return value of in a + /// derived or custom . + /// + protected abstract IReadOnlyList ChildrenCore { get; } + + internal VirtualizingLayoutContext GetVirtualizingContextAdapter() => + _contextAdapter ?? (_contextAdapter = new LayoutContextAdapter(this)); + } +} diff --git a/src/Avalonia.Layout/NonVirtualizingStackLayout.cs b/src/Avalonia.Layout/NonVirtualizingStackLayout.cs new file mode 100644 index 0000000000..6fa8601916 --- /dev/null +++ b/src/Avalonia.Layout/NonVirtualizingStackLayout.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Data; + +namespace Avalonia.Layout +{ + public class NonVirtualizingStackLayout : NonVirtualizingLayout + { + /// + /// Defines the property. + /// + public static readonly StyledProperty OrientationProperty = + StackLayout.OrientationProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty SpacingProperty = + StackLayout.SpacingProperty.AddOwner(); + + /// + /// Gets or sets the axis along which items are laid out. + /// + /// + /// One of the enumeration values that specifies the axis along which items are laid out. + /// The default is Vertical. + /// + public Orientation Orientation + { + get => GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + /// + /// Gets or sets a uniform distance (in pixels) between stacked items. It is applied in the + /// direction of the StackLayout's Orientation. + /// + public double Spacing + { + get => GetValue(SpacingProperty); + set => SetValue(SpacingProperty, value); + } + + protected internal override Size MeasureOverride( + NonVirtualizingLayoutContext context, + Size availableSize) + { + var extentU = 0.0; + var extentV = 0.0; + var childCount = context.Children.Count; + var isVertical = Orientation == Orientation.Vertical; + var spacing = Spacing; + var constraint = isVertical ? + availableSize.WithHeight(double.PositiveInfinity) : + availableSize.WithWidth(double.PositiveInfinity); + + for (var i = 0; i < childCount; ++i) + { + var element = context.Children[i]; + + if (!element.IsVisible) + { + continue; + } + + element.Measure(constraint); + + if (isVertical) + { + extentU += element.DesiredSize.Height; + extentV = Math.Max(extentV, element.DesiredSize.Width); + } + else + { + extentU += element.DesiredSize.Width; + extentV = Math.Max(extentV, element.DesiredSize.Height); + } + + if (i < childCount - 1) + { + extentU += spacing; + } + } + + return isVertical ? new Size(extentV, extentU) : new Size(extentU, extentV); + } + + protected internal override Size ArrangeOverride( + NonVirtualizingLayoutContext context, + Size finalSize) + { + var u = 0.0; + var childCount = context.Children.Count; + var isVertical = Orientation == Orientation.Vertical; + var spacing = Spacing; + var bounds = new Rect(); + + for (var i = 0; i < childCount; ++i) + { + var element = context.Children[i]; + + if (!element.IsVisible) + { + continue; + } + + bounds = isVertical ? + LayoutVertical(element, u, finalSize) : + LayoutHorizontal(element, u, finalSize); + element.Arrange(bounds); + u = (isVertical ? bounds.Bottom : bounds.Right) + spacing; + } + + return new Size( + Math.Max(finalSize.Width, bounds.Width), + Math.Max(finalSize.Height, bounds.Height)); + } + + private static Rect LayoutVertical(ILayoutable element, double y, Size constraint) + { + var x = 0.0; + var width = element.DesiredSize.Width; + + switch (element.HorizontalAlignment) + { + case HorizontalAlignment.Center: + x += (constraint.Width - element.DesiredSize.Width) / 2; + break; + case HorizontalAlignment.Right: + x += constraint.Width - element.DesiredSize.Width; + break; + case HorizontalAlignment.Stretch: + width = constraint.Width; + break; + } + + return new Rect(x, y, width, element.DesiredSize.Height); + } + + private static Rect LayoutHorizontal(ILayoutable element, double x, Size constraint) + { + var y = 0.0; + var height = element.DesiredSize.Height; + + switch (element.VerticalAlignment) + { + case VerticalAlignment.Center: + y += (constraint.Height - element.DesiredSize.Height) / 2; + break; + case VerticalAlignment.Bottom: + y += constraint.Height - element.DesiredSize.Height; + break; + case VerticalAlignment.Stretch: + height = constraint.Height; + break; + } + + return new Rect(x, y, element.DesiredSize.Width, height); + } + } +} diff --git a/src/Avalonia.Layout/Orientation.cs b/src/Avalonia.Layout/Orientation.cs index f03b087adc..54f09b5877 100644 --- a/src/Avalonia.Layout/Orientation.cs +++ b/src/Avalonia.Layout/Orientation.cs @@ -1,7 +1,4 @@ -// 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. - -namespace Avalonia.Layout +namespace Avalonia.Layout { /// /// Defines vertical or horizontal orientation. diff --git a/src/Avalonia.Layout/Properties/AssemblyInfo.cs b/src/Avalonia.Layout/Properties/AssemblyInfo.cs index 392ad323e5..0a6a32493a 100644 --- a/src/Avalonia.Layout/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Layout/Properties/AssemblyInfo.cs @@ -1,6 +1,3 @@ -// 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 System.Runtime.CompilerServices; using Avalonia.Metadata; diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs index e9735b9b31..909c7bc7eb 100644 --- a/src/Avalonia.Layout/StackLayout.cs +++ b/src/Avalonia.Layout/StackLayout.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Specialized; +using Avalonia.Data; +using Avalonia.Logging; namespace Avalonia.Layout { @@ -13,6 +15,12 @@ namespace Avalonia.Layout /// public class StackLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates { + /// + /// Defines the property. + /// + public static readonly StyledProperty DisableVirtualizationProperty = + AvaloniaProperty.Register(nameof(DisableVirtualization)); + /// /// Defines the property. /// @@ -35,6 +43,15 @@ namespace Avalonia.Layout LayoutId = "StackLayout"; } + /// + /// Gets or sets a value indicating whether virtualization is disabled on the layout. + /// + public bool DisableVirtualization + { + get => GetValue(DisableVirtualizationProperty); + set => SetValue(DisableVirtualizationProperty, value); + } + /// /// Gets or sets the axis along which items are laid out. /// @@ -91,8 +108,15 @@ namespace Avalonia.Layout _orientation.MajorStart(extent) + (remainingItems * averageElementSize)); } + else + { + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Estimating extent with no realized elements", + LayoutId); + } } + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Extent is ({Size}). Based on average {Average}", + LayoutId, extent.Size, averageElementSize); return extent; } @@ -233,7 +257,7 @@ namespace Avalonia.Layout return new FlowLayoutAnchorInfo { Index = anchorIndex, Offset = offset, }; } - protected override void InitializeForContextCore(VirtualizingLayoutContext context) + protected internal override void InitializeForContextCore(VirtualizingLayoutContext context) { var state = context.LayoutState; var stackState = state as StackLayoutState; @@ -253,36 +277,39 @@ namespace Avalonia.Layout stackState.InitializeForContext(context, this); } - protected override void UninitializeForContextCore(VirtualizingLayoutContext context) + protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context) { var stackState = (StackLayoutState)context.LayoutState; stackState.UninitializeForContext(context); } - protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) + protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) { + ((StackLayoutState)context.LayoutState).OnMeasureStart(); + var desiredSize = GetFlowAlgorithm(context).Measure( availableSize, context, false, 0, Spacing, + int.MaxValue, _orientation.ScrollOrientation, + DisableVirtualization, LayoutId); return new Size(desiredSize.Width, desiredSize.Height); } - protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) + protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) { var value = GetFlowAlgorithm(context).Arrange( finalSize, context, + false, FlowLayoutAlgorithm.LineAlignment.Start, LayoutId); - ((StackLayoutState)context.LayoutState).OnArrangeLayoutEnd(); - return new Size(value.Width, value.Height); } @@ -293,11 +320,11 @@ namespace Avalonia.Layout InvalidateLayout(); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (e.Property == OrientationProperty) + if (change.Property == OrientationProperty) { - var orientation = (Orientation)e.NewValue; + var orientation = change.NewValue.GetValueOrDefault(); //Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation. //Horizontal Orientation means we have a Horizontal ScrollOrientation. diff --git a/src/Avalonia.Layout/StackLayoutState.cs b/src/Avalonia.Layout/StackLayoutState.cs index 05ad9bca8e..e6164e02e6 100644 --- a/src/Avalonia.Layout/StackLayoutState.cs +++ b/src/Avalonia.Layout/StackLayoutState.cs @@ -56,6 +56,6 @@ namespace Avalonia.Layout MaxArrangeBounds = Math.Max(MaxArrangeBounds, minorSize); } - internal void OnArrangeLayoutEnd() => MaxArrangeBounds = 0; + internal void OnMeasureStart() => MaxArrangeBounds = 0; } } diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs index edc2042922..68f08d7cbb 100644 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Specialized; +using Avalonia.Data; +using Avalonia.Logging; namespace Avalonia.Layout { @@ -110,6 +112,12 @@ namespace Avalonia.Layout public static readonly StyledProperty MinRowSpacingProperty = AvaloniaProperty.Register(nameof(MinRowSpacing)); + /// + /// Defines the property. + /// + public static readonly StyledProperty MaximumRowsOrColumnsProperty = + AvaloniaProperty.Register(nameof(MinItemWidth)); + /// /// Defines the property. /// @@ -123,6 +131,7 @@ namespace Avalonia.Layout private double _minColumnSpacing; private UniformGridLayoutItemsJustification _itemsJustification; private UniformGridLayoutItemsStretch _itemsStretch; + private int _maximumRowsOrColumns = int.MaxValue; /// /// Initializes a new instance of the class. @@ -219,6 +228,15 @@ namespace Avalonia.Layout set => SetValue(MinRowSpacingProperty, value); } + /// + /// Gets or sets the maximum row or column count. + /// + public int MaximumRowsOrColumns + { + get => GetValue(MaximumRowsOrColumnsProperty); + set => SetValue(MaximumRowsOrColumnsProperty, value); + } + /// /// Gets or sets the axis along which items are laid out. /// @@ -269,15 +287,17 @@ namespace Avalonia.Layout { var gridState = (UniformGridLayoutState)context.LayoutState; var lastExtent = gridState.FlowAlgorithm.LastExtent; - int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); - double majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context); - double realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent); + var itemsPerLine = Math.Min( // note use of unsigned ints + Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), + Math.Max(1u, (uint)_maximumRowsOrColumns)); + var majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context); + var realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent); if ((realizationWindowStartWithinExtent + _orientation.MajorSize(realizationRect)) >= 0 && realizationWindowStartWithinExtent <= majorSize) { double offset = Math.Max(0.0, _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent)); int anchorRowIndex = (int)(offset / GetMajorSizeWithSpacing(context)); - anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine)); + anchorIndex = (int)Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine)); bounds = GetLayoutRectForDataIndex(availableSize, anchorIndex, lastExtent, context); } } @@ -299,7 +319,9 @@ namespace Avalonia.Layout int count = context.ItemCount; if (targetIndex >= 0 && targetIndex < count) { - int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); + int itemsPerLine = (int)Math.Min( // note use of unsigned ints + Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), + Math.Max(1u, _maximumRowsOrColumns)); int indexOfFirstInLine = (targetIndex / itemsPerLine) * itemsPerLine; index = indexOfFirstInLine; var state = context.LayoutState as UniformGridLayoutState; @@ -329,17 +351,21 @@ namespace Avalonia.Layout // Constants int itemsCount = context.ItemCount; double availableSizeMinor = _orientation.Minor(availableSize); - int itemsPerLine = Math.Max(1, !double.IsInfinity(availableSizeMinor) ? - (int)(availableSizeMinor / GetMinorSizeWithSpacing(context)) : itemsCount); + int itemsPerLine = + (int)Math.Min( // note use of unsigned ints + Math.Max(1u, !double.IsInfinity(availableSizeMinor) + ? (uint)(availableSizeMinor / GetMinorSizeWithSpacing(context)) + : (uint)itemsCount), + Math.Max(1u, _maximumRowsOrColumns)); double lineSize = GetMajorSizeWithSpacing(context); if (itemsCount > 0) { _orientation.SetMinorSize( ref extent, - !double.IsInfinity(availableSizeMinor) ? + !double.IsInfinity(availableSizeMinor) && _itemsStretch == UniformGridLayoutItemsStretch.Fill ? availableSizeMinor : - Math.Max(0.0, itemsCount * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing)); + Math.Max(0.0, itemsPerLine * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing)); _orientation.SetMajorSize( ref extent, Math.Max(0.0, (itemsCount / itemsPerLine) * lineSize - (double)LineSpacing)); @@ -354,8 +380,14 @@ namespace Avalonia.Layout ref extent, _orientation.MajorEnd(lastRealizedLayoutBounds) - _orientation.MajorStart(extent) + (remainingItems / itemsPerLine) * lineSize); } + else + { + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Estimating extent with no realized elements", LayoutId); + } } + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Extent is ({Size}). Based on lineSize {LineSize} and items per line {ItemsPerLine}", + LayoutId, extent.Size, lineSize, itemsPerLine); return extent; } @@ -367,7 +399,7 @@ namespace Avalonia.Layout { } - protected override void InitializeForContextCore(VirtualizingLayoutContext context) + protected internal override void InitializeForContextCore(VirtualizingLayoutContext context) { var state = context.LayoutState; var gridState = state as UniformGridLayoutState; @@ -387,18 +419,18 @@ namespace Avalonia.Layout gridState.InitializeForContext(context, this); } - protected override void UninitializeForContextCore(VirtualizingLayoutContext context) + protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context) { var gridState = (UniformGridLayoutState)context.LayoutState; gridState.UninitializeForContext(context); } - protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) + protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) { // Set the width and height on the grid state. If the user already set them then use the preset. // If not, we have to measure the first element and get back a size which we're going to be using for the rest of the items. var gridState = (UniformGridLayoutState)context.LayoutState; - gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing); + gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing, _maximumRowsOrColumns); var desiredSize = GetFlowAlgorithm(context).Measure( availableSize, @@ -406,7 +438,9 @@ namespace Avalonia.Layout true, MinItemSpacing, LineSpacing, + _maximumRowsOrColumns, _orientation.ScrollOrientation, + false, LayoutId); // If after Measure the first item is in the realization rect, then we revoke grid state's ownership, @@ -416,11 +450,12 @@ namespace Avalonia.Layout return new Size(desiredSize.Width, desiredSize.Height); } - protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) + protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) { var value = GetFlowAlgorithm(context).Arrange( finalSize, context, + true, (FlowLayoutAlgorithm.LineAlignment)_itemsJustification, LayoutId); return new Size(value.Width, value.Height); @@ -436,40 +471,44 @@ namespace Avalonia.Layout gridState.ClearElementOnDataSourceChange(context, args); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (args.Property == OrientationProperty) + if (change.Property == OrientationProperty) { - var orientation = (Orientation)args.NewValue; + var orientation = change.NewValue.GetValueOrDefault(); //Note: For UniformGridLayout Vertical Orientation means we have a Horizontal ScrollOrientation. Horizontal Orientation means we have a Vertical ScrollOrientation. //i.e. the properties are the inverse of each other. var scrollOrientation = (orientation == Orientation.Horizontal) ? ScrollOrientation.Vertical : ScrollOrientation.Horizontal; _orientation.ScrollOrientation = scrollOrientation; } - else if (args.Property == MinColumnSpacingProperty) + else if (change.Property == MinColumnSpacingProperty) + { + _minColumnSpacing = change.NewValue.GetValueOrDefault(); + } + else if (change.Property == MinRowSpacingProperty) { - _minColumnSpacing = (double)args.NewValue; + _minRowSpacing = change.NewValue.GetValueOrDefault(); } - else if (args.Property == MinRowSpacingProperty) + else if (change.Property == ItemsJustificationProperty) { - _minRowSpacing = (double)args.NewValue; + _itemsJustification = change.NewValue.GetValueOrDefault(); } - else if (args.Property == ItemsJustificationProperty) + else if (change.Property == ItemsStretchProperty) { - _itemsJustification = (UniformGridLayoutItemsJustification)args.NewValue; + _itemsStretch = change.NewValue.GetValueOrDefault(); } - else if (args.Property == ItemsStretchProperty) + else if (change.Property == MinItemWidthProperty) { - _itemsStretch = (UniformGridLayoutItemsStretch)args.NewValue; + _minItemWidth = change.NewValue.GetValueOrDefault(); } - else if (args.Property == MinItemWidthProperty) + else if (change.Property == MinItemHeightProperty) { - _minItemWidth = (double)args.NewValue; + _minItemHeight = change.NewValue.GetValueOrDefault(); } - else if (args.Property == MinItemHeightProperty) + else if (change.Property == MaximumRowsOrColumnsProperty) { - _minItemHeight = (double)args.NewValue; + _maximumRowsOrColumns = change.NewValue.GetValueOrDefault(); } InvalidateLayout(); @@ -499,7 +538,9 @@ namespace Avalonia.Layout Rect lastExtent, VirtualizingLayoutContext context) { - int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); + int itemsPerLine = (int)Math.Min( //note use of unsigned ints + Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), + Math.Max(1u, _maximumRowsOrColumns)); int rowIndex = (int)(index / itemsPerLine); int indexInRow = index - (rowIndex * itemsPerLine); diff --git a/src/Avalonia.Layout/UniformGridLayoutState.cs b/src/Avalonia.Layout/UniformGridLayoutState.cs index e6d75bcf35..62c5174775 100644 --- a/src/Avalonia.Layout/UniformGridLayoutState.cs +++ b/src/Avalonia.Layout/UniformGridLayoutState.cs @@ -48,8 +48,14 @@ namespace Avalonia.Layout UniformGridLayoutItemsStretch stretch, Orientation orientation, double minRowSpacing, - double minColumnSpacing) + double minColumnSpacing, + int maxItemsPerLine) { + if (maxItemsPerLine == 0) + { + maxItemsPerLine = 1; + } + if (context.ItemCount > 0) { // If the first element is realized we don't need to cache it or to get it from the context @@ -57,7 +63,7 @@ namespace Avalonia.Layout if (realizedElement != null) { realizedElement.Measure(availableSize); - SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing); + SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine); _cachedFirstElement = null; } else @@ -72,7 +78,7 @@ namespace Avalonia.Layout _cachedFirstElement.Measure(availableSize); - SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing); + SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine); // See if we can move ownership to the flow algorithm. If we can, we do not need a local cache. bool added = FlowAlgorithm.TryAddElement0(_cachedFirstElement); @@ -92,8 +98,14 @@ namespace Avalonia.Layout UniformGridLayoutItemsStretch stretch, Orientation orientation, double minRowSpacing, - double minColumnSpacing) + double minColumnSpacing, + int maxItemsPerLine) { + if (maxItemsPerLine == 0) + { + maxItemsPerLine = 1; + } + EffectiveItemWidth = (double.IsNaN(layoutItemWidth) ? element.DesiredSize.Width : layoutItemWidth); EffectiveItemHeight = (double.IsNaN(LayoutItemHeight) ? element.DesiredSize.Height : LayoutItemHeight); @@ -101,11 +113,17 @@ namespace Avalonia.Layout var minorItemSpacing = orientation == Orientation.Vertical ? minRowSpacing : minColumnSpacing; var itemSizeMinor = orientation == Orientation.Horizontal ? EffectiveItemWidth : EffectiveItemHeight; - itemSizeMinor += minorItemSpacing; - var numItemsPerColumn = (int)(Math.Max(1.0, availableSizeMinor / itemSizeMinor)); - var remainingSpace = ((int)availableSizeMinor) % ((int)itemSizeMinor); - var extraMinorPixelsForEachItem = remainingSpace / numItemsPerColumn; + double extraMinorPixelsForEachItem = 0.0; + if (!double.IsInfinity(availableSizeMinor)) + { + var numItemsPerColumn = Math.Min( + maxItemsPerLine, + Math.Max(1.0, availableSizeMinor / (itemSizeMinor + minorItemSpacing))); + var usedSpace = (numItemsPerColumn * (itemSizeMinor + minorItemSpacing)) - minorItemSpacing; + var remainingSpace = ((int)(availableSizeMinor - usedSpace)); + extraMinorPixelsForEachItem = remainingSpace / ((int)numItemsPerColumn); + } if (stretch == UniformGridLayoutItemsStretch.Fill) { diff --git a/src/Avalonia.Layout/VirtualLayoutContextAdapter.cs b/src/Avalonia.Layout/VirtualLayoutContextAdapter.cs new file mode 100644 index 0000000000..80ccee2114 --- /dev/null +++ b/src/Avalonia.Layout/VirtualLayoutContextAdapter.cs @@ -0,0 +1,42 @@ +using System.Collections; +using System.Collections.Generic; + +namespace Avalonia.Layout +{ + public class VirtualLayoutContextAdapter : NonVirtualizingLayoutContext + { + private readonly VirtualizingLayoutContext _virtualizingContext; + private ChildrenCollection _children; + + public VirtualLayoutContextAdapter(VirtualizingLayoutContext virtualizingContext) + { + _virtualizingContext = virtualizingContext; + } + + protected override object LayoutStateCore + { + get => _virtualizingContext.LayoutState; + set => _virtualizingContext.LayoutState = value; + } + + protected override IReadOnlyList ChildrenCore => + _children ?? (_children = new ChildrenCollection(_virtualizingContext)); + + private class ChildrenCollection : IReadOnlyList + { + private readonly VirtualizingLayoutContext _context; + public ChildrenCollection(VirtualizingLayoutContext context) => _context = context; + public ILayoutable this[int index] => _context.GetOrCreateElementAt(index); + public int Count => _context.ItemCount; + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < Count; ++i) + { + yield return this[i]; + } + } + } + } +} diff --git a/src/Avalonia.Layout/VirtualizingLayout.cs b/src/Avalonia.Layout/VirtualizingLayout.cs index 4c601175f3..15c7749dfe 100644 --- a/src/Avalonia.Layout/VirtualizingLayout.cs +++ b/src/Avalonia.Layout/VirtualizingLayout.cs @@ -19,30 +19,6 @@ namespace Avalonia.Layout /// public abstract class VirtualizingLayout : AttachedLayout { - /// - public sealed override void InitializeForContext(LayoutContext context) - { - InitializeForContextCore((VirtualizingLayoutContext)context); - } - - /// - public sealed override void UninitializeForContext(LayoutContext context) - { - UninitializeForContextCore((VirtualizingLayoutContext)context); - } - - /// - public sealed override Size Measure(LayoutContext context, Size availableSize) - { - return MeasureOverride((VirtualizingLayoutContext)context, availableSize); - } - - /// - public sealed override Size Arrange(LayoutContext context, Size finalSize) - { - return ArrangeOverride((VirtualizingLayoutContext)context, finalSize); - } - /// /// Notifies the layout when the data collection assigned to the container element (Items) /// has changed. @@ -70,7 +46,7 @@ namespace Avalonia.Layout /// The context object that facilitates communication between the layout and its host /// container. /// - protected virtual void InitializeForContextCore(VirtualizingLayoutContext context) + protected internal virtual void InitializeForContextCore(VirtualizingLayoutContext context) { } @@ -82,7 +58,7 @@ namespace Avalonia.Layout /// The context object that facilitates communication between the layout and its host /// container. /// - protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context) + protected internal virtual void UninitializeForContextCore(VirtualizingLayoutContext context) { } @@ -104,7 +80,9 @@ namespace Avalonia.Layout /// of the allocated sizes for child objects or based on other considerations such as a /// fixed container size. /// - protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize); + protected internal abstract Size MeasureOverride( + VirtualizingLayoutContext context, + Size availableSize); /// /// When implemented in a derived class, provides the behavior for the "Arrange" pass of @@ -119,7 +97,9 @@ namespace Avalonia.Layout /// its children. /// /// The actual size that is used after the element is arranged in layout. - protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize; + protected internal virtual Size ArrangeOverride( + VirtualizingLayoutContext context, + Size finalSize) => finalSize; /// /// Notifies the layout when the data collection assigned to the container element (Items) diff --git a/src/Avalonia.Layout/VirtualizingLayoutContext.cs b/src/Avalonia.Layout/VirtualizingLayoutContext.cs index 980daec2eb..079b91a90f 100644 --- a/src/Avalonia.Layout/VirtualizingLayoutContext.cs +++ b/src/Avalonia.Layout/VirtualizingLayoutContext.cs @@ -43,6 +43,8 @@ namespace Avalonia.Layout /// public abstract class VirtualizingLayoutContext : LayoutContext { + private NonVirtualizingLayoutContext _contextAdapter; + /// /// Gets the number of items in the data. /// @@ -186,5 +188,8 @@ namespace Avalonia.Layout /// /// The element to clear. protected abstract void RecycleElementCore(ILayoutable element); + + internal NonVirtualizingLayoutContext GetNonVirtualizingContextAdapter() => + _contextAdapter ?? (_contextAdapter = new VirtualLayoutContextAdapter(this)); } } diff --git a/src/Avalonia.Logging.Serilog/Avalonia.Logging.Serilog.csproj b/src/Avalonia.Logging.Serilog/Avalonia.Logging.Serilog.csproj deleted file mode 100644 index 3e76001556..0000000000 --- a/src/Avalonia.Logging.Serilog/Avalonia.Logging.Serilog.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - netstandard2.0 - - - - - - - diff --git a/src/Avalonia.Logging.Serilog/SerilogExtensions.cs b/src/Avalonia.Logging.Serilog/SerilogExtensions.cs deleted file mode 100644 index 2ce38769da..0000000000 --- a/src/Avalonia.Logging.Serilog/SerilogExtensions.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using Avalonia.Controls; -using Serilog; -using Serilog.Configuration; -using Serilog.Filters; -using SerilogLevel = Serilog.Events.LogEventLevel; - -namespace Avalonia.Logging.Serilog -{ - /// - /// Extension methods for Serilog logging. - /// - public static class SerilogExtensions - { - private const string DefaultTemplate = "[{Area}] {Message} ({SourceType} #{SourceHash})"; - - /// - /// Logs Avalonia events to the sink. - /// - /// The application class type. - /// The app builder instance. - /// The minimum level to log. - /// The app builder instance. - public static T LogToDebug( - this T builder, - LogEventLevel level = LogEventLevel.Warning) - where T : AppBuilderBase, new() - { - SerilogLogger.Initialize(new LoggerConfiguration() - .MinimumLevel.Is((SerilogLevel)level) - .Enrich.FromLogContext() - .WriteTo.Debug(outputTemplate: DefaultTemplate) - .CreateLogger()); - return builder; - } - - /// - /// Logs Avalonia events to the sink. - /// - /// The application class type. - /// The app builder instance. - /// The area to log. Valid values are listed in . - /// The minimum level to log. - /// The app builder instance. - public static T LogToDebug( - this T builder, - string area, - LogEventLevel level = LogEventLevel.Warning) - where T : AppBuilderBase, new() - { - SerilogLogger.Initialize(new LoggerConfiguration() - .MinimumLevel.Is((SerilogLevel)level) - .Filter.ByIncludingOnly(Matching.WithProperty("Area", area)) - .Enrich.FromLogContext() - .WriteTo.Debug(outputTemplate: DefaultTemplate) - .CreateLogger()); - return builder; - } - - /// - /// Logs Avalonia events to the sink. - /// - /// The application class type. - /// The app builder instance. - /// The minimum level to log. - /// The app builder instance. - public static T LogToTrace( - this T builder, - LogEventLevel level = LogEventLevel.Warning) - where T : AppBuilderBase, new() - { - SerilogLogger.Initialize(new LoggerConfiguration() - .MinimumLevel.Is((SerilogLevel)level) - .Enrich.FromLogContext() - .WriteTo.Trace(outputTemplate: DefaultTemplate) - .CreateLogger()); - return builder; - } - - /// - /// Logs Avalonia events to the sink. - /// - /// The application class type. - /// The app builder instance. - /// The area to log. Valid values are listed in . - /// The minimum level to log. - /// The app builder instance. - public static T LogToTrace( - this T builder, - string area, - LogEventLevel level = LogEventLevel.Warning) - where T : AppBuilderBase, new() - { - SerilogLogger.Initialize(new LoggerConfiguration() - .MinimumLevel.Is((SerilogLevel)level) - .Filter.ByIncludingOnly(Matching.WithProperty("Area", area)) - .Enrich.FromLogContext() - .WriteTo.Trace(outputTemplate: DefaultTemplate) - .CreateLogger()); - return builder; - } - } -} diff --git a/src/Avalonia.Logging.Serilog/SerilogLogger.cs b/src/Avalonia.Logging.Serilog/SerilogLogger.cs deleted file mode 100644 index 895ee268d2..0000000000 --- a/src/Avalonia.Logging.Serilog/SerilogLogger.cs +++ /dev/null @@ -1,154 +0,0 @@ -// 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 System; -using Serilog; -using Serilog.Context; -using AvaloniaLogEventLevel = Avalonia.Logging.LogEventLevel; -using SerilogLogEventLevel = Serilog.Events.LogEventLevel; - -namespace Avalonia.Logging.Serilog -{ - /// - /// Sends log output to serilog. - /// - public class SerilogLogger : ILogSink - { - private readonly ILogger _output; - - /// - /// Initializes a new instance of the class. - /// - /// The serilog logger to use. - public SerilogLogger(ILogger output) - { - _output = output; - } - - /// - /// Initializes the Avalonia logging with a new instance of a . - /// - /// The serilog logger to use. - public static void Initialize(ILogger output) - { - Logger.Sink = new SerilogLogger(output); - } - - public bool IsEnabled(LogEventLevel level) - { - return _output.IsEnabled((SerilogLogEventLevel)level); - } - - public void Log( - LogEventLevel level, - string area, - object source, - string messageTemplate) - { - Contract.Requires(area != null); - Contract.Requires(messageTemplate != null); - - using (PushLogContextProperties(area, source)) - { - _output.Write((SerilogLogEventLevel)level, messageTemplate); - } - } - - public void Log( - LogEventLevel level, - string area, object source, - string messageTemplate, - T0 propertyValue0) - { - Contract.Requires(area != null); - Contract.Requires(messageTemplate != null); - - using (PushLogContextProperties(area, source)) - { - _output.Write((SerilogLogEventLevel)level, messageTemplate, propertyValue0); - } - } - - public void Log( - LogEventLevel level, - string area, - object source, - string messageTemplate, - T0 propertyValue0, - T1 propertyValue1) - { - Contract.Requires(area != null); - Contract.Requires(messageTemplate != null); - - using (PushLogContextProperties(area, source)) - { - _output.Write((SerilogLogEventLevel)level, messageTemplate, propertyValue0, propertyValue1); - } - } - - public void Log( - LogEventLevel level, - string area, - object source, - string messageTemplate, - T0 propertyValue0, - T1 propertyValue1, - T2 propertyValue2) - { - Contract.Requires(area != null); - Contract.Requires(messageTemplate != null); - - using (PushLogContextProperties(area, source)) - { - _output.Write((SerilogLogEventLevel)level, messageTemplate, propertyValue0, propertyValue1, propertyValue2); - } - } - - /// - public void Log( - AvaloniaLogEventLevel level, - string area, - object source, - string messageTemplate, - params object[] propertyValues) - { - Contract.Requires(area != null); - Contract.Requires(messageTemplate != null); - - using (PushLogContextProperties(area, source)) - { - _output.Write((SerilogLogEventLevel)level, messageTemplate, propertyValues); - } - } - - private static LogContextDisposable PushLogContextProperties(string area, object source) - { - return new LogContextDisposable( - LogContext.PushProperty("Area", area), - LogContext.PushProperty("SourceType", source?.GetType()), - LogContext.PushProperty("SourceHash", source?.GetHashCode()) - ); - } - - private readonly struct LogContextDisposable : IDisposable - { - private readonly IDisposable _areaDisposable; - private readonly IDisposable _sourceTypeDisposable; - private readonly IDisposable _sourceHashDisposable; - - public LogContextDisposable(IDisposable areaDisposable, IDisposable sourceTypeDisposable, IDisposable sourceHashDisposable) - { - _areaDisposable = areaDisposable; - _sourceTypeDisposable = sourceTypeDisposable; - _sourceHashDisposable = sourceHashDisposable; - } - - public void Dispose() - { - _areaDisposable.Dispose(); - _sourceTypeDisposable.Dispose(); - _sourceHashDisposable.Dispose(); - } - } - } -} diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index 35e50b1b36..1a2bdeef1e 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -19,8 +19,10 @@ - - + + + + diff --git a/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs b/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs deleted file mode 100644 index ce2b03e355..0000000000 --- a/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs +++ /dev/null @@ -1,42 +0,0 @@ -// 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 System; -using System.Threading; -using Avalonia.Native.Interop; -using Avalonia.Rendering; - -namespace Avalonia.Native -{ - public class AvaloniaNativeDeferredRendererLock : IDeferredRendererLock - { - private readonly IAvnWindowBase _window; - - public AvaloniaNativeDeferredRendererLock(IAvnWindowBase window) - { - _window = window; - } - - public IDisposable TryLock() - { - if (_window.TryLock()) - return new UnlockDisposable(_window); - return null; - } - - private sealed class UnlockDisposable : IDisposable - { - private IAvnWindowBase _window; - - public UnlockDisposable(IAvnWindowBase window) - { - _window = window; - } - - public void Dispose() - { - Interlocked.Exchange(ref _window, null)?.Unlock(); - } - } - } -} diff --git a/src/Avalonia.Native/AvaloniaNativeDragSource.cs b/src/Avalonia.Native/AvaloniaNativeDragSource.cs new file mode 100644 index 0000000000..80d54d8a10 --- /dev/null +++ b/src/Avalonia.Native/AvaloniaNativeDragSource.cs @@ -0,0 +1,76 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Interactivity; +using Avalonia.Native.Interop; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Native +{ + class AvaloniaNativeDragSource : IPlatformDragSource + { + private readonly IAvaloniaNativeFactory _factory; + + public AvaloniaNativeDragSource(IAvaloniaNativeFactory factory) + { + _factory = factory; + } + + TopLevel FindRoot(IInteractive interactive) + { + while (interactive != null && !(interactive is IVisual)) + interactive = interactive.InteractiveParent; + if (interactive == null) + return null; + var visual = (IVisual)interactive; + return visual.VisualRoot as TopLevel; + } + + class DndCallback : CallbackBase, IAvnDndResultCallback + { + private TaskCompletionSource _tcs; + + public DndCallback(TaskCompletionSource tcs) + { + _tcs = tcs; + } + public void OnDragAndDropComplete(AvnDragDropEffects effect) + { + _tcs?.TrySetResult((DragDropEffects)effect); + _tcs = null; + } + } + + public Task DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects) + { + // Sanity check + var tl = FindRoot(triggerEvent.Source); + var view = tl?.PlatformImpl as WindowBaseImpl; + if (view == null) + throw new ArgumentException(); + + triggerEvent.Pointer.Capture(null); + + var tcs = new TaskCompletionSource(); + + var clipboardImpl = _factory.CreateDndClipboard(); + using (var clipboard = new ClipboardImpl(clipboardImpl)) + using (var cb = new DndCallback(tcs)) + { + if (data.Contains(DataFormats.Text)) + // API is synchronous, so it's OK + clipboard.SetTextAsync(data.GetText()).Wait(); + + view.BeginDraggingSession((AvnDragDropEffects)allowedEffects, + triggerEvent.GetPosition(tl).ToAvnPoint(), clipboardImpl, cb, + GCHandle.ToIntPtr(GCHandle.Alloc(data))); + } + + return tcs.Task; + } + } +} diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 950943d54a..0f2551ffeb 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -1,182 +1,21 @@ using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using System.Text; using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Platform; -using Avalonia.Input; +using Avalonia.Dialogs; using Avalonia.Native.Interop; -using Avalonia.Platform.Interop; using Avalonia.Threading; namespace Avalonia.Native { - enum OsxUnicodeSpecialKey - { - NSUpArrowFunctionKey = 0xF700, - NSDownArrowFunctionKey = 0xF701, - NSLeftArrowFunctionKey = 0xF702, - NSRightArrowFunctionKey = 0xF703, - NSF1FunctionKey = 0xF704, - NSF2FunctionKey = 0xF705, - NSF3FunctionKey = 0xF706, - NSF4FunctionKey = 0xF707, - NSF5FunctionKey = 0xF708, - NSF6FunctionKey = 0xF709, - NSF7FunctionKey = 0xF70A, - NSF8FunctionKey = 0xF70B, - NSF9FunctionKey = 0xF70C, - NSF10FunctionKey = 0xF70D, - NSF11FunctionKey = 0xF70E, - NSF12FunctionKey = 0xF70F, - NSF13FunctionKey = 0xF710, - NSF14FunctionKey = 0xF711, - NSF15FunctionKey = 0xF712, - NSF16FunctionKey = 0xF713, - NSF17FunctionKey = 0xF714, - NSF18FunctionKey = 0xF715, - NSF19FunctionKey = 0xF716, - NSF20FunctionKey = 0xF717, - NSF21FunctionKey = 0xF718, - NSF22FunctionKey = 0xF719, - NSF23FunctionKey = 0xF71A, - NSF24FunctionKey = 0xF71B, - NSF25FunctionKey = 0xF71C, - NSF26FunctionKey = 0xF71D, - NSF27FunctionKey = 0xF71E, - NSF28FunctionKey = 0xF71F, - NSF29FunctionKey = 0xF720, - NSF30FunctionKey = 0xF721, - NSF31FunctionKey = 0xF722, - NSF32FunctionKey = 0xF723, - NSF33FunctionKey = 0xF724, - NSF34FunctionKey = 0xF725, - NSF35FunctionKey = 0xF726, - NSInsertFunctionKey = 0xF727, - NSDeleteFunctionKey = 0xF728, - NSHomeFunctionKey = 0xF729, - NSBeginFunctionKey = 0xF72A, - NSEndFunctionKey = 0xF72B, - NSPageUpFunctionKey = 0xF72C, - NSPageDownFunctionKey = 0xF72D, - NSPrintScreenFunctionKey = 0xF72E, - NSScrollLockFunctionKey = 0xF72F, - NSPauseFunctionKey = 0xF730, - NSSysReqFunctionKey = 0xF731, - NSBreakFunctionKey = 0xF732, - NSResetFunctionKey = 0xF733, - NSStopFunctionKey = 0xF734, - NSMenuFunctionKey = 0xF735, - NSUserFunctionKey = 0xF736, - NSSystemFunctionKey = 0xF737, - NSPrintFunctionKey = 0xF738, - NSClearLineFunctionKey = 0xF739, - NSClearDisplayFunctionKey = 0xF73A, - NSInsertLineFunctionKey = 0xF73B, - NSDeleteLineFunctionKey = 0xF73C, - NSInsertCharFunctionKey = 0xF73D, - NSDeleteCharFunctionKey = 0xF73E, - NSPrevFunctionKey = 0xF73F, - NSNextFunctionKey = 0xF740, - NSSelectFunctionKey = 0xF741, - NSExecuteFunctionKey = 0xF742, - NSUndoFunctionKey = 0xF743, - NSRedoFunctionKey = 0xF744, - NSFindFunctionKey = 0xF745, - NSHelpFunctionKey = 0xF746, - NSModeSwitchFunctionKey = 0xF747 - } - - public class MenuActionCallback : CallbackBase, IAvnActionCallback - { - private Action _action; - - public MenuActionCallback(Action action) - { - _action = action; - } - - void IAvnActionCallback.Run() - { - _action?.Invoke(); - } - } - - public class PredicateCallback : CallbackBase, IAvnPredicateCallback - { - private Func _predicate; - - public PredicateCallback(Func predicate) - { - _predicate = predicate; - } - - bool IAvnPredicateCallback.Evaluate() - { - return _predicate(); - } - } - class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter { private IAvaloniaNativeFactory _factory; - private NativeMenu _menu; private bool _resetQueued; private bool _exported = false; private IAvnWindow _nativeWindow; - private List _menuItems = new List(); - - private static Dictionary osxKeys = new Dictionary - { - {Key.Up, OsxUnicodeSpecialKey.NSUpArrowFunctionKey }, - {Key.Down, OsxUnicodeSpecialKey.NSDownArrowFunctionKey }, - {Key.Left, OsxUnicodeSpecialKey.NSLeftArrowFunctionKey }, - {Key.Right, OsxUnicodeSpecialKey.NSRightArrowFunctionKey }, - { Key.F1, OsxUnicodeSpecialKey.NSF1FunctionKey }, - { Key.F2, OsxUnicodeSpecialKey.NSF2FunctionKey }, - { Key.F3, OsxUnicodeSpecialKey.NSF3FunctionKey }, - { Key.F4, OsxUnicodeSpecialKey.NSF4FunctionKey }, - { Key.F5, OsxUnicodeSpecialKey.NSF5FunctionKey }, - { Key.F6, OsxUnicodeSpecialKey.NSF6FunctionKey }, - { Key.F7, OsxUnicodeSpecialKey.NSF7FunctionKey }, - { Key.F8, OsxUnicodeSpecialKey.NSF8FunctionKey }, - { Key.F9, OsxUnicodeSpecialKey.NSF9FunctionKey }, - { Key.F10, OsxUnicodeSpecialKey.NSF10FunctionKey }, - { Key.F11, OsxUnicodeSpecialKey.NSF11FunctionKey }, - { Key.F12, OsxUnicodeSpecialKey.NSF12FunctionKey }, - { Key.F13, OsxUnicodeSpecialKey.NSF13FunctionKey }, - { Key.F14, OsxUnicodeSpecialKey.NSF14FunctionKey }, - { Key.F15, OsxUnicodeSpecialKey.NSF15FunctionKey }, - { Key.F16, OsxUnicodeSpecialKey.NSF16FunctionKey }, - { Key.F17, OsxUnicodeSpecialKey.NSF17FunctionKey }, - { Key.F18, OsxUnicodeSpecialKey.NSF18FunctionKey }, - { Key.F19, OsxUnicodeSpecialKey.NSF19FunctionKey }, - { Key.F20, OsxUnicodeSpecialKey.NSF20FunctionKey }, - { Key.F21, OsxUnicodeSpecialKey.NSF21FunctionKey }, - { Key.F22, OsxUnicodeSpecialKey.NSF22FunctionKey }, - { Key.F23, OsxUnicodeSpecialKey.NSF23FunctionKey }, - { Key.F24, OsxUnicodeSpecialKey.NSF24FunctionKey }, - { Key.Insert, OsxUnicodeSpecialKey.NSInsertFunctionKey }, - { Key.Delete, OsxUnicodeSpecialKey.NSDeleteFunctionKey }, - { Key.Home, OsxUnicodeSpecialKey.NSHomeFunctionKey }, - //{ Key.Begin, OsxUnicodeSpecialKey.NSBeginFunctionKey }, - { Key.End, OsxUnicodeSpecialKey.NSEndFunctionKey }, - { Key.PageUp, OsxUnicodeSpecialKey.NSPageUpFunctionKey }, - { Key.PageDown, OsxUnicodeSpecialKey.NSPageDownFunctionKey }, - { Key.PrintScreen, OsxUnicodeSpecialKey.NSPrintScreenFunctionKey }, - { Key.Scroll, OsxUnicodeSpecialKey.NSScrollLockFunctionKey }, - //{ Key.SysReq, OsxUnicodeSpecialKey.NSSysReqFunctionKey }, - //{ Key.Break, OsxUnicodeSpecialKey.NSBreakFunctionKey }, - //{ Key.Reset, OsxUnicodeSpecialKey.NSResetFunctionKey }, - //{ Key.Stop, OsxUnicodeSpecialKey.NSStopFunctionKey }, - //{ Key.Menu, OsxUnicodeSpecialKey.NSMenuFunctionKey }, - //{ Key.UserFunction, OsxUnicodeSpecialKey.NSUserFunctionKey }, - //{ Key.SystemFunction, OsxUnicodeSpecialKey.NSSystemFunctionKey }, - { Key.Print, OsxUnicodeSpecialKey.NSPrintFunctionKey }, - //{ Key.ClearLine, OsxUnicodeSpecialKey.NSClearLineFunctionKey }, - //{ Key.ClearDisplay, OsxUnicodeSpecialKey.NSClearDisplayFunctionKey }, - }; + private NativeMenu _menu; + private IAvnMenu _nativeMenu; public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) { @@ -190,7 +29,6 @@ namespace Avalonia.Native { _factory = factory; - _menu = NativeMenu.GetMenu(Application.Current); DoLayoutReset(); } @@ -200,57 +38,70 @@ namespace Avalonia.Native public void SetNativeMenu(NativeMenu menu) { - if (menu == null) - menu = new NativeMenu(); - - if (_menu != null) - ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= OnMenuItemsChanged; - _menu = menu; - ((INotifyCollectionChanged)_menu.Items).CollectionChanged += OnMenuItemsChanged; + _menu = menu == null ? new NativeMenu() : menu; DoLayoutReset(); } - private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + internal void UpdateIfNeeded() { - QueueReset(); + if (_resetQueued) + { + DoLayoutReset(); + } } - private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e) + private static NativeMenu CreateDefaultAppMenu() { - QueueReset(); + var result = new NativeMenu(); + + var aboutItem = new NativeMenuItem + { + Header = "About Avalonia", + }; + + aboutItem.Clicked += async (sender, e) => + { + var dialog = new AboutAvaloniaDialog(); + + var mainWindow = (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow; + + await dialog.ShowDialog(mainWindow); + }; + + result.Add(aboutItem); + + return result; } void DoLayoutReset() { _resetQueued = false; - foreach (var i in _menuItems) - { - i.PropertyChanged -= OnItemPropertyChanged; - if (i.Menu != null) - ((INotifyCollectionChanged)i.Menu.Items).CollectionChanged -= OnMenuItemsChanged; - } - _menuItems.Clear(); - - if(_nativeWindow is null) + if (_nativeWindow is null) { - _menu = NativeMenu.GetMenu(Application.Current); + var appMenu = NativeMenu.GetMenu(Application.Current); - if(_menu != null) + if (appMenu == null) { - SetMenu(_menu); + appMenu = CreateDefaultAppMenu(); + NativeMenu.SetMenu(Application.Current, appMenu); } + + SetMenu(appMenu); } else { - SetMenu(_nativeWindow, _menu?.Items); + if (_menu != null) + { + SetMenu(_nativeWindow, _menu); + } } _exported = true; } - private void QueueReset() + internal void QueueReset() { if (_resetQueued) return; @@ -258,188 +109,64 @@ namespace Avalonia.Native Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background); } - private IAvnAppMenu CreateSubmenu(ICollection children) + private void SetMenu(NativeMenu menu) { - var menu = _factory.CreateMenu(); - - SetChildren(menu, children); + var menuItem = menu.Parent; - return menu; - } + var appMenuHolder = menuItem?.Parent; - private void AddMenuItem(NativeMenuItem item) - { - if (item.Menu?.Items != null) + if (menu.Parent is null) { - ((INotifyCollectionChanged)item.Menu.Items).CollectionChanged += OnMenuItemsChanged; + menuItem = new NativeMenuItem(); } - } - private static string ConvertOSXSpecialKeyCodes(Key key) - { - if (osxKeys.ContainsKey(key)) + if (appMenuHolder is null) { - return ((char)osxKeys[key]).ToString(); - } - else - { - return key.ToString().ToLower(); - } - } + appMenuHolder = new NativeMenu(); - private void SetChildren(IAvnAppMenu menu, ICollection children) - { - foreach (var i in children) - { - if (i is NativeMenuItem item) - { - AddMenuItem(item); - - var menuItem = _factory.CreateMenuItem(); - - using (var buffer = new Utf8Buffer(item.Header)) - { - menuItem.Title = buffer.DangerousGetHandle(); - } - - if (item.Gesture != null) - { - using (var buffer = new Utf8Buffer(ConvertOSXSpecialKeyCodes(item.Gesture.Key))) - { - menuItem.SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers); - } - } - - menuItem.SetAction(new PredicateCallback(() => - { - if (item.Command != null || item.HasClickHandlers) - { - return item.Enabled; - } - - return false; - }), new MenuActionCallback(() => { item.RaiseClick(); })); - menu.AddItem(menuItem); - - if (item.Menu?.Items?.Count > 0) - { - var submenu = _factory.CreateMenu(); - - using (var buffer = new Utf8Buffer(item.Header)) - { - submenu.Title = buffer.DangerousGetHandle(); - } - - menuItem.SetSubMenu(submenu); - - AddItemsToMenu(submenu, item.Menu?.Items); - } - } - else if (i is NativeMenuItemSeperator seperator) - { - menu.AddItem(_factory.CreateMenuItemSeperator()); - } + appMenuHolder.Add(menuItem); } - } - private void AddItemsToMenu(IAvnAppMenu menu, ICollection items, bool isMainMenu = false) - { - foreach (var i in items) - { - if (i is NativeMenuItem item) - { - var menuItem = _factory.CreateMenuItem(); - - AddMenuItem(item); - - menuItem.SetAction(new PredicateCallback(() => - { - if (item.Command != null || item.HasClickHandlers) - { - return item.Enabled; - } - - return false; - }), new MenuActionCallback(() => { item.RaiseClick(); })); - - if (item.Menu?.Items.Count > 0 || isMainMenu) - { - var subMenu = CreateSubmenu(item.Menu?.Items); - - menuItem.SetSubMenu(subMenu); - - using (var buffer = new Utf8Buffer(item.Header)) - { - subMenu.Title = buffer.DangerousGetHandle(); - } - } - else - { - using (var buffer = new Utf8Buffer(item.Header)) - { - menuItem.Title = buffer.DangerousGetHandle(); - } - - if (item.Gesture != null) - { - using (var buffer = new Utf8Buffer(item.Gesture.Key.ToString().ToLower())) - { - menuItem.SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers); - } - } - } - - menu.AddItem(menuItem); - } - else if(i is NativeMenuItemSeperator seperator) - { - menu.AddItem(_factory.CreateMenuItemSeperator()); - } - } - } + menuItem.Menu = menu; - private void SetMenu(NativeMenu menu) - { - var appMenu = _factory.ObtainAppMenu(); + var setMenu = false; - if (appMenu is null) + if (_nativeMenu is null) { - appMenu = _factory.CreateMenu(); - } + _nativeMenu = IAvnMenu.Create(_factory); - var menuItem = menu.Parent; + _nativeMenu.Initialise(this, appMenuHolder, ""); - if(menu.Parent is null) - { - menuItem = new NativeMenuItem(); + setMenu = true; } - menuItem.Menu = menu; - - appMenu.Clear(); - AddItemsToMenu(appMenu, new List { menuItem }); + _nativeMenu.Update(_factory, appMenuHolder); - _factory.SetAppMenu(appMenu); + if (setMenu) + { + _factory.SetAppMenu(_nativeMenu); + } } - private void SetMenu(IAvnWindow avnWindow, ICollection menuItems) + private void SetMenu(IAvnWindow avnWindow, NativeMenu menu) { - if (menuItems is null) + var setMenu = false; + + if (_nativeMenu is null) { - menuItems = new List(); - } + _nativeMenu = IAvnMenu.Create(_factory); - var appMenu = avnWindow.ObtainMainMenu(); + _nativeMenu.Initialise(this, menu, ""); - if (appMenu is null) - { - appMenu = _factory.CreateMenu(); + setMenu = true; } - appMenu.Clear(); - AddItemsToMenu(appMenu, menuItems); + _nativeMenu.Update(_factory, menu); - avnWindow.SetMainMenu(appMenu); + if(setMenu) + { + avnWindow.SetMainMenu(_nativeMenu); + } } } } diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index ddb71b61bb..cfd47d48de 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -1,7 +1,6 @@ -// 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 System; using System.Runtime.InteropServices; +using System.Security.Cryptography; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; @@ -17,11 +16,11 @@ namespace Avalonia.Native { private readonly IAvaloniaNativeFactory _factory; private AvaloniaNativePlatformOptions _options; + private GlPlatformFeature _glFeature; [DllImport("libAvaloniaNative")] static extern IntPtr CreateAvaloniaNative(); - internal static readonly MouseDevice MouseDevice = new MouseDevice(); internal static readonly KeyboardDevice KeyboardDevice = new KeyboardDevice(); public Size DoubleClickSize => new Size(4, 4); @@ -78,10 +77,18 @@ namespace Avalonia.Native _factory = factory; } + class GCHandleDeallocator : CallbackBase, IAvnGCHandleDeallocatorCallback + { + public void FreeGCHandle(IntPtr handle) + { + GCHandle.FromIntPtr(handle).Free(); + } + } + void DoInitialize(AvaloniaNativePlatformOptions options) { _options = options; - _factory.Initialize(); + _factory.Initialize(new GCHandleDeallocator()); if (_factory.MacOptions != null) { var macOpts = AvaloniaLocator.Current.GetService(); @@ -95,24 +102,27 @@ namespace Avalonia.Native .Bind().ToConstant(new CursorFactory(_factory.CreateCursorFactory())) .Bind().ToSingleton() .Bind().ToConstant(KeyboardDevice) - .Bind().ToConstant(MouseDevice) .Bind().ToConstant(this) .Bind().ToConstant(this) .Bind().ToConstant(new ClipboardImpl(_factory.CreateClipboard())) .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs())) - .Bind().ToConstant(new GlPlatformFeature(_factory.ObtainGlFeature())) - .Bind().ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Windows)) - .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()); + .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta)) + .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()) + .Bind().ToConstant(new AvaloniaNativeDragSource(_factory)) + ; + if (_options.UseGpu) + AvaloniaLocator.CurrentMutable.Bind() + .ToConstant(_glFeature = new GlPlatformFeature(_factory.ObtainGlDisplay())); } public IWindowImpl CreateWindow() { - return new WindowImpl(_factory, _options); + return new WindowImpl(_factory, _options, _glFeature); } - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public IWindowImpl CreateEmbeddableWindow() { throw new NotImplementedException(); } diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index 091056142f..545659f813 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Controls; using Avalonia.Native; diff --git a/src/Avalonia.Native/AvnString.cs b/src/Avalonia.Native/AvnString.cs new file mode 100644 index 0000000000..11b1a33276 --- /dev/null +++ b/src/Avalonia.Native/AvnString.cs @@ -0,0 +1,54 @@ +using System.Runtime.InteropServices; + +namespace Avalonia.Native.Interop +{ + unsafe partial class IAvnString + { + private string _managed; + private byte[] _bytes; + + public string String + { + get + { + if (_managed == null) + { + var ptr = Pointer(); + if (ptr == null) + return null; + _managed = System.Text.Encoding.UTF8.GetString((byte*)ptr.ToPointer(), Length()); + } + + return _managed; + } + } + + public byte[] Bytes + { + get + { + if (_bytes == null) + { + _bytes = new byte[Length()]; + Marshal.Copy(Pointer(), _bytes, 0, _bytes.Length); + } + + return _bytes; + } + } + + public override string ToString() => String; + } + + partial class IAvnStringArray + { + public string[] ToStringArray() + { + var arr = new string[Count]; + for(uint c = 0; c() is PlatformThreadingInterface threadingInterface) + { + threadingInterface.TerminateNativeApp(); + + threadingInterface.DispatchException(ExceptionDispatchInfo.Capture(e)); + } + } } } diff --git a/src/Avalonia.Native/ClipboardImpl.cs b/src/Avalonia.Native/ClipboardImpl.cs index c756a6d9c2..554e7a497a 100644 --- a/src/Avalonia.Native/ClipboardImpl.cs +++ b/src/Avalonia.Native/ClipboardImpl.cs @@ -1,18 +1,22 @@ -// 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 System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using System.Runtime.InteropServices; +using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Native.Interop; using Avalonia.Platform.Interop; namespace Avalonia.Native { - class ClipboardImpl : IClipboard + class ClipboardImpl : IClipboard, IDisposable { private IAvnClipboard _native; - + private const string NSPasteboardTypeString = "public.utf8-plain-text"; + private const string NSFilenamesPboardType = "NSFilenamesPboardType"; + private const string NSPasteboardTypeFileUrl = "public.file-url"; + public ClipboardImpl(IAvnClipboard native) { _native = native; @@ -25,14 +29,10 @@ namespace Avalonia.Native return Task.CompletedTask; } - public unsafe Task GetTextAsync() + public Task GetTextAsync() { - using (var text = _native.GetText()) - { - var result = System.Text.Encoding.UTF8.GetString((byte*)text.Pointer(), text.Length()); - - return Task.FromResult(result); - } + using (var text = _native.GetText(NSPasteboardTypeString)) + return Task.FromResult(text.String); } public Task SetTextAsync(string text) @@ -43,11 +43,116 @@ namespace Avalonia.Native { using (var buffer = new Utf8Buffer(text)) { - _native.SetText(buffer.DangerousGetHandle()); + _native.SetText(NSPasteboardTypeString, buffer.DangerousGetHandle()); } } return Task.CompletedTask; } + + public IEnumerable GetFormats() + { + var rv = new List(); + using (var formats = _native.ObtainFormats()) + { + var cnt = formats.Count; + for (uint c = 0; c < cnt; c++) + { + using (var fmt = formats.Get(c)) + { + if(fmt.String == NSPasteboardTypeString) + rv.Add(DataFormats.Text); + if(fmt.String == NSFilenamesPboardType) + rv.Add(DataFormats.FileNames); + } + } + } + + return rv; + } + + public void Dispose() + { + _native?.Dispose(); + _native = null; + } + + public IEnumerable GetFileNames() + { + using (var strings = _native.GetStrings(NSFilenamesPboardType)) + return strings.ToStringArray(); + } + + public unsafe Task SetDataObjectAsync(IDataObject data) + { + _native.Clear(); + foreach (var fmt in data.GetDataFormats()) + { + var o = data.Get(fmt); + if(o is string s) + using (var b = new Utf8Buffer(s)) + _native.SetText(fmt, b.DangerousGetHandle()); + else if(o is byte[] bytes) + fixed (byte* pbytes = bytes) + _native.SetBytes(fmt, new IntPtr(pbytes), bytes.Length); + } + return Task.CompletedTask; + } + + public Task GetFormatsAsync() + { + using (var n = _native.ObtainFormats()) + return Task.FromResult(n.ToStringArray()); + } + + public async Task GetDataAsync(string format) + { + if (format == DataFormats.Text) + return await GetTextAsync(); + if (format == DataFormats.FileNames) + return GetFileNames(); + using (var n = _native.GetBytes(format)) + return n.Bytes; + } + } + + class ClipboardDataObject : IDataObject, IDisposable + { + private ClipboardImpl _clipboard; + private List _formats; + + public ClipboardDataObject(IAvnClipboard clipboard) + { + _clipboard = new ClipboardImpl(clipboard); + } + + public void Dispose() + { + _clipboard?.Dispose(); + _clipboard = null; + } + + List Formats => _formats ??= _clipboard.GetFormats().ToList(); + + public IEnumerable GetDataFormats() => Formats; + + public bool Contains(string dataFormat) => Formats.Contains(dataFormat); + + public string GetText() + { + // bad idea in general, but API is synchronous anyway + return _clipboard.GetTextAsync().Result; + } + + public IEnumerable GetFileNames() => _clipboard.GetFileNames(); + + public object Get(string dataFormat) + { + if (dataFormat == DataFormats.Text) + return GetText(); + if (dataFormat == DataFormats.FileNames) + return GetFileNames(); + return null; + } } } diff --git a/src/Avalonia.Native/Cursor.cs b/src/Avalonia.Native/Cursor.cs index 1093eca1a4..3c65367c12 100644 --- a/src/Avalonia.Native/Cursor.cs +++ b/src/Avalonia.Native/Cursor.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using Avalonia.Input; using Avalonia.Platform; using Avalonia.Native.Interop; diff --git a/src/Avalonia.Native/DeferredFramebuffer.cs b/src/Avalonia.Native/DeferredFramebuffer.cs index adc721de42..8ea7b20b8c 100644 --- a/src/Avalonia.Native/DeferredFramebuffer.cs +++ b/src/Avalonia.Native/DeferredFramebuffer.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Runtime.InteropServices; using Avalonia.Native.Interop; using Avalonia.Platform; @@ -21,7 +18,7 @@ namespace Avalonia.Native Size = new PixelSize(width, height); RowBytes = width * 4; Dpi = dpi; - Format = PixelFormat.Rgba8888; + Format = PixelFormat.Bgra8888; } public IntPtr Address { get; set; } diff --git a/src/Avalonia.Native/DynLoader.cs b/src/Avalonia.Native/DynLoader.cs index c9c064fb95..237ea8d0af 100644 --- a/src/Avalonia.Native/DynLoader.cs +++ b/src/Avalonia.Native/DynLoader.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Runtime.InteropServices; /* diff --git a/src/Avalonia.Native/GlPlatformFeature.cs b/src/Avalonia.Native/GlPlatformFeature.cs index e39680bdee..e321db6eda 100644 --- a/src/Avalonia.Native/GlPlatformFeature.cs +++ b/src/Avalonia.Native/GlPlatformFeature.cs @@ -8,59 +8,89 @@ namespace Avalonia.Native { class GlPlatformFeature : IWindowingPlatformGlFeature { + private readonly IAvnGlDisplay _display; - public GlPlatformFeature(IAvnGlFeature feature) + public GlPlatformFeature(IAvnGlDisplay display) { - Display = new GlDisplay(feature.ObtainDisplay()); - ImmediateContext = new GlContext(Display, feature.ObtainImmediateContext()); + _display = display; + var immediate = display.CreateContext(null); + var deferred = display.CreateContext(immediate); + + + int major, minor; + GlInterface glInterface; + using (immediate.MakeCurrent()) + { + var basic = new GlBasicInfoInterface(display.GetProcAddress); + basic.GetIntegerv(GlConsts.GL_MAJOR_VERSION, out major); + basic.GetIntegerv(GlConsts.GL_MINOR_VERSION, out minor); + _version = new GlVersion(GlProfileType.OpenGL, major, minor); + glInterface = new GlInterface(_version, (name) => + { + var rv = _display.GetProcAddress(name); + return rv; + }); + } + + GlDisplay = new GlDisplay(display, glInterface, immediate.SampleCount, immediate.StencilSize); + + ImmediateContext = new GlContext(GlDisplay, immediate, _version); + DeferredContext = new GlContext(GlDisplay, deferred, _version); } - public IGlContext ImmediateContext { get; } - public GlDisplay Display { get; } + internal IGlContext ImmediateContext { get; } + public IGlContext MainContext => DeferredContext; + internal GlContext DeferredContext { get; } + internal GlDisplay GlDisplay; + private readonly GlVersion _version; + + public IGlContext CreateContext() => new GlContext(GlDisplay, + _display.CreateContext(((GlContext)ImmediateContext).Context), _version); } - class GlDisplay : IGlDisplay + class GlDisplay { private readonly IAvnGlDisplay _display; - public GlDisplay(IAvnGlDisplay display) + public GlDisplay(IAvnGlDisplay display, GlInterface glInterface, int sampleCount, int stencilSize) { _display = display; - GlInterface = new GlInterface((name, optional) => - { - var rv = _display.GetProcAddress(name); - if (rv == IntPtr.Zero && !optional) - throw new OpenGlException($"{name} not found in system OpenGL"); - return rv; - }); + SampleCount = sampleCount; + StencilSize = stencilSize; + GlInterface = glInterface; } - public GlDisplayType Type => GlDisplayType.OpenGL2; - public GlInterface GlInterface { get; } - public int SampleCount => _display.GetSampleCount(); + public int SampleCount { get; } - public int StencilSize => _display.GetStencilSize(); + public int StencilSize { get; } - public void ClearContext() => _display.ClearContext(); + public void ClearContext() => _display.LegacyClearCurrentContext(); } class GlContext : IGlContext { - public IAvnGlContext Context { get; } + private readonly GlDisplay _display; + public IAvnGlContext Context { get; private set; } - public GlContext(GlDisplay display, IAvnGlContext context) + public GlContext(GlDisplay display, IAvnGlContext context, GlVersion version) { - Display = display; + _display = display; Context = context; + Version = version; } - public IGlDisplay Display { get; } + public GlVersion Version { get; } + public GlInterface GlInterface => _display.GlInterface; + public int SampleCount => _display.SampleCount; + public int StencilSize => _display.StencilSize; + public IDisposable MakeCurrent() => Context.MakeCurrent(); - public void MakeCurrent() + public void Dispose() { - Context.MakeCurrent(); + Context.Dispose(); + Context = null; } } @@ -68,15 +98,18 @@ namespace Avalonia.Native class GlPlatformSurfaceRenderTarget : IGlPlatformSurfaceRenderTarget { private IAvnGlSurfaceRenderTarget _target; - public GlPlatformSurfaceRenderTarget(IAvnGlSurfaceRenderTarget target) + private readonly IGlContext _context; + + public GlPlatformSurfaceRenderTarget(IAvnGlSurfaceRenderTarget target, IGlContext context) { _target = target; + _context = context; } public IGlPlatformSurfaceRenderingSession BeginDraw() { var feature = (GlPlatformFeature)AvaloniaLocator.Current.GetService(); - return new GlPlatformSurfaceRenderingSession(feature.Display, _target.BeginDrawing()); + return new GlPlatformSurfaceRenderingSession(_context, _target.BeginDrawing()); } public void Dispose() @@ -90,13 +123,13 @@ namespace Avalonia.Native { private IAvnGlSurfaceRenderingSession _session; - public GlPlatformSurfaceRenderingSession(GlDisplay display, IAvnGlSurfaceRenderingSession session) + public GlPlatformSurfaceRenderingSession(IGlContext context, IAvnGlSurfaceRenderingSession session) { - Display = display; + Context = context; _session = session; } - public IGlDisplay Display { get; } + public IGlContext Context { get; } public PixelSize Size { @@ -109,6 +142,9 @@ namespace Avalonia.Native public double Scaling => _session.GetScaling(); + + public bool IsYFlipped => true; + public void Dispose() { _session?.Dispose(); @@ -119,14 +155,17 @@ namespace Avalonia.Native class GlPlatformSurface : IGlPlatformSurface { private readonly IAvnWindowBase _window; + private readonly IGlContext _context; - public GlPlatformSurface(IAvnWindowBase window) + public GlPlatformSurface(IAvnWindowBase window, IGlContext context) { _window = window; + _context = context; } public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() { - return new GlPlatformSurfaceRenderTarget(_window.CreateGlRenderTarget()); + return new GlPlatformSurfaceRenderTarget(_window.CreateGlRenderTarget(), _context); } + } } diff --git a/src/Avalonia.Native/Helpers.cs b/src/Avalonia.Native/Helpers.cs index a77e7d4ff8..25e8250232 100644 --- a/src/Avalonia.Native/Helpers.cs +++ b/src/Avalonia.Native/Helpers.cs @@ -1,7 +1,4 @@ -// 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.Native.Interop; +using Avalonia.Native.Interop; namespace Avalonia.Native { diff --git a/src/Avalonia.Native/IAvnMenu.cs b/src/Avalonia.Native/IAvnMenu.cs new file mode 100644 index 0000000000..8a49559a02 --- /dev/null +++ b/src/Avalonia.Native/IAvnMenu.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Reactive.Disposables; +using Avalonia.Controls; +using Avalonia.Platform.Interop; + +namespace Avalonia.Native.Interop +{ + class MenuEvents : CallbackBase, IAvnMenuEvents + { + private IAvnMenu _parent; + + public void Initialise(IAvnMenu parent) + { + _parent = parent; + } + + public void NeedsUpdate() + { + _parent?.RaiseNeedsUpdate(); + } + } + + public partial class IAvnMenu + { + private MenuEvents _events; + private AvaloniaNativeMenuExporter _exporter; + private List _menuItems = new List(); + private Dictionary _menuItemLookup = new Dictionary(); + private CompositeDisposable _propertyDisposables = new CompositeDisposable(); + + internal void RaiseNeedsUpdate() + { + (ManagedMenu as INativeMenuExporterEventsImplBridge).RaiseNeedsUpdate(); + + _exporter.UpdateIfNeeded(); + } + + internal NativeMenu ManagedMenu { get; private set; } + + public static IAvnMenu Create(IAvaloniaNativeFactory factory) + { + var events = new MenuEvents(); + + var menu = factory.CreateMenu(events); + + events.Initialise(menu); + + menu._events = events; + + return menu; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _events.Dispose(); + } + } + + private void RemoveAndDispose(IAvnMenuItem item) + { + _menuItemLookup.Remove(item.ManagedMenuItem); + _menuItems.Remove(item); + RemoveItem(item); + + item.Deinitialise(); + item.Dispose(); + } + + private void MoveExistingTo(int index, IAvnMenuItem item) + { + _menuItems.Remove(item); + _menuItems.Insert(index, item); + + RemoveItem(item); + InsertItem(index, item); + } + + private IAvnMenuItem CreateNewAt(IAvaloniaNativeFactory factory, int index, NativeMenuItemBase item) + { + var result = CreateNew(factory, item); + + result.Initialise(item); + + _menuItemLookup.Add(result.ManagedMenuItem, result); + _menuItems.Insert(index, result); + + InsertItem(index, result); + + return result; + } + + private IAvnMenuItem CreateNew(IAvaloniaNativeFactory factory, NativeMenuItemBase item) + { + var nativeItem = item is NativeMenuItemSeperator ? factory.CreateMenuItemSeperator() : factory.CreateMenuItem(); + nativeItem.ManagedMenuItem = item; + + return nativeItem; + } + + internal void Initialise(AvaloniaNativeMenuExporter exporter, NativeMenu managedMenu, string title) + { + _exporter = exporter; + ManagedMenu = managedMenu; + + ((INotifyCollectionChanged)ManagedMenu.Items).CollectionChanged += OnMenuItemsChanged; + + if (!string.IsNullOrWhiteSpace(title)) + { + using (var buffer = new Utf8Buffer(title)) + { + Title = buffer.DangerousGetHandle(); + } + } + } + + internal void Deinitialise() + { + ((INotifyCollectionChanged)ManagedMenu.Items).CollectionChanged -= OnMenuItemsChanged; + + foreach (var item in _menuItems) + { + item.Deinitialise(); + item.Dispose(); + } + } + + internal void Update(IAvaloniaNativeFactory factory, NativeMenu menu) + { + if (menu != ManagedMenu) + { + throw new ArgumentException("The menu being updated does not match.", nameof(menu)); + } + + for (int i = 0; i < menu.Items.Count; i++) + { + IAvnMenuItem nativeItem; + + if (i >= _menuItems.Count) + { + nativeItem = CreateNewAt(factory, i, menu.Items[i]); + } + else if (menu.Items[i] == _menuItems[i].ManagedMenuItem) + { + nativeItem = _menuItems[i]; + } + else if (_menuItemLookup.TryGetValue(menu.Items[i], out nativeItem)) + { + MoveExistingTo(i, nativeItem); + } + else + { + nativeItem = CreateNewAt(factory, i, menu.Items[i]); + } + + if (menu.Items[i] is NativeMenuItem nmi) + { + nativeItem.Update(_exporter, factory, nmi); + } + } + + while (_menuItems.Count > menu.Items.Count) + { + RemoveAndDispose(_menuItems[_menuItems.Count - 1]); + } + } + + private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + _exporter.QueueReset(); + } + } +} diff --git a/src/Avalonia.Native/IAvnMenuItem.cs b/src/Avalonia.Native/IAvnMenuItem.cs new file mode 100644 index 0000000000..c8819d1994 --- /dev/null +++ b/src/Avalonia.Native/IAvnMenuItem.cs @@ -0,0 +1,175 @@ +using System; +using System.IO; +using System.Reactive.Disposables; +using Avalonia.Controls; +using Avalonia.Media.Imaging; +using Avalonia.Platform.Interop; + +namespace Avalonia.Native.Interop +{ + public partial class IAvnMenuItem + { + private IAvnMenu _subMenu; + private CompositeDisposable _propertyDisposables = new CompositeDisposable(); + private IDisposable _currentActionDisposable; + + public NativeMenuItemBase ManagedMenuItem { get; set; } + + private void UpdateTitle(string title) + { + using (var buffer = new Utf8Buffer(string.IsNullOrWhiteSpace(title) ? "" : title)) + { + Title = buffer.DangerousGetHandle(); + } + } + + private void UpdateIsChecked(bool isChecked) + { + IsChecked = isChecked; + } + + private void UpdateToggleType(NativeMenuItemToggleType toggleType) + { + ToggleType = (AvnMenuItemToggleType)toggleType; + } + + private unsafe void UpdateIcon (IBitmap icon) + { + if(icon is null) + { + SetIcon(IntPtr.Zero, 0); + } + else + { + using(var ms = new MemoryStream()) + { + icon.Save(ms); + + var imageData = ms.ToArray(); + + fixed(void* ptr = imageData) + { + SetIcon(new IntPtr(ptr), imageData.Length); + } + } + } + } + + private void UpdateGesture(Input.KeyGesture gesture) + { + // todo ensure backend can cope with setting null gesture. + using (var buffer = new Utf8Buffer(gesture == null ? "" : OsxUnicodeKeys.ConvertOSXSpecialKeyCodes(gesture.Key))) + { + var modifiers = gesture == null ? AvnInputModifiers.AvnInputModifiersNone : (AvnInputModifiers)gesture.KeyModifiers; + SetGesture(buffer.DangerousGetHandle(), modifiers); + } + } + + private void UpdateAction(NativeMenuItem item) + { + _currentActionDisposable?.Dispose(); + + var action = new PredicateCallback(() => + { + if (item.Command != null || item.HasClickHandlers) + { + return item.IsEnabled; + } + + return false; + }); + + var callback = new MenuActionCallback(() => { (item as INativeMenuItemExporterEventsImplBridge)?.RaiseClicked(); }); + + _currentActionDisposable = Disposable.Create(() => + { + action.Dispose(); + callback.Dispose(); + }); + + SetAction(action, callback); + } + + internal void Initialise(NativeMenuItemBase nativeMenuItem) + { + ManagedMenuItem = nativeMenuItem; + + if (ManagedMenuItem is NativeMenuItem item) + { + UpdateTitle(item.Header); + + UpdateGesture(item.Gesture); + + UpdateAction(ManagedMenuItem as NativeMenuItem); + + UpdateToggleType(item.ToggleType); + + UpdateIcon(item.Icon); + + UpdateIsChecked(item.IsChecked); + + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.HeaderProperty) + .Subscribe(x => UpdateTitle(x))); + + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.GestureProperty) + .Subscribe(x => UpdateGesture(x))); + + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.CommandProperty) + .Subscribe(x => UpdateAction(ManagedMenuItem as NativeMenuItem))); + + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.ToggleTypeProperty) + .Subscribe(x => UpdateToggleType(x))); + + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.IsCheckedProperty) + .Subscribe(x => UpdateIsChecked(x))); + + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.IconProperty) + .Subscribe(x => UpdateIcon(x))); + } + } + + internal void Deinitialise() + { + if (_subMenu != null) + { + SetSubMenu(null); + _subMenu.Deinitialise(); + _subMenu.Dispose(); + _subMenu = null; + } + + _propertyDisposables?.Dispose(); + _currentActionDisposable?.Dispose(); + } + + internal void Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenuItem item) + { + if (item != ManagedMenuItem) + { + throw new ArgumentException("The item does not match the menuitem being updated.", nameof(item)); + } + + if (item.Menu != null) + { + if (_subMenu == null) + { + _subMenu = IAvnMenu.Create(factory); + + _subMenu.Initialise(exporter, item.Menu, item.Header); + + SetSubMenu(_subMenu); + } + + _subMenu.Update(factory, item.Menu); + } + + if (item.Menu == null && _subMenu != null) + { + _subMenu.Deinitialise(); + _subMenu.Dispose(); + + SetSubMenu(null); + } + } + } +} diff --git a/src/Avalonia.Native/IconLoader.cs b/src/Avalonia.Native/IconLoader.cs index 608c24d031..edb8b94e83 100644 --- a/src/Avalonia.Native/IconLoader.cs +++ b/src/Avalonia.Native/IconLoader.cs @@ -1,7 +1,4 @@ -// 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 System.IO; +using System.IO; using Avalonia.Platform; namespace Avalonia.Native diff --git a/src/Avalonia.Native/Mappings.xml b/src/Avalonia.Native/Mappings.xml index 7ac6377f78..fcaa31a249 100644 --- a/src/Avalonia.Native/Mappings.xml +++ b/src/Avalonia.Native/Mappings.xml @@ -19,5 +19,7 @@ + + diff --git a/src/Avalonia.Native/MenuActionCallback.cs b/src/Avalonia.Native/MenuActionCallback.cs new file mode 100644 index 0000000000..5318195f30 --- /dev/null +++ b/src/Avalonia.Native/MenuActionCallback.cs @@ -0,0 +1,20 @@ +using System; +using Avalonia.Native.Interop; + +namespace Avalonia.Native +{ + public class MenuActionCallback : CallbackBase, IAvnActionCallback + { + private Action _action; + + public MenuActionCallback(Action action) + { + _action = action; + } + + void IAvnActionCallback.Run() + { + _action?.Invoke(); + } + } +} diff --git a/src/Avalonia.Native/NativeControlHostImpl.cs b/src/Avalonia.Native/NativeControlHostImpl.cs new file mode 100644 index 0000000000..0777c6416b --- /dev/null +++ b/src/Avalonia.Native/NativeControlHostImpl.cs @@ -0,0 +1,136 @@ +using System; +using Avalonia.Controls.Platform; +using Avalonia.Native.Interop; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Native +{ + class NativeControlHostImpl : IDisposable, INativeControlHostImpl + { + private IAvnNativeControlHost _host; + + public NativeControlHostImpl(IAvnNativeControlHost host) + { + _host = host; + } + + public void Dispose() + { + _host?.Dispose(); + _host = null; + } + + class DestroyableNSView : INativeControlHostDestroyableControlHandle + { + private IAvnNativeControlHost _impl; + private IntPtr _nsView; + + public DestroyableNSView(IAvnNativeControlHost impl) + { + _impl = new IAvnNativeControlHost(impl.NativePointer); + _impl.AddRef(); + _nsView = _impl.CreateDefaultChild(IntPtr.Zero); + } + + public IntPtr Handle => _nsView; + public string HandleDescriptor => "NSView"; + public void Destroy() + { + if (_impl != null) + { + _impl.DestroyDefaultChild(_nsView); + _impl.Dispose(); + _impl = null; + _nsView = IntPtr.Zero; + } + } + } + + public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent) + => new DestroyableNSView(_host); + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment( + Func create) + { + var a = new Attachment(_host.CreateAttachment()); + try + { + var child = create(a.GetParentHandle()); + a.InitWithChild(child); + a.AttachedTo = this; + return a; + } + catch + { + a.Dispose(); + throw; + } + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle) + { + var a = new Attachment(_host.CreateAttachment()); + a.InitWithChild(handle); + a.AttachedTo = this; + return a; + } + + public bool IsCompatibleWith(IPlatformHandle handle) => handle.HandleDescriptor == "NSView"; + + class Attachment : INativeControlHostControlTopLevelAttachment + { + private IAvnNativeControlHostTopLevelAttachment _native; + private NativeControlHostImpl _attachedTo; + public IPlatformHandle GetParentHandle() => new PlatformHandle(_native.ParentHandle, "NSView"); + public Attachment(IAvnNativeControlHostTopLevelAttachment native) + { + _native = native; + } + + public void Dispose() + { + if (_native != null) + { + _native.ReleaseChild(); + _native.Dispose(); + _native = null; + } + } + + public INativeControlHostImpl AttachedTo + { + get => _attachedTo; + set + { + var host = (NativeControlHostImpl)value; + if(host == null) + _native.AttachTo(null); + else + _native.AttachTo(host._host); + _attachedTo = host; + } + } + + public bool IsCompatibleWith(INativeControlHostImpl host) => host is NativeControlHostImpl; + + public void Hide() + { + _native?.Hide(); + } + + public void ShowInBounds(TransformedBounds transformedBounds) + { + if (_attachedTo == null) + throw new InvalidOperationException("Native control isn't attached to a toplevel"); + var bounds = transformedBounds.Bounds.TransformToAABB(transformedBounds.Transform); + bounds = new Rect(bounds.X, bounds.Y, Math.Max(1, bounds.Width), + Math.Max(1, bounds.Height)); + _native.MoveTo((float) bounds.X, (float) bounds.Y, (float) bounds.Width, (float) bounds.Height); + } + + public void InitWithChild(IPlatformHandle handle) + => _native.InitializeWithChildHandle(handle.Handle); + } + } +} diff --git a/src/Avalonia.Native/OsxManagedPopupPositionerPopupImplHelper.cs b/src/Avalonia.Native/OsxManagedPopupPositionerPopupImplHelper.cs index 6c98e3c0cc..8aa9b1a122 100644 --- a/src/Avalonia.Native/OsxManagedPopupPositionerPopupImplHelper.cs +++ b/src/Avalonia.Native/OsxManagedPopupPositionerPopupImplHelper.cs @@ -1,6 +1,4 @@ -// 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.Controls.Primitives.PopupPositioning; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Platform; namespace Avalonia.Native @@ -11,8 +9,7 @@ namespace Avalonia.Native { } - public override Point TranslatePoint(Point pt) => pt; - public override Size TranslateSize(Size size) => size; + public override double Scaling => 1; } } diff --git a/src/Avalonia.Native/OsxUnicodeKeys.cs b/src/Avalonia.Native/OsxUnicodeKeys.cs new file mode 100644 index 0000000000..b4056c8cd8 --- /dev/null +++ b/src/Avalonia.Native/OsxUnicodeKeys.cs @@ -0,0 +1,147 @@ +using System.Collections.Generic; +using Avalonia.Input; + +namespace Avalonia.Native.Interop +{ + internal static class OsxUnicodeKeys + { + enum OsxUnicodeSpecialKey + { + NSUpArrowFunctionKey = 0xF700, + NSDownArrowFunctionKey = 0xF701, + NSLeftArrowFunctionKey = 0xF702, + NSRightArrowFunctionKey = 0xF703, + NSF1FunctionKey = 0xF704, + NSF2FunctionKey = 0xF705, + NSF3FunctionKey = 0xF706, + NSF4FunctionKey = 0xF707, + NSF5FunctionKey = 0xF708, + NSF6FunctionKey = 0xF709, + NSF7FunctionKey = 0xF70A, + NSF8FunctionKey = 0xF70B, + NSF9FunctionKey = 0xF70C, + NSF10FunctionKey = 0xF70D, + NSF11FunctionKey = 0xF70E, + NSF12FunctionKey = 0xF70F, + NSF13FunctionKey = 0xF710, + NSF14FunctionKey = 0xF711, + NSF15FunctionKey = 0xF712, + NSF16FunctionKey = 0xF713, + NSF17FunctionKey = 0xF714, + NSF18FunctionKey = 0xF715, + NSF19FunctionKey = 0xF716, + NSF20FunctionKey = 0xF717, + NSF21FunctionKey = 0xF718, + NSF22FunctionKey = 0xF719, + NSF23FunctionKey = 0xF71A, + NSF24FunctionKey = 0xF71B, + NSF25FunctionKey = 0xF71C, + NSF26FunctionKey = 0xF71D, + NSF27FunctionKey = 0xF71E, + NSF28FunctionKey = 0xF71F, + NSF29FunctionKey = 0xF720, + NSF30FunctionKey = 0xF721, + NSF31FunctionKey = 0xF722, + NSF32FunctionKey = 0xF723, + NSF33FunctionKey = 0xF724, + NSF34FunctionKey = 0xF725, + NSF35FunctionKey = 0xF726, + NSInsertFunctionKey = 0xF727, + NSDeleteFunctionKey = 0xF728, + NSHomeFunctionKey = 0xF729, + NSBeginFunctionKey = 0xF72A, + NSEndFunctionKey = 0xF72B, + NSPageUpFunctionKey = 0xF72C, + NSPageDownFunctionKey = 0xF72D, + NSPrintScreenFunctionKey = 0xF72E, + NSScrollLockFunctionKey = 0xF72F, + NSPauseFunctionKey = 0xF730, + NSSysReqFunctionKey = 0xF731, + NSBreakFunctionKey = 0xF732, + NSResetFunctionKey = 0xF733, + NSStopFunctionKey = 0xF734, + NSMenuFunctionKey = 0xF735, + NSUserFunctionKey = 0xF736, + NSSystemFunctionKey = 0xF737, + NSPrintFunctionKey = 0xF738, + NSClearLineFunctionKey = 0xF739, + NSClearDisplayFunctionKey = 0xF73A, + NSInsertLineFunctionKey = 0xF73B, + NSDeleteLineFunctionKey = 0xF73C, + NSInsertCharFunctionKey = 0xF73D, + NSDeleteCharFunctionKey = 0xF73E, + NSPrevFunctionKey = 0xF73F, + NSNextFunctionKey = 0xF740, + NSSelectFunctionKey = 0xF741, + NSExecuteFunctionKey = 0xF742, + NSUndoFunctionKey = 0xF743, + NSRedoFunctionKey = 0xF744, + NSFindFunctionKey = 0xF745, + NSHelpFunctionKey = 0xF746, + NSModeSwitchFunctionKey = 0xF747 + } + + private static Dictionary s_osxKeys = new Dictionary + { + {Key.Up, OsxUnicodeSpecialKey.NSUpArrowFunctionKey }, + {Key.Down, OsxUnicodeSpecialKey.NSDownArrowFunctionKey }, + {Key.Left, OsxUnicodeSpecialKey.NSLeftArrowFunctionKey }, + {Key.Right, OsxUnicodeSpecialKey.NSRightArrowFunctionKey }, + { Key.F1, OsxUnicodeSpecialKey.NSF1FunctionKey }, + { Key.F2, OsxUnicodeSpecialKey.NSF2FunctionKey }, + { Key.F3, OsxUnicodeSpecialKey.NSF3FunctionKey }, + { Key.F4, OsxUnicodeSpecialKey.NSF4FunctionKey }, + { Key.F5, OsxUnicodeSpecialKey.NSF5FunctionKey }, + { Key.F6, OsxUnicodeSpecialKey.NSF6FunctionKey }, + { Key.F7, OsxUnicodeSpecialKey.NSF7FunctionKey }, + { Key.F8, OsxUnicodeSpecialKey.NSF8FunctionKey }, + { Key.F9, OsxUnicodeSpecialKey.NSF9FunctionKey }, + { Key.F10, OsxUnicodeSpecialKey.NSF10FunctionKey }, + { Key.F11, OsxUnicodeSpecialKey.NSF11FunctionKey }, + { Key.F12, OsxUnicodeSpecialKey.NSF12FunctionKey }, + { Key.F13, OsxUnicodeSpecialKey.NSF13FunctionKey }, + { Key.F14, OsxUnicodeSpecialKey.NSF14FunctionKey }, + { Key.F15, OsxUnicodeSpecialKey.NSF15FunctionKey }, + { Key.F16, OsxUnicodeSpecialKey.NSF16FunctionKey }, + { Key.F17, OsxUnicodeSpecialKey.NSF17FunctionKey }, + { Key.F18, OsxUnicodeSpecialKey.NSF18FunctionKey }, + { Key.F19, OsxUnicodeSpecialKey.NSF19FunctionKey }, + { Key.F20, OsxUnicodeSpecialKey.NSF20FunctionKey }, + { Key.F21, OsxUnicodeSpecialKey.NSF21FunctionKey }, + { Key.F22, OsxUnicodeSpecialKey.NSF22FunctionKey }, + { Key.F23, OsxUnicodeSpecialKey.NSF23FunctionKey }, + { Key.F24, OsxUnicodeSpecialKey.NSF24FunctionKey }, + { Key.Insert, OsxUnicodeSpecialKey.NSInsertFunctionKey }, + { Key.Delete, OsxUnicodeSpecialKey.NSDeleteFunctionKey }, + { Key.Home, OsxUnicodeSpecialKey.NSHomeFunctionKey }, + //{ Key.Begin, OsxUnicodeSpecialKey.NSBeginFunctionKey }, + { Key.End, OsxUnicodeSpecialKey.NSEndFunctionKey }, + { Key.PageUp, OsxUnicodeSpecialKey.NSPageUpFunctionKey }, + { Key.PageDown, OsxUnicodeSpecialKey.NSPageDownFunctionKey }, + { Key.PrintScreen, OsxUnicodeSpecialKey.NSPrintScreenFunctionKey }, + { Key.Scroll, OsxUnicodeSpecialKey.NSScrollLockFunctionKey }, + //{ Key.SysReq, OsxUnicodeSpecialKey.NSSysReqFunctionKey }, + //{ Key.Break, OsxUnicodeSpecialKey.NSBreakFunctionKey }, + //{ Key.Reset, OsxUnicodeSpecialKey.NSResetFunctionKey }, + //{ Key.Stop, OsxUnicodeSpecialKey.NSStopFunctionKey }, + //{ Key.Menu, OsxUnicodeSpecialKey.NSMenuFunctionKey }, + //{ Key.UserFunction, OsxUnicodeSpecialKey.NSUserFunctionKey }, + //{ Key.SystemFunction, OsxUnicodeSpecialKey.NSSystemFunctionKey }, + { Key.Print, OsxUnicodeSpecialKey.NSPrintFunctionKey }, + //{ Key.ClearLine, OsxUnicodeSpecialKey.NSClearLineFunctionKey }, + //{ Key.ClearDisplay, OsxUnicodeSpecialKey.NSClearDisplayFunctionKey }, + }; + + public static string ConvertOSXSpecialKeyCodes(Key key) + { + if (s_osxKeys.ContainsKey(key)) + { + return ((char)s_osxKeys[key]).ToString(); + } + else + { + return key.ToString().ToLower(); + } + } + } +} diff --git a/src/Avalonia.Native/PlatformThreadingInterface.cs b/src/Avalonia.Native/PlatformThreadingInterface.cs index 353124a9d1..5a81f6a3bf 100644 --- a/src/Avalonia.Native/PlatformThreadingInterface.cs +++ b/src/Avalonia.Native/PlatformThreadingInterface.cs @@ -1,7 +1,5 @@ -// 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 System; +using System.Runtime.ExceptionServices; using System.Threading; using Avalonia.Native.Interop; using Avalonia.Platform; @@ -43,6 +41,8 @@ namespace Avalonia.Native } readonly IAvnPlatformThreadingInterface _native; + private ExceptionDispatchInfo _exceptionDispatchInfo; + private CancellationTokenSource _exceptionCancellationSource; public PlatformThreadingInterface(IAvnPlatformThreadingInterface native) { @@ -57,32 +57,49 @@ namespace Avalonia.Native public void RunLoop(CancellationToken cancellationToken) { - if (cancellationToken.CanBeCanceled == false) - _native.RunLoop(null); - else + _exceptionDispatchInfo?.Throw(); + var l = new object(); + _exceptionCancellationSource = new CancellationTokenSource(); + + var compositeCancellation = CancellationTokenSource + .CreateLinkedTokenSource(cancellationToken, _exceptionCancellationSource.Token).Token; + + var cancellation = _native.CreateLoopCancellation(); + compositeCancellation.Register(() => { - var l = new object(); - var cancellation = _native.CreateLoopCancellation(); - cancellationToken.Register(() => + lock (l) { - lock (l) - { - cancellation?.Cancel(); - } - }); - try - { - _native.RunLoop(cancellation); + cancellation?.Cancel(); } - finally + }); + + try + { + _native.RunLoop(cancellation); + } + finally + { + lock (l) { - lock(l) - { - cancellation?.Dispose(); - cancellation = null; - } + cancellation?.Dispose(); + cancellation = null; } } + + if (_exceptionDispatchInfo != null) + { + _exceptionDispatchInfo.Throw(); + } + } + + public void DispatchException (ExceptionDispatchInfo exceptionInfo) + { + _exceptionDispatchInfo = exceptionInfo; + } + + public void TerminateNativeApp() + { + _exceptionCancellationSource?.Cancel(); } public void Signal(DispatcherPriority priority) diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index d7fa1052ff..c41be1723b 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -1,26 +1,28 @@ -// 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 System; +using System; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Native.Interop; using Avalonia.Platform; namespace Avalonia.Native { - public class PopupImpl : WindowBaseImpl, IPopupImpl + class PopupImpl : WindowBaseImpl, IPopupImpl { private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; + private readonly GlPlatformFeature _glFeature; + public PopupImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, - IWindowBaseImpl parent) : base(opts) + GlPlatformFeature glFeature, + IWindowBaseImpl parent) : base(opts, glFeature) { _factory = factory; _opts = opts; + _glFeature = glFeature; using (var e = new PopupEvents(this)) { - Init(factory.CreatePopup(e), factory.CreateScreens()); + var context = _opts.UseGpu ? glFeature?.DeferredContext : null; + Init(factory.CreatePopup(e, context?.Context), factory.CreateScreens(), context); } PopupPositioner = new ManagedPopupPositioner(new OsxManagedPopupPositionerPopupImplHelper(parent, MoveResize)); } @@ -41,6 +43,11 @@ namespace Avalonia.Native _parent = parent; } + public void GotInputWhenDisabled() + { + // NOP on Popup + } + bool IAvnWindowEvents.Closing() { return true; @@ -51,7 +58,12 @@ namespace Avalonia.Native } } - public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts, this); + public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts, _glFeature, this); + + public void SetWindowManagerAddShadowHint(bool enabled) + { + } + public IPopupPositioner PopupPositioner { get; } } } diff --git a/src/Avalonia.Native/PredicateCallback.cs b/src/Avalonia.Native/PredicateCallback.cs new file mode 100644 index 0000000000..1ed2ae36af --- /dev/null +++ b/src/Avalonia.Native/PredicateCallback.cs @@ -0,0 +1,20 @@ +using System; +using Avalonia.Native.Interop; + +namespace Avalonia.Native +{ + public class PredicateCallback : CallbackBase, IAvnPredicateCallback + { + private Func _predicate; + + public PredicateCallback(Func predicate) + { + _predicate = predicate; + } + + bool IAvnPredicateCallback.Evaluate() + { + return _predicate(); + } + } +} diff --git a/src/Avalonia.Native/ScreenImpl.cs b/src/Avalonia.Native/ScreenImpl.cs index 3ec3be20d3..0d593bc2f6 100644 --- a/src/Avalonia.Native/ScreenImpl.cs +++ b/src/Avalonia.Native/ScreenImpl.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using Avalonia.Native.Interop; diff --git a/src/Avalonia.Native/SystemDialogs.cs b/src/Avalonia.Native/SystemDialogs.cs index 275c93dc45..98e3e383dc 100644 --- a/src/Avalonia.Native/SystemDialogs.cs +++ b/src/Avalonia.Native/SystemDialogs.cs @@ -1,14 +1,10 @@ -// 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 System; +using System; using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Native.Interop; -using Avalonia.Platform; namespace Avalonia.Native { @@ -21,25 +17,27 @@ namespace Avalonia.Native _native = native; } - public Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + public Task ShowFileDialogAsync(FileDialog dialog, Window parent) { var events = new SystemDialogEvents(); + var nativeParent = GetNativeWindow(parent); + if (dialog is OpenFileDialog ofd) { - _native.OpenFileDialog((parent as WindowImpl)?.Native, + _native.OpenFileDialog(nativeParent, events, ofd.AllowMultiple, ofd.Title ?? "", - ofd.InitialDirectory ?? "", + ofd.Directory ?? "", ofd.InitialFileName ?? "", string.Join(";", dialog.Filters.SelectMany(f => f.Extensions))); } else { - _native.SaveFileDialog((parent as WindowImpl)?.Native, + _native.SaveFileDialog(nativeParent, events, dialog.Title ?? "", - dialog.InitialDirectory ?? "", + dialog.Directory ?? "", dialog.InitialFileName ?? "", string.Join(";", dialog.Filters.SelectMany(f => f.Extensions))); } @@ -47,14 +45,21 @@ namespace Avalonia.Native return events.Task.ContinueWith(t => { events.Dispose(); return t.Result; }); } - public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + public Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) { var events = new SystemDialogEvents(); - _native.SelectFolderDialog((parent as WindowImpl)?.Native, events, dialog.Title ?? "", dialog.InitialDirectory ?? ""); + var nativeParent = GetNativeWindow(parent); + + _native.SelectFolderDialog(nativeParent, events, dialog.Title ?? "", dialog.Directory ?? ""); return events.Task.ContinueWith(t => { events.Dispose(); return t.Result.FirstOrDefault(); }); } + + private IAvnWindow GetNativeWindow(Window window) + { + return (window?.PlatformImpl as WindowImpl)?.Native; + } } public class SystemDialogEvents : CallbackBase, IAvnSystemDialogEvents diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index a7828bedaf..e91445000a 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -1,10 +1,8 @@ -// 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 System; +using System; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Native.Interop; +using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Platform.Interop; @@ -14,14 +12,18 @@ namespace Avalonia.Native { private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; + private readonly GlPlatformFeature _glFeature; IAvnWindow _native; - public WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts) : base(opts) + internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, + GlPlatformFeature glFeature) : base(opts, glFeature) { _factory = factory; _opts = opts; + _glFeature = glFeature; using (var e = new WindowEvents(this)) { - Init(_native = factory.CreateWindow(e), factory.CreateScreens()); + var context = _opts.UseGpu ? glFeature?.DeferredContext : null; + Init(_native = factory.CreateWindow(e, context?.Context), factory.CreateScreens(), context); } NativeMenuExporter = new AvaloniaNativeMenuExporter(_native, factory); @@ -38,7 +40,7 @@ namespace Avalonia.Native bool IAvnWindowEvents.Closing() { - if(_parent.Closing != null) + if (_parent.Closing != null) { return _parent.Closing(); } @@ -50,26 +52,26 @@ namespace Avalonia.Native { _parent.WindowStateChanged?.Invoke((WindowState)state); } + + void IAvnWindowEvents.GotInputWhenDisabled() + { + _parent.GotInputWhenDisabled?.Invoke(); + } } public IAvnWindow Native => _native; - public void ShowDialog(IWindowImpl window) - { - _native.ShowDialog(((WindowImpl)window).Native); - } - public void CanResize(bool value) { _native.CanResize = value; } - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(Controls.SystemDecorations enabled) { - _native.HasDecorations = enabled; + _native.Decorations = (Interop.SystemDecorations)enabled; } - public void SetTitleBarColor (Avalonia.Media.Color color) + public void SetTitleBarColor(Avalonia.Media.Color color) { _native.SetTitleBarColor(new AvnColor { Alpha = color.A, Red = color.R, Green = color.G, Blue = color.B }); } @@ -113,6 +115,18 @@ namespace Avalonia.Native public void Move(PixelPoint point) => Position = point; public override IPopupImpl CreatePopup() => - _opts.OverlayPopups ? null : new PopupImpl(_factory, _opts, this); + _opts.OverlayPopups ? null : new PopupImpl(_factory, _opts, _glFeature, this); + + public Action GotInputWhenDisabled { get; set; } + + public void SetParent(IWindowImpl parent) + { + _native.SetParent(((WindowImpl)parent).Native); + } + + public void SetEnabled(bool enable) + { + _native.SetEnabled(enable); + } } } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index d8ff370c45..930b5800ba 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -1,10 +1,9 @@ -// 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 System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Input; using Avalonia.Input.Raw; @@ -16,39 +15,74 @@ using Avalonia.Threading; namespace Avalonia.Native { + public class MacOSTopLevelWindowHandle : IPlatformHandle, IMacOSTopLevelPlatformHandle + { + IAvnWindowBase _native; + + public MacOSTopLevelWindowHandle(IAvnWindowBase native) + { + _native = native; + } + + public IntPtr Handle => NSWindow; + + public string HandleDescriptor => "NSWindow"; + + public IntPtr NSView => _native.ObtainNSViewHandle(); + + public IntPtr NSWindow => _native.ObtainNSWindowHandle(); + + public IntPtr GetNSViewRetained() + { + return _native.ObtainNSViewHandleRetained(); + } + + public IntPtr GetNSWindowRetained() + { + return _native.ObtainNSWindowHandleRetained(); + } + } + public abstract class WindowBaseImpl : IWindowBaseImpl, - IFramebufferPlatformSurface + IFramebufferPlatformSurface, ITopLevelImplWithNativeControlHost { IInputRoot _inputRoot; IAvnWindowBase _native; private object _syncRoot = new object(); private bool _deferredRendering = false; private bool _gpu = false; - private readonly IMouseDevice _mouse; + private readonly MouseDevice _mouse; private readonly IKeyboardDevice _keyboard; private readonly IStandardCursorFactory _cursorFactory; private Size _savedLogicalSize; private Size _lastRenderedLogicalSize; private double _savedScaling; private GlPlatformSurface _glSurface; + private NativeControlHostImpl _nativeControlHost; + private IGlContext _glContext; - public WindowBaseImpl(AvaloniaNativePlatformOptions opts) + internal WindowBaseImpl(AvaloniaNativePlatformOptions opts, GlPlatformFeature glFeature) { - _gpu = opts.UseGpu; + _gpu = opts.UseGpu && glFeature != null; _deferredRendering = opts.UseDeferredRendering; _keyboard = AvaloniaLocator.Current.GetService(); - _mouse = AvaloniaLocator.Current.GetService(); + _mouse = new MouseDevice(); _cursorFactory = AvaloniaLocator.Current.GetService(); } - protected void Init(IAvnWindowBase window, IAvnScreens screens) + protected void Init(IAvnWindowBase window, IAvnScreens screens, IGlContext glContext) { _native = window; - _glSurface = new GlPlatformSurface(window); + _glContext = glContext; + + Handle = new MacOSTopLevelWindowHandle(window); + if (_gpu) + _glSurface = new GlPlatformSurface(window, _glContext); Screen = new ScreenImpl(screens); _savedLogicalSize = ClientSize; _savedScaling = Scaling; + _nativeControlHost = new NativeControlHostImpl(_native.CreateNativeControlHost()); var monitor = Screen.AllScreens.OrderBy(x => x.PixelDensity) .FirstOrDefault(m => m.Bounds.Contains(Position)); @@ -70,57 +104,34 @@ namespace Avalonia.Native this }; + public INativeControlHostImpl NativeControlHost => _nativeControlHost; + public ILockedFramebuffer Lock() { - if(_deferredRendering) + var w = _savedLogicalSize.Width * _savedScaling; + var h = _savedLogicalSize.Height * _savedScaling; + var dpi = _savedScaling * 96; + return new DeferredFramebuffer(cb => { - var w = _savedLogicalSize.Width * _savedScaling; - var h = _savedLogicalSize.Height * _savedScaling; - var dpi = _savedScaling * 96; - return new DeferredFramebuffer(cb => + lock (_syncRoot) { - lock (_syncRoot) - { - if (_native == null) - return false; - cb(_native); - _lastRenderedLogicalSize = _savedLogicalSize; - return true; - } - }, (int)w, (int)h, new Vector(dpi, dpi)); - } - - return new FramebufferWrapper(_native.GetSoftwareFramebuffer()); + if (_native == null) + return false; + cb(_native); + _lastRenderedLogicalSize = _savedLogicalSize; + return true; + } + }, (int)w, (int)h, new Vector(dpi, dpi)); } + public Action LostFocus { get; set; } + public Action Paint { get; set; } public Action Resized { get; set; } public Action Closed { get; set; } - public IMouseDevice MouseDevice => AvaloniaNativePlatform.MouseDevice; + public IMouseDevice MouseDevice => _mouse; public abstract IPopupImpl CreatePopup(); - - class FramebufferWrapper : ILockedFramebuffer - { - public FramebufferWrapper(AvnFramebuffer fb) - { - Address = fb.Data; - Size = new PixelSize(fb.Width, fb.Height); - RowBytes = fb.Stride; - Dpi = new Vector(fb.Dpi.X, fb.Dpi.Y); - Format = (PixelFormat)fb.PixelFormat; - } - public IntPtr Address { get; set; } - public PixelSize Size { get; set; } - public int RowBytes {get;set;} - public Vector Dpi { get; set; } - public PixelFormat Format { get; } - public void Dispose() - { - // Do nothing - } - } - protected class WindowBaseEvents : CallbackBase, IAvnWindowBaseEvents { private readonly WindowBaseImpl _parent; @@ -142,6 +153,7 @@ namespace Avalonia.Native { n?.Dispose(); } + _parent._mouse.Dispose(); } void IAvnWindowBaseEvents.Activated() => _parent.Activated?.Invoke(); @@ -157,9 +169,12 @@ namespace Avalonia.Native void IAvnWindowBaseEvents.Resized(AvnSize size) { - var s = new Size(size.Width, size.Height); - _parent._savedLogicalSize = s; - _parent.Resized?.Invoke(s); + if (_parent._native != null) + { + var s = new Size(size.Width, size.Height); + _parent._savedLogicalSize = s; + _parent.Resized?.Invoke(s); + } } void IAvnWindowBaseEvents.PositionChanged(AvnPoint position) @@ -193,6 +208,35 @@ namespace Avalonia.Native { Dispatcher.UIThread.RunJobs(DispatcherPriority.Render); } + + void IAvnWindowBaseEvents.LostFocus() + { + _parent.LostFocus?.Invoke(); + } + + public AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position, + AvnInputModifiers modifiers, + AvnDragDropEffects effects, + IAvnClipboard clipboard, IntPtr dataObjectHandle) + { + var device = AvaloniaLocator.Current.GetService(); + + IDataObject dataObject = null; + if (dataObjectHandle != IntPtr.Zero) + dataObject = GCHandle.FromIntPtr(dataObjectHandle).Target as IDataObject; + + using(var clipboardDataObject = new ClipboardDataObject(clipboard)) + { + if (dataObject == null) + dataObject = clipboardDataObject; + + var args = new RawDragEvent(device, (RawDragEventType)type, + _parent._inputRoot, position.ToAvaloniaPoint(), dataObject, (DragDropEffects)effects, + (RawInputModifiers)modifiers); + _parent.Input(args); + return (AvnDragDropEffects)args.Effects; + } + } } public void Activate() @@ -204,7 +248,7 @@ namespace Avalonia.Native { Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); - var args = new RawTextInputEventArgs(_keyboard, timeStamp, text); + var args = new RawTextInputEventArgs(_keyboard, timeStamp, _inputRoot, text); Input?.Invoke(args); @@ -215,7 +259,7 @@ namespace Avalonia.Native { Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); - var args = new RawKeyEventArgs(_keyboard, timeStamp, (RawKeyEventType)type, (Key)key, (RawInputModifiers)modifiers); + var args = new RawKeyEventArgs(_keyboard, timeStamp, _inputRoot, (RawKeyEventType)type, (Key)key, (RawInputModifiers)modifiers); Input?.Invoke(args); @@ -246,9 +290,7 @@ namespace Avalonia.Native public IRenderer CreateRenderer(IRenderRoot root) { if (_deferredRendering) - return new DeferredRenderer(root, AvaloniaLocator.Current.GetService(), - rendererLock: - _gpu ? new AvaloniaNativeDeferredRendererLock(_native) : null); + return new DeferredRenderer(root, AvaloniaLocator.Current.GetService()); return new ImmediateRenderer(root); } @@ -258,6 +300,9 @@ namespace Avalonia.Native _native?.Dispose(); _native = null; + _nativeControlHost?.Dispose(); + _nativeControlHost = null; + (Screen as ScreenImpl)?.Dispose(); } @@ -301,12 +346,12 @@ namespace Avalonia.Native _native.Hide(); } - public void BeginMoveDrag() + public void BeginMoveDrag(PointerPressedEventArgs e) { _native.BeginMoveDrag(); } - public Size MaxClientSize => Screen.AllScreens.Select(s => s.Bounds.Size.ToSize(s.PixelDensity)) + public Size MaxAutoSizeHint => Screen.AllScreens.Select(s => s.Bounds.Size.ToSize(s.PixelDensity)) .OrderByDescending(x => x.Width + x.Height).FirstOrDefault(); public void SetTopmost(bool value) @@ -314,13 +359,18 @@ namespace Avalonia.Native _native.SetTopMost(value); } - public double Scaling => _native.GetScaling(); + public double Scaling => _native?.GetScaling() ?? 1; public Action Deactivated { get; set; } public Action Activated { get; set; } public void SetCursor(IPlatformHandle cursor) { + if (_native == null) + { + return; + } + var newCursor = cursor as AvaloniaNativeCursor; newCursor = newCursor ?? (_cursorFactory.GetCursor(StandardCursorType.Arrow) as AvaloniaNativeCursor); _native.Cursor = newCursor.Cursor; @@ -334,6 +384,8 @@ namespace Avalonia.Native Action ITopLevelImpl.ScalingChanged { get; set; } + public Action TransparencyLevelChanged { get; set; } + public IScreenImpl Screen { get; private set; } // TODO @@ -343,11 +395,40 @@ namespace Avalonia.Native _native.SetMinMaxSize(minSize.ToAvnSize(), maxSize.ToAvnSize()); } - public void BeginResizeDrag(WindowEdge edge) + public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) { } - public IPlatformHandle Handle => new PlatformHandle(IntPtr.Zero, "NOT SUPPORTED"); + internal void BeginDraggingSession(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard clipboard, + IAvnDndResultCallback callback, IntPtr sourceHandle) + { + _native.BeginDragAndDropOperation(effects, point, clipboard, callback, sourceHandle); + } + + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) + { + if (TransparencyLevel != transparencyLevel) + { + if (transparencyLevel >= WindowTransparencyLevel.Blur) + { + transparencyLevel = WindowTransparencyLevel.AcrylicBlur; + } + + if(transparencyLevel == WindowTransparencyLevel.None) + { + transparencyLevel = WindowTransparencyLevel.Transparent; + } + + TransparencyLevel = transparencyLevel; + + _native.SetBlurEnabled(TransparencyLevel >= WindowTransparencyLevel.Blur); + TransparencyLevelChanged?.Invoke(TransparencyLevel); + } + } + + public WindowTransparencyLevel TransparencyLevel { get; private set; } = WindowTransparencyLevel.Transparent; + + public IPlatformHandle Handle { get; private set; } } } diff --git a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj index 310d0c8dba..72132eed93 100644 --- a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj +++ b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj @@ -2,10 +2,12 @@ netstandard2.0 + true + diff --git a/src/Avalonia.OpenGL/EglContext.cs b/src/Avalonia.OpenGL/EglContext.cs index a39000f198..871665e857 100644 --- a/src/Avalonia.OpenGL/EglContext.cs +++ b/src/Avalonia.OpenGL/EglContext.cs @@ -1,6 +1,7 @@ using System; using System.Reactive.Disposables; using System.Threading; +using static Avalonia.OpenGL.EglConsts; namespace Avalonia.OpenGL { @@ -10,17 +11,27 @@ namespace Avalonia.OpenGL private readonly EglInterface _egl; private readonly object _lock = new object(); - public EglContext(EglDisplay display, EglInterface egl, IntPtr ctx, EglSurface offscreenSurface) + public EglContext(EglDisplay display, EglInterface egl, IntPtr ctx, EglSurface offscreenSurface, + GlVersion version, int sampleCount, int stencilSize) { _disp = display; _egl = egl; Context = ctx; OffscreenSurface = offscreenSurface; + Version = version; + SampleCount = sampleCount; + StencilSize = stencilSize; + using (MakeCurrent()) + GlInterface = GlInterface.FromNativeUtf8GetProcAddress(version, b => _egl.GetProcAddress(b)); } public IntPtr Context { get; } public EglSurface OffscreenSurface { get; } - public IGlDisplay Display => _disp; + public GlVersion Version { get; } + public GlInterface GlInterface { get; } + public int SampleCount { get; } + public int StencilSize { get; } + public EglDisplay Display => _disp; public IDisposable Lock() { @@ -28,17 +39,53 @@ namespace Avalonia.OpenGL return Disposable.Create(() => Monitor.Exit(_lock)); } - public void MakeCurrent() + class RestoreContext : IDisposable { + private readonly EglInterface _egl; + private readonly IntPtr _display; + private IntPtr _context, _read, _draw; + + public RestoreContext(EglInterface egl, IntPtr defDisplay) + { + _egl = egl; + _display = _egl.GetCurrentDisplay(); + if (_display == IntPtr.Zero) + _display = defDisplay; + _context = _egl.GetCurrentContext(); + _read = _egl.GetCurrentSurface(EGL_READ); + _draw = _egl.GetCurrentSurface(EGL_DRAW); + } + + public void Dispose() + { + _egl.MakeCurrent(_display, _draw, _read, _context); + } + + } + + public IDisposable MakeCurrent() + { + var old = new RestoreContext(_egl, _disp.Handle); + _egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); if (!_egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, Context)) throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl); + return old; } - public void MakeCurrent(EglSurface surface) + public IDisposable MakeCurrent(EglSurface surface) { + var old = new RestoreContext(_egl, _disp.Handle); var surf = surface ?? OffscreenSurface; + _egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); if (!_egl.MakeCurrent(_disp.Handle, surf.DangerousGetHandle(), surf.DangerousGetHandle(), Context)) throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl); + return old; + } + + public void Dispose() + { + _egl.DestroyContext(_disp.Handle, Context); + OffscreenSurface?.Dispose(); } } } diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index 66418c0e15..0436f6ac52 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -6,7 +6,7 @@ using static Avalonia.OpenGL.EglConsts; namespace Avalonia.OpenGL { - public class EglDisplay : IGlDisplay + public class EglDisplay { private readonly EglInterface _egl; private readonly IntPtr _display; @@ -16,6 +16,9 @@ namespace Avalonia.OpenGL public IntPtr Handle => _display; private AngleOptions.PlatformApi? _angleApi; + private int _sampleCount; + private int _stencilSize; + private GlVersion _version; public EglDisplay(EglInterface egl) : this(egl, -1, IntPtr.Zero, null) { @@ -76,13 +79,6 @@ namespace Avalonia.OpenGL foreach (var cfg in new[] { - new - { - Attributes = new[] {EGL_NONE}, - Api = EGL_OPENGL_API, - RenderableTypeBit = EGL_OPENGL_BIT, - Type = GlDisplayType.OpenGL2 - }, new { Attributes = new[] @@ -92,7 +88,7 @@ namespace Avalonia.OpenGL }, Api = EGL_OPENGL_ES_API, RenderableTypeBit = EGL_OPENGL_ES2_BIT, - Type = GlDisplayType.OpenGLES2 + Version = new GlVersion(GlProfileType.OpenGLES, 2, 0) } }) { @@ -120,14 +116,16 @@ namespace Avalonia.OpenGL continue; _contextAttributes = cfg.Attributes; _surfaceType = surfaceType; - Type = cfg.Type; + _version = cfg.Version; + _egl.GetConfigAttrib(_display, _config, EGL_SAMPLES, out _sampleCount); + _egl.GetConfigAttrib(_display, _config, EGL_STENCIL_SIZE, out _stencilSize); + goto Found; } - } + } + Found: if (_contextAttributes == null) throw new OpenGlException("No suitable EGL config was found"); - - GlInterface = GlInterface.FromNativeUtf8GetProcAddress(b => _egl.GetProcAddress(b)); } public EglDisplay() : this(new EglInterface()) @@ -135,8 +133,6 @@ namespace Avalonia.OpenGL } - public GlDisplayType Type { get; } - public GlInterface GlInterface { get; } public EglInterface EglInterface => _egl; public EglContext CreateContext(IGlContext share) { @@ -154,8 +150,8 @@ namespace Avalonia.OpenGL }); if (surf == IntPtr.Zero) throw OpenGlException.GetFormattedException("eglCreatePBufferSurface", _egl); - var rv = new EglContext(this, _egl, ctx, new EglSurface(this, _egl, surf)); - rv.MakeCurrent(null); + var rv = new EglContext(this, _egl, ctx, new EglSurface(this, _egl, surf), + _version, _sampleCount, _stencilSize); return rv; } @@ -164,17 +160,11 @@ namespace Avalonia.OpenGL var ctx = _egl.CreateContext(_display, _config, share?.Context ?? IntPtr.Zero, _contextAttributes); if (ctx == IntPtr.Zero) throw OpenGlException.GetFormattedException("eglCreateContext", _egl); - var rv = new EglContext(this, _egl, ctx, offscreenSurface); + var rv = new EglContext(this, _egl, ctx, offscreenSurface, _version, _sampleCount, _stencilSize); rv.MakeCurrent(null); return rv; } - public void ClearContext() - { - if (!_egl.MakeCurrent(_display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero)) - throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl); - } - public EglSurface CreateWindowSurface(IntPtr window) { var s = _egl.CreateWindowSurface(_display, _config, window, new[] {EGL_NONE, EGL_NONE}); @@ -182,23 +172,5 @@ namespace Avalonia.OpenGL throw OpenGlException.GetFormattedException("eglCreateWindowSurface", _egl); return new EglSurface(this, _egl, s); } - - public int SampleCount - { - get - { - _egl.GetConfigAttrib(_display, _config, EGL_SAMPLES, out var rv); - return rv; - } - } - - public int StencilSize - { - get - { - _egl.GetConfigAttrib(_display, _config, EGL_STENCIL_SIZE, out var rv); - return rv; - } - } } } diff --git a/src/Avalonia.OpenGL/EglGlPlatformFeature.cs b/src/Avalonia.OpenGL/EglGlPlatformFeature.cs index 5f5064fba5..f59c6b7751 100644 --- a/src/Avalonia.OpenGL/EglGlPlatformFeature.cs +++ b/src/Avalonia.OpenGL/EglGlPlatformFeature.cs @@ -5,9 +5,14 @@ namespace Avalonia.OpenGL { public class EglGlPlatformFeature : IWindowingPlatformGlFeature { - public IGlDisplay Display { get; set; } - public IGlContext ImmediateContext { get; set; } - public EglContext DeferredContext { get; set; } + private EglDisplay _display; + public EglDisplay Display => _display; + public IGlContext CreateContext() + { + return _display.CreateContext(DeferredContext); + } + public EglContext DeferredContext { get; private set; } + public IGlContext MainContext => DeferredContext; public static void TryInitialize() { @@ -21,17 +26,15 @@ namespace Avalonia.OpenGL try { var disp = new EglDisplay(); - var ctx = disp.CreateContext(null); return new EglGlPlatformFeature { - Display = disp, - ImmediateContext = ctx, - DeferredContext = (EglContext)disp.CreateContext(ctx) + _display = disp, + DeferredContext = disp.CreateContext(null) }; } catch(Exception e) { - Logger.TryGet(LogEventLevel.Error)?.Log("OpenGL", null, "Unable to initialize EGL-based rendering: {0}", e); + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(null, "Unable to initialize EGL-based rendering: {0}", e); return null; } } diff --git a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs b/src/Avalonia.OpenGL/EglGlPlatformSurface.cs index d2e4543af3..3e4befe2c6 100644 --- a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs +++ b/src/Avalonia.OpenGL/EglGlPlatformSurface.cs @@ -16,9 +16,9 @@ namespace Avalonia.OpenGL private readonly EglContext _context; private readonly IEglWindowGlPlatformSurfaceInfo _info; - public EglGlPlatformSurface(EglDisplay display, EglContext context, IEglWindowGlPlatformSurfaceInfo info) + public EglGlPlatformSurface(EglContext context, IEglWindowGlPlatformSurfaceInfo info) { - _display = display; + _display = context.Display; _context = context; _info = info; } @@ -58,12 +58,12 @@ namespace Avalonia.OpenGL { if (IsCorrupted) throw new RenderTargetCorruptedException(); - _context.MakeCurrent(_glSurface); + var restoreContext = _context.MakeCurrent(_glSurface); _display.EglInterface.WaitClient(); _display.EglInterface.WaitGL(); - _display.EglInterface.WaitNative(); + _display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); - return new Session(_display, _context, _glSurface, _info, l); + return new Session(_display, _context, _glSurface, _info, l, restoreContext); } catch { @@ -79,34 +79,37 @@ namespace Avalonia.OpenGL private readonly IEglWindowGlPlatformSurfaceInfo _info; private readonly EglDisplay _display; private IDisposable _lock; - + private readonly IDisposable _restoreContext; + public Session(EglDisplay display, EglContext context, EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info, - IDisposable @lock) + IDisposable @lock, IDisposable restoreContext) { _context = context; _display = display; _glSurface = glSurface; _info = info; _lock = @lock; + _restoreContext = restoreContext; } public void Dispose() { - _context.Display.GlInterface.Flush(); + _context.GlInterface.Flush(); _display.EglInterface.WaitGL(); _glSurface.SwapBuffers(); _display.EglInterface.WaitClient(); _display.EglInterface.WaitGL(); - _display.EglInterface.WaitNative(); - _context.Display.ClearContext(); + _display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); + _restoreContext.Dispose(); _lock.Dispose(); } - public IGlDisplay Display => _context.Display; + public IGlContext Context => _context; public PixelSize Size => _info.Size; public double Scaling => _info.Scaling; + public bool IsYFlipped { get; } } } } diff --git a/src/Avalonia.OpenGL/EglInterface.cs b/src/Avalonia.OpenGL/EglInterface.cs index 47088972a4..c0665a1ea1 100644 --- a/src/Avalonia.OpenGL/EglInterface.cs +++ b/src/Avalonia.OpenGL/EglInterface.cs @@ -24,7 +24,7 @@ namespace Avalonia.OpenGL [DllImport("libegl.dll", CharSet = CharSet.Ansi)] static extern IntPtr eglGetProcAddress(string proc); - static Func Load() + static Func Load() { var os = AvaloniaLocator.Current.GetService().GetRuntimeInfo().OperatingSystem; if(os == OperatingSystemType.Linux || os == OperatingSystemType.Android) @@ -34,23 +34,17 @@ namespace Avalonia.OpenGL var disp = eglGetProcAddress("eglGetPlatformDisplayEXT"); if (disp == IntPtr.Zero) throw new OpenGlException("libegl.dll doesn't have eglGetPlatformDisplayEXT entry point"); - return (name, optional) => - { - var r = eglGetProcAddress(name); - if (r == IntPtr.Zero && !optional) - throw new OpenGlException($"Entry point {r} is not found"); - return r; - }; + return eglGetProcAddress; } throw new PlatformNotSupportedException(); } - static Func Load(string library) + static Func Load(string library) { var dyn = AvaloniaLocator.Current.GetService(); var lib = dyn.LoadLibrary(library); - return (s, o) => dyn.GetProcAddress(lib, s, o); + return (s) => dyn.GetProcAddress(lib, s, true); } // ReSharper disable UnassignedGetOnlyAutoProperty @@ -63,7 +57,8 @@ namespace Avalonia.OpenGL public EglGetDisplay GetDisplay { get; } public delegate IntPtr EglGetPlatformDisplayEXT(int platform, IntPtr nativeDisplay, int[] attrs); - [GlEntryPoint("eglGetPlatformDisplayEXT", true)] + [GlEntryPoint("eglGetPlatformDisplayEXT")] + [GlOptionalEntryPoint] public EglGetPlatformDisplayEXT GetPlatformDisplayEXT { get; } public delegate bool EglInitialize(IntPtr display, out int major, out int minor); @@ -87,6 +82,10 @@ namespace Avalonia.OpenGL IntPtr share, int[] attrs); [GlEntryPoint("eglCreateContext")] public EglCreateContext CreateContext { get; } + + public delegate bool EglDestroyContext(IntPtr display, IntPtr context); + [GlEntryPoint("eglDestroyContext")] + public EglDestroyContext DestroyContext { get; } public delegate IntPtr EglCreatePBufferSurface(IntPtr display, IntPtr config, int[] attrs); [GlEntryPoint("eglCreatePbufferSurface")] @@ -96,6 +95,18 @@ namespace Avalonia.OpenGL [GlEntryPoint("eglMakeCurrent")] public EglMakeCurrent MakeCurrent { get; } + public delegate IntPtr EglGetCurrentContext(); + [GlEntryPoint("eglGetCurrentContext")] + public EglGetCurrentContext GetCurrentContext { get; } + + public delegate IntPtr EglGetCurrentDisplay(); + [GlEntryPoint("eglGetCurrentDisplay")] + public EglGetCurrentContext GetCurrentDisplay { get; } + + public delegate IntPtr EglGetCurrentSurface(int readDraw); + [GlEntryPoint("eglGetCurrentSurface")] + public EglGetCurrentSurface GetCurrentSurface { get; } + public delegate void EglDisplaySurfaceVoidDelegate(IntPtr display, IntPtr surface); [GlEntryPoint("eglDestroySurface")] public EglDisplaySurfaceVoidDelegate DestroySurface { get; } @@ -120,9 +131,9 @@ namespace Avalonia.OpenGL [GlEntryPoint("eglWaitClient")] public EglWaitGL WaitClient { get; } - public delegate bool EglWaitNative(); + public delegate bool EglWaitNative(int engine); [GlEntryPoint("eglWaitNative")] - public EglWaitGL WaitNative { get; } + public EglWaitNative WaitNative { get; } public delegate IntPtr EglQueryString(IntPtr display, int i); diff --git a/src/Avalonia.OpenGL/EglSurface.cs b/src/Avalonia.OpenGL/EglSurface.cs index 90bb1e36d0..5ac56a00e3 100644 --- a/src/Avalonia.OpenGL/EglSurface.cs +++ b/src/Avalonia.OpenGL/EglSurface.cs @@ -21,8 +21,6 @@ namespace Avalonia.OpenGL } public override bool IsInvalid => handle == IntPtr.Zero; - - public IGlDisplay Display => _display; public void SwapBuffers() => _egl.SwapBuffers(_display.Handle, handle); } -} \ No newline at end of file +} diff --git a/src/Avalonia.OpenGL/GlBasicInfoInterface.cs b/src/Avalonia.OpenGL/GlBasicInfoInterface.cs new file mode 100644 index 0000000000..a3383ac5ae --- /dev/null +++ b/src/Avalonia.OpenGL/GlBasicInfoInterface.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using Avalonia.Platform.Interop; + +namespace Avalonia.OpenGL +{ + public class GlBasicInfoInterface : GlBasicInfoInterface + { + public GlBasicInfoInterface(Func getProcAddress) : base(getProcAddress, null) + { + } + + public GlBasicInfoInterface(Func nativeGetProcAddress) : base(nativeGetProcAddress, null) + { + } + + public delegate void GlGetIntegerv(int name, out int rv); + public delegate IntPtr GlGetString(int v); + public delegate IntPtr GlGetStringi(int v, int v1); + } + + public class GlBasicInfoInterface : GlInterfaceBase + { + public GlBasicInfoInterface(Func getProcAddress, TContextInfo context) : base(getProcAddress, context) + { + } + + public GlBasicInfoInterface(Func nativeGetProcAddress, TContextInfo context) : base(nativeGetProcAddress, context) + { + } + + [GlEntryPoint("glGetIntegerv")] + public GlBasicInfoInterface.GlGetIntegerv GetIntegerv { get; } + + + [GlEntryPoint("glGetString")] + public GlBasicInfoInterface.GlGetString GetStringNative { get; } + + [GlEntryPoint("glGetStringi")] + public GlBasicInfoInterface.GlGetStringi GetStringiNative { get; } + + public string GetString(int v) + { + var ptr = GetStringNative(v); + if (ptr != IntPtr.Zero) + return Marshal.PtrToStringAnsi(ptr); + return null; + } + + public string GetString(int v, int index) + { + var ptr = GetStringiNative(v, index); + if (ptr != IntPtr.Zero) + return Marshal.PtrToStringAnsi(ptr); + return null; + } + + public List GetExtensions() + { + var sp = GetString(GlConsts.GL_EXTENSIONS); + if (sp != null) + return sp.Split(' ').ToList(); + GetIntegerv(GlConsts.GL_NUM_EXTENSIONS, out int count); + var rv = new List(count); + for (var c = 0; c < count; c++) + rv.Add(GetString(GlConsts.GL_EXTENSIONS, c)); + return rv; + } + } +} diff --git a/src/Avalonia.OpenGL/GlConsts.cs b/src/Avalonia.OpenGL/GlConsts.cs index 275f351e3e..2a6aa0d14d 100644 --- a/src/Avalonia.OpenGL/GlConsts.cs +++ b/src/Avalonia.OpenGL/GlConsts.cs @@ -786,7 +786,4679 @@ namespace Avalonia.OpenGL // glext.h - + public const int GL_BLEND_DST_RGB = 0x80C8; + public const int GL_BLEND_SRC_RGB = 0x80C9; + public const int GL_BLEND_DST_ALPHA = 0x80CA; + public const int GL_BLEND_SRC_ALPHA = 0x80CB; + public const int GL_POINT_FADE_THRESHOLD_SIZE = 0x8128; + public const int GL_DEPTH_COMPONENT16 = 0x81A5; + public const int GL_DEPTH_COMPONENT24 = 0x81A6; + public const int GL_DEPTH_COMPONENT32 = 0x81A7; + public const int GL_MIRRORED_REPEAT = 0x8370; + public const int GL_MAX_TEXTURE_LOD_BIAS = 0x84FD; + public const int GL_TEXTURE_LOD_BIAS = 0x8501; + public const int GL_INCR_WRAP = 0x8507; + public const int GL_DECR_WRAP = 0x8508; + public const int GL_TEXTURE_DEPTH_SIZE = 0x884A; + public const int GL_TEXTURE_COMPARE_MODE = 0x884C; + public const int GL_TEXTURE_COMPARE_FUNC = 0x884D; + public const int GL_POINT_SIZE_MIN = 0x8126; + public const int GL_POINT_SIZE_MAX = 0x8127; + public const int GL_POINT_DISTANCE_ATTENUATION = 0x8129; + public const int GL_GENERATE_MIPMAP = 0x8191; + public const int GL_GENERATE_MIPMAP_HINT = 0x8192; + public const int GL_FOG_COORDINATE_SOURCE = 0x8450; + public const int GL_FOG_COORDINATE = 0x8451; + public const int GL_FRAGMENT_DEPTH = 0x8452; + public const int GL_CURRENT_FOG_COORDINATE = 0x8453; + public const int GL_FOG_COORDINATE_ARRAY_TYPE = 0x8454; + public const int GL_FOG_COORDINATE_ARRAY_STRIDE = 0x8455; + public const int GL_FOG_COORDINATE_ARRAY_POINTER = 0x8456; + public const int GL_FOG_COORDINATE_ARRAY = 0x8457; + public const int GL_COLOR_SUM = 0x8458; + public const int GL_CURRENT_SECONDARY_COLOR = 0x8459; + public const int GL_SECONDARY_COLOR_ARRAY_SIZE = 0x845A; + public const int GL_SECONDARY_COLOR_ARRAY_TYPE = 0x845B; + public const int GL_SECONDARY_COLOR_ARRAY_STRIDE = 0x845C; + public const int GL_SECONDARY_COLOR_ARRAY_POINTER = 0x845D; + public const int GL_SECONDARY_COLOR_ARRAY = 0x845E; + public const int GL_TEXTURE_FILTER_CONTROL = 0x8500; + public const int GL_DEPTH_TEXTURE_MODE = 0x884B; + public const int GL_COMPARE_R_TO_TEXTURE = 0x884E; + public const int GL_BUFFER_SIZE = 0x8764; + public const int GL_BUFFER_USAGE = 0x8765; + public const int GL_QUERY_COUNTER_BITS = 0x8864; + public const int GL_CURRENT_QUERY = 0x8865; + public const int GL_QUERY_RESULT = 0x8866; + public const int GL_QUERY_RESULT_AVAILABLE = 0x8867; + public const int GL_ARRAY_BUFFER = 0x8892; + public const int GL_ELEMENT_ARRAY_BUFFER = 0x8893; + public const int GL_ARRAY_BUFFER_BINDING = 0x8894; + public const int GL_ELEMENT_ARRAY_BUFFER_BINDING = 0x8895; + public const int GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING = 0x889F; + public const int GL_READ_ONLY = 0x88B8; + public const int GL_WRITE_ONLY = 0x88B9; + public const int GL_READ_WRITE = 0x88BA; + public const int GL_BUFFER_ACCESS = 0x88BB; + public const int GL_BUFFER_MAPPED = 0x88BC; + public const int GL_BUFFER_MAP_POINTER = 0x88BD; + public const int GL_STREAM_DRAW = 0x88E0; + public const int GL_STREAM_READ = 0x88E1; + public const int GL_STREAM_COPY = 0x88E2; + public const int GL_STATIC_DRAW = 0x88E4; + public const int GL_STATIC_READ = 0x88E5; + public const int GL_STATIC_COPY = 0x88E6; + public const int GL_DYNAMIC_DRAW = 0x88E8; + public const int GL_DYNAMIC_READ = 0x88E9; + public const int GL_DYNAMIC_COPY = 0x88EA; + public const int GL_SAMPLES_PASSED = 0x8914; + public const int GL_SRC1_ALPHA = 0x8589; + public const int GL_VERTEX_ARRAY_BUFFER_BINDING = 0x8896; + public const int GL_NORMAL_ARRAY_BUFFER_BINDING = 0x8897; + public const int GL_COLOR_ARRAY_BUFFER_BINDING = 0x8898; + public const int GL_INDEX_ARRAY_BUFFER_BINDING = 0x8899; + public const int GL_TEXTURE_COORD_ARRAY_BUFFER_BINDING = 0x889A; + public const int GL_EDGE_FLAG_ARRAY_BUFFER_BINDING = 0x889B; + public const int GL_SECONDARY_COLOR_ARRAY_BUFFER_BINDING = 0x889C; + public const int GL_FOG_COORDINATE_ARRAY_BUFFER_BINDING = 0x889D; + public const int GL_WEIGHT_ARRAY_BUFFER_BINDING = 0x889E; + public const int GL_FOG_COORD_SRC = 0x8450; + public const int GL_FOG_COORD = 0x8451; + public const int GL_CURRENT_FOG_COORD = 0x8453; + public const int GL_FOG_COORD_ARRAY_TYPE = 0x8454; + public const int GL_FOG_COORD_ARRAY_STRIDE = 0x8455; + public const int GL_FOG_COORD_ARRAY_POINTER = 0x8456; + public const int GL_FOG_COORD_ARRAY = 0x8457; + public const int GL_FOG_COORD_ARRAY_BUFFER_BINDING = 0x889D; + public const int GL_SRC0_RGB = 0x8580; + public const int GL_SRC1_RGB = 0x8581; + public const int GL_SRC2_RGB = 0x8582; + public const int GL_SRC0_ALPHA = 0x8588; + public const int GL_SRC2_ALPHA = 0x858A; + public const int GL_BLEND_EQUATION_RGB = 0x8009; + public const int GL_VERTEX_ATTRIB_ARRAY_ENABLED = 0x8622; + public const int GL_VERTEX_ATTRIB_ARRAY_SIZE = 0x8623; + public const int GL_VERTEX_ATTRIB_ARRAY_STRIDE = 0x8624; + public const int GL_VERTEX_ATTRIB_ARRAY_TYPE = 0x8625; + public const int GL_CURRENT_VERTEX_ATTRIB = 0x8626; + public const int GL_VERTEX_PROGRAM_POINT_SIZE = 0x8642; + public const int GL_VERTEX_ATTRIB_ARRAY_POINTER = 0x8645; + public const int GL_STENCIL_BACK_FUNC = 0x8800; + public const int GL_STENCIL_BACK_FAIL = 0x8801; + public const int GL_STENCIL_BACK_PASS_DEPTH_FAIL = 0x8802; + public const int GL_STENCIL_BACK_PASS_DEPTH_PASS = 0x8803; + public const int GL_MAX_DRAW_BUFFERS = 0x8824; + public const int GL_DRAW_BUFFER0 = 0x8825; + public const int GL_DRAW_BUFFER1 = 0x8826; + public const int GL_DRAW_BUFFER2 = 0x8827; + public const int GL_DRAW_BUFFER3 = 0x8828; + public const int GL_DRAW_BUFFER4 = 0x8829; + public const int GL_DRAW_BUFFER5 = 0x882A; + public const int GL_DRAW_BUFFER6 = 0x882B; + public const int GL_DRAW_BUFFER7 = 0x882C; + public const int GL_DRAW_BUFFER8 = 0x882D; + public const int GL_DRAW_BUFFER9 = 0x882E; + public const int GL_DRAW_BUFFER10 = 0x882F; + public const int GL_DRAW_BUFFER11 = 0x8830; + public const int GL_DRAW_BUFFER12 = 0x8831; + public const int GL_DRAW_BUFFER13 = 0x8832; + public const int GL_DRAW_BUFFER14 = 0x8833; + public const int GL_DRAW_BUFFER15 = 0x8834; + public const int GL_BLEND_EQUATION_ALPHA = 0x883D; + public const int GL_MAX_VERTEX_ATTRIBS = 0x8869; + public const int GL_VERTEX_ATTRIB_ARRAY_NORMALIZED = 0x886A; + public const int GL_MAX_TEXTURE_IMAGE_UNITS = 0x8872; + public const int GL_FRAGMENT_SHADER = 0x8B30; + public const int GL_VERTEX_SHADER = 0x8B31; + public const int GL_MAX_FRAGMENT_UNIFORM_COMPONENTS = 0x8B49; + public const int GL_MAX_VERTEX_UNIFORM_COMPONENTS = 0x8B4A; + public const int GL_MAX_VARYING_FLOATS = 0x8B4B; + public const int GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS = 0x8B4C; + public const int GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS = 0x8B4D; + public const int GL_SHADER_TYPE = 0x8B4F; + public const int GL_FLOAT_VEC2 = 0x8B50; + public const int GL_FLOAT_VEC3 = 0x8B51; + public const int GL_FLOAT_VEC4 = 0x8B52; + public const int GL_INT_VEC2 = 0x8B53; + public const int GL_INT_VEC3 = 0x8B54; + public const int GL_INT_VEC4 = 0x8B55; + public const int GL_BOOL = 0x8B56; + public const int GL_BOOL_VEC2 = 0x8B57; + public const int GL_BOOL_VEC3 = 0x8B58; + public const int GL_BOOL_VEC4 = 0x8B59; + public const int GL_FLOAT_MAT2 = 0x8B5A; + public const int GL_FLOAT_MAT3 = 0x8B5B; + public const int GL_FLOAT_MAT4 = 0x8B5C; + public const int GL_SAMPLER_1D = 0x8B5D; + public const int GL_SAMPLER_2D = 0x8B5E; + public const int GL_SAMPLER_3D = 0x8B5F; + public const int GL_SAMPLER_CUBE = 0x8B60; + public const int GL_SAMPLER_1D_SHADOW = 0x8B61; + public const int GL_SAMPLER_2D_SHADOW = 0x8B62; + public const int GL_DELETE_STATUS = 0x8B80; + public const int GL_COMPILE_STATUS = 0x8B81; + public const int GL_LINK_STATUS = 0x8B82; + public const int GL_VALIDATE_STATUS = 0x8B83; + public const int GL_INFO_LOG_LENGTH = 0x8B84; + public const int GL_ATTACHED_SHADERS = 0x8B85; + public const int GL_ACTIVE_UNIFORMS = 0x8B86; + public const int GL_ACTIVE_UNIFORM_MAX_LENGTH = 0x8B87; + public const int GL_SHADER_SOURCE_LENGTH = 0x8B88; + public const int GL_ACTIVE_ATTRIBUTES = 0x8B89; + public const int GL_ACTIVE_ATTRIBUTE_MAX_LENGTH = 0x8B8A; + public const int GL_FRAGMENT_SHADER_DERIVATIVE_HINT = 0x8B8B; + public const int GL_SHADING_LANGUAGE_VERSION = 0x8B8C; + public const int GL_CURRENT_PROGRAM = 0x8B8D; + public const int GL_POINT_SPRITE_COORD_ORIGIN = 0x8CA0; + public const int GL_LOWER_LEFT = 0x8CA1; + public const int GL_UPPER_LEFT = 0x8CA2; + public const int GL_STENCIL_BACK_REF = 0x8CA3; + public const int GL_STENCIL_BACK_VALUE_MASK = 0x8CA4; + public const int GL_STENCIL_BACK_WRITEMASK = 0x8CA5; + public const int GL_VERTEX_PROGRAM_TWO_SIDE = 0x8643; + public const int GL_POINT_SPRITE = 0x8861; + public const int GL_COORD_REPLACE = 0x8862; + public const int GL_MAX_TEXTURE_COORDS = 0x8871; + public const int GL_VERSION_2_1 = 1; + public const int GL_PIXEL_PACK_BUFFER = 0x88EB; + public const int GL_PIXEL_UNPACK_BUFFER = 0x88EC; + public const int GL_PIXEL_PACK_BUFFER_BINDING = 0x88ED; + public const int GL_PIXEL_UNPACK_BUFFER_BINDING = 0x88EF; + public const int GL_FLOAT_MAT2x3 = 0x8B65; + public const int GL_FLOAT_MAT2x4 = 0x8B66; + public const int GL_FLOAT_MAT3x2 = 0x8B67; + public const int GL_FLOAT_MAT3x4 = 0x8B68; + public const int GL_FLOAT_MAT4x2 = 0x8B69; + public const int GL_FLOAT_MAT4x3 = 0x8B6A; + public const int GL_SRGB = 0x8C40; + public const int GL_SRGB8 = 0x8C41; + public const int GL_SRGB_ALPHA = 0x8C42; + public const int GL_SRGB8_ALPHA8 = 0x8C43; + public const int GL_COMPRESSED_SRGB = 0x8C48; + public const int GL_COMPRESSED_SRGB_ALPHA = 0x8C49; + public const int GL_CURRENT_RASTER_SECONDARY_COLOR = 0x845F; + public const int GL_SLUMINANCE_ALPHA = 0x8C44; + public const int GL_SLUMINANCE8_ALPHA8 = 0x8C45; + public const int GL_SLUMINANCE = 0x8C46; + public const int GL_SLUMINANCE8 = 0x8C47; + public const int GL_COMPRESSED_SLUMINANCE = 0x8C4A; + public const int GL_COMPRESSED_SLUMINANCE_ALPHA = 0x8C4B; + public const int GL_VERSION_3_0 = 1; + public const int GL_COMPARE_REF_TO_TEXTURE = 0x884E; + public const int GL_CLIP_DISTANCE0 = 0x3000; + public const int GL_CLIP_DISTANCE1 = 0x3001; + public const int GL_CLIP_DISTANCE2 = 0x3002; + public const int GL_CLIP_DISTANCE3 = 0x3003; + public const int GL_CLIP_DISTANCE4 = 0x3004; + public const int GL_CLIP_DISTANCE5 = 0x3005; + public const int GL_CLIP_DISTANCE6 = 0x3006; + public const int GL_CLIP_DISTANCE7 = 0x3007; + public const int GL_MAX_CLIP_DISTANCES = 0x0D32; + public const int GL_MAJOR_VERSION = 0x821B; + public const int GL_MINOR_VERSION = 0x821C; + public const int GL_NUM_EXTENSIONS = 0x821D; + public const int GL_CONTEXT_FLAGS = 0x821E; + public const int GL_COMPRESSED_RED = 0x8225; + public const int GL_COMPRESSED_RG = 0x8226; + public const int GL_CONTEXT_FLAG_FORWARD_COMPATIBLE_BIT = 0x00000001; + public const int GL_RGBA32F = 0x8814; + public const int GL_RGB32F = 0x8815; + public const int GL_RGBA16F = 0x881A; + public const int GL_RGB16F = 0x881B; + public const int GL_VERTEX_ATTRIB_ARRAY_INTEGER = 0x88FD; + public const int GL_MAX_ARRAY_TEXTURE_LAYERS = 0x88FF; + public const int GL_MIN_PROGRAM_TEXEL_OFFSET = 0x8904; + public const int GL_MAX_PROGRAM_TEXEL_OFFSET = 0x8905; + public const int GL_CLAMP_READ_COLOR = 0x891C; + public const int GL_FIXED_ONLY = 0x891D; + public const int GL_MAX_VARYING_COMPONENTS = 0x8B4B; + public const int GL_TEXTURE_1D_ARRAY = 0x8C18; + public const int GL_PROXY_TEXTURE_1D_ARRAY = 0x8C19; + public const int GL_TEXTURE_2D_ARRAY = 0x8C1A; + public const int GL_PROXY_TEXTURE_2D_ARRAY = 0x8C1B; + public const int GL_TEXTURE_BINDING_1D_ARRAY = 0x8C1C; + public const int GL_TEXTURE_BINDING_2D_ARRAY = 0x8C1D; + public const int GL_R11F_G11F_B10F = 0x8C3A; + public const int GL_UNSIGNED_INT_10F_11F_11F_REV = 0x8C3B; + public const int GL_RGB9_E5 = 0x8C3D; + public const int GL_UNSIGNED_INT_5_9_9_9_REV = 0x8C3E; + public const int GL_TEXTURE_SHARED_SIZE = 0x8C3F; + public const int GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH = 0x8C76; + public const int GL_TRANSFORM_FEEDBACK_BUFFER_MODE = 0x8C7F; + public const int GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS = 0x8C80; + public const int GL_TRANSFORM_FEEDBACK_VARYINGS = 0x8C83; + public const int GL_TRANSFORM_FEEDBACK_BUFFER_START = 0x8C84; + public const int GL_TRANSFORM_FEEDBACK_BUFFER_SIZE = 0x8C85; + public const int GL_PRIMITIVES_GENERATED = 0x8C87; + public const int GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN = 0x8C88; + public const int GL_RASTERIZER_DISCARD = 0x8C89; + public const int GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS = 0x8C8A; + public const int GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS = 0x8C8B; + public const int GL_INTERLEAVED_ATTRIBS = 0x8C8C; + public const int GL_SEPARATE_ATTRIBS = 0x8C8D; + public const int GL_TRANSFORM_FEEDBACK_BUFFER = 0x8C8E; + public const int GL_TRANSFORM_FEEDBACK_BUFFER_BINDING = 0x8C8F; + public const int GL_RGBA32UI = 0x8D70; + public const int GL_RGB32UI = 0x8D71; + public const int GL_RGBA16UI = 0x8D76; + public const int GL_RGB16UI = 0x8D77; + public const int GL_RGBA8UI = 0x8D7C; + public const int GL_RGB8UI = 0x8D7D; + public const int GL_RGBA32I = 0x8D82; + public const int GL_RGB32I = 0x8D83; + public const int GL_RGBA16I = 0x8D88; + public const int GL_RGB16I = 0x8D89; + public const int GL_RGBA8I = 0x8D8E; + public const int GL_RGB8I = 0x8D8F; + public const int GL_RED_INTEGER = 0x8D94; + public const int GL_GREEN_INTEGER = 0x8D95; + public const int GL_BLUE_INTEGER = 0x8D96; + public const int GL_RGB_INTEGER = 0x8D98; + public const int GL_RGBA_INTEGER = 0x8D99; + public const int GL_BGR_INTEGER = 0x8D9A; + public const int GL_BGRA_INTEGER = 0x8D9B; + public const int GL_SAMPLER_1D_ARRAY = 0x8DC0; + public const int GL_SAMPLER_2D_ARRAY = 0x8DC1; + public const int GL_SAMPLER_1D_ARRAY_SHADOW = 0x8DC3; + public const int GL_SAMPLER_2D_ARRAY_SHADOW = 0x8DC4; + public const int GL_SAMPLER_CUBE_SHADOW = 0x8DC5; + public const int GL_UNSIGNED_INT_VEC2 = 0x8DC6; + public const int GL_UNSIGNED_INT_VEC3 = 0x8DC7; + public const int GL_UNSIGNED_INT_VEC4 = 0x8DC8; + public const int GL_INT_SAMPLER_1D = 0x8DC9; + public const int GL_INT_SAMPLER_2D = 0x8DCA; + public const int GL_INT_SAMPLER_3D = 0x8DCB; + public const int GL_INT_SAMPLER_CUBE = 0x8DCC; + public const int GL_INT_SAMPLER_1D_ARRAY = 0x8DCE; + public const int GL_INT_SAMPLER_2D_ARRAY = 0x8DCF; + public const int GL_UNSIGNED_INT_SAMPLER_1D = 0x8DD1; + public const int GL_UNSIGNED_INT_SAMPLER_2D = 0x8DD2; + public const int GL_UNSIGNED_INT_SAMPLER_3D = 0x8DD3; + public const int GL_UNSIGNED_INT_SAMPLER_CUBE = 0x8DD4; + public const int GL_UNSIGNED_INT_SAMPLER_1D_ARRAY = 0x8DD6; + public const int GL_UNSIGNED_INT_SAMPLER_2D_ARRAY = 0x8DD7; + public const int GL_QUERY_WAIT = 0x8E13; + public const int GL_QUERY_NO_WAIT = 0x8E14; + public const int GL_QUERY_BY_REGION_WAIT = 0x8E15; + public const int GL_QUERY_BY_REGION_NO_WAIT = 0x8E16; + public const int GL_BUFFER_ACCESS_FLAGS = 0x911F; + public const int GL_BUFFER_MAP_LENGTH = 0x9120; + public const int GL_BUFFER_MAP_OFFSET = 0x9121; + public const int GL_DEPTH_COMPONENT32F = 0x8CAC; + public const int GL_DEPTH32F_STENCIL8 = 0x8CAD; + public const int GL_FLOAT_32_UNSIGNED_INT_24_8_REV = 0x8DAD; + public const int GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING = 0x8210; + public const int GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE = 0x8211; + public const int GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE = 0x8212; + public const int GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE = 0x8213; + public const int GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE = 0x8214; + public const int GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE = 0x8215; + public const int GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE = 0x8216; + public const int GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE = 0x8217; + public const int GL_FRAMEBUFFER_DEFAULT = 0x8218; + public const int GL_FRAMEBUFFER_UNDEFINED = 0x8219; + public const int GL_DEPTH_STENCIL_ATTACHMENT = 0x821A; + public const int GL_MAX_RENDERBUFFER_SIZE = 0x84E8; + public const int GL_DEPTH_STENCIL = 0x84F9; + public const int GL_UNSIGNED_INT_24_8 = 0x84FA; + public const int GL_DEPTH24_STENCIL8 = 0x88F0; + public const int GL_TEXTURE_STENCIL_SIZE = 0x88F1; + public const int GL_TEXTURE_RED_TYPE = 0x8C10; + public const int GL_TEXTURE_GREEN_TYPE = 0x8C11; + public const int GL_TEXTURE_BLUE_TYPE = 0x8C12; + public const int GL_TEXTURE_ALPHA_TYPE = 0x8C13; + public const int GL_TEXTURE_DEPTH_TYPE = 0x8C16; + public const int GL_UNSIGNED_NORMALIZED = 0x8C17; public const int GL_FRAMEBUFFER_BINDING = 0x8CA6; + public const int GL_DRAW_FRAMEBUFFER_BINDING = 0x8CA6; + public const int GL_RENDERBUFFER_BINDING = 0x8CA7; + public const int GL_READ_FRAMEBUFFER = 0x8CA8; + public const int GL_DRAW_FRAMEBUFFER = 0x8CA9; + public const int GL_READ_FRAMEBUFFER_BINDING = 0x8CAA; + public const int GL_RENDERBUFFER_SAMPLES = 0x8CAB; + public const int GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE = 0x8CD0; + public const int GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME = 0x8CD1; + public const int GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL = 0x8CD2; + public const int GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE = 0x8CD3; + public const int GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LAYER = 0x8CD4; + public const int GL_FRAMEBUFFER_COMPLETE = 0x8CD5; + public const int GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT = 0x8CD6; + public const int GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT = 0x8CD7; + public const int GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER = 0x8CDB; + public const int GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER = 0x8CDC; + public const int GL_FRAMEBUFFER_UNSUPPORTED = 0x8CDD; + public const int GL_MAX_COLOR_ATTACHMENTS = 0x8CDF; + public const int GL_COLOR_ATTACHMENT0 = 0x8CE0; + public const int GL_COLOR_ATTACHMENT1 = 0x8CE1; + public const int GL_COLOR_ATTACHMENT2 = 0x8CE2; + public const int GL_COLOR_ATTACHMENT3 = 0x8CE3; + public const int GL_COLOR_ATTACHMENT4 = 0x8CE4; + public const int GL_COLOR_ATTACHMENT5 = 0x8CE5; + public const int GL_COLOR_ATTACHMENT6 = 0x8CE6; + public const int GL_COLOR_ATTACHMENT7 = 0x8CE7; + public const int GL_COLOR_ATTACHMENT8 = 0x8CE8; + public const int GL_COLOR_ATTACHMENT9 = 0x8CE9; + public const int GL_COLOR_ATTACHMENT10 = 0x8CEA; + public const int GL_COLOR_ATTACHMENT11 = 0x8CEB; + public const int GL_COLOR_ATTACHMENT12 = 0x8CEC; + public const int GL_COLOR_ATTACHMENT13 = 0x8CED; + public const int GL_COLOR_ATTACHMENT14 = 0x8CEE; + public const int GL_COLOR_ATTACHMENT15 = 0x8CEF; + public const int GL_COLOR_ATTACHMENT16 = 0x8CF0; + public const int GL_COLOR_ATTACHMENT17 = 0x8CF1; + public const int GL_COLOR_ATTACHMENT18 = 0x8CF2; + public const int GL_COLOR_ATTACHMENT19 = 0x8CF3; + public const int GL_COLOR_ATTACHMENT20 = 0x8CF4; + public const int GL_COLOR_ATTACHMENT21 = 0x8CF5; + public const int GL_COLOR_ATTACHMENT22 = 0x8CF6; + public const int GL_COLOR_ATTACHMENT23 = 0x8CF7; + public const int GL_COLOR_ATTACHMENT24 = 0x8CF8; + public const int GL_COLOR_ATTACHMENT25 = 0x8CF9; + public const int GL_COLOR_ATTACHMENT26 = 0x8CFA; + public const int GL_COLOR_ATTACHMENT27 = 0x8CFB; + public const int GL_COLOR_ATTACHMENT28 = 0x8CFC; + public const int GL_COLOR_ATTACHMENT29 = 0x8CFD; + public const int GL_COLOR_ATTACHMENT30 = 0x8CFE; + public const int GL_COLOR_ATTACHMENT31 = 0x8CFF; + public const int GL_DEPTH_ATTACHMENT = 0x8D00; + public const int GL_STENCIL_ATTACHMENT = 0x8D20; + public const int GL_FRAMEBUFFER = 0x8D40; + public const int GL_RENDERBUFFER = 0x8D41; + public const int GL_RENDERBUFFER_WIDTH = 0x8D42; + public const int GL_RENDERBUFFER_HEIGHT = 0x8D43; + public const int GL_RENDERBUFFER_INTERNAL_FORMAT = 0x8D44; + public const int GL_STENCIL_INDEX1 = 0x8D46; + public const int GL_STENCIL_INDEX4 = 0x8D47; + public const int GL_STENCIL_INDEX8 = 0x8D48; + public const int GL_STENCIL_INDEX16 = 0x8D49; + public const int GL_RENDERBUFFER_RED_SIZE = 0x8D50; + public const int GL_RENDERBUFFER_GREEN_SIZE = 0x8D51; + public const int GL_RENDERBUFFER_BLUE_SIZE = 0x8D52; + public const int GL_RENDERBUFFER_ALPHA_SIZE = 0x8D53; + public const int GL_RENDERBUFFER_DEPTH_SIZE = 0x8D54; + public const int GL_RENDERBUFFER_STENCIL_SIZE = 0x8D55; + public const int GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE = 0x8D56; + public const int GL_MAX_SAMPLES = 0x8D57; + public const int GL_INDEX = 0x8222; + public const int GL_TEXTURE_LUMINANCE_TYPE = 0x8C14; + public const int GL_TEXTURE_INTENSITY_TYPE = 0x8C15; + public const int GL_FRAMEBUFFER_SRGB = 0x8DB9; + public const int GL_HALF_FLOAT = 0x140B; + public const int GL_MAP_READ_BIT = 0x0001; + public const int GL_MAP_WRITE_BIT = 0x0002; + public const int GL_MAP_INVALIDATE_RANGE_BIT = 0x0004; + public const int GL_MAP_INVALIDATE_BUFFER_BIT = 0x0008; + public const int GL_MAP_FLUSH_EXPLICIT_BIT = 0x0010; + public const int GL_MAP_UNSYNCHRONIZED_BIT = 0x0020; + public const int GL_COMPRESSED_RED_RGTC1 = 0x8DBB; + public const int GL_COMPRESSED_SIGNED_RED_RGTC1 = 0x8DBC; + public const int GL_COMPRESSED_RG_RGTC2 = 0x8DBD; + public const int GL_COMPRESSED_SIGNED_RG_RGTC2 = 0x8DBE; + public const int GL_RG = 0x8227; + public const int GL_RG_INTEGER = 0x8228; + public const int GL_R8 = 0x8229; + public const int GL_R16 = 0x822A; + public const int GL_RG8 = 0x822B; + public const int GL_RG16 = 0x822C; + public const int GL_R16F = 0x822D; + public const int GL_R32F = 0x822E; + public const int GL_RG16F = 0x822F; + public const int GL_RG32F = 0x8230; + public const int GL_R8I = 0x8231; + public const int GL_R8UI = 0x8232; + public const int GL_R16I = 0x8233; + public const int GL_R16UI = 0x8234; + public const int GL_R32I = 0x8235; + public const int GL_R32UI = 0x8236; + public const int GL_RG8I = 0x8237; + public const int GL_RG8UI = 0x8238; + public const int GL_RG16I = 0x8239; + public const int GL_RG16UI = 0x823A; + public const int GL_RG32I = 0x823B; + public const int GL_RG32UI = 0x823C; + public const int GL_VERTEX_ARRAY_BINDING = 0x85B5; + public const int GL_CLAMP_VERTEX_COLOR = 0x891A; + public const int GL_CLAMP_FRAGMENT_COLOR = 0x891B; + public const int GL_ALPHA_INTEGER = 0x8D97; + public const int GL_VERSION_3_1 = 1; + public const int GL_SAMPLER_2D_RECT = 0x8B63; + public const int GL_SAMPLER_2D_RECT_SHADOW = 0x8B64; + public const int GL_SAMPLER_BUFFER = 0x8DC2; + public const int GL_INT_SAMPLER_2D_RECT = 0x8DCD; + public const int GL_INT_SAMPLER_BUFFER = 0x8DD0; + public const int GL_UNSIGNED_INT_SAMPLER_2D_RECT = 0x8DD5; + public const int GL_UNSIGNED_INT_SAMPLER_BUFFER = 0x8DD8; + public const int GL_TEXTURE_BUFFER = 0x8C2A; + public const int GL_MAX_TEXTURE_BUFFER_SIZE = 0x8C2B; + public const int GL_TEXTURE_BINDING_BUFFER = 0x8C2C; + public const int GL_TEXTURE_BUFFER_DATA_STORE_BINDING = 0x8C2D; + public const int GL_TEXTURE_RECTANGLE = 0x84F5; + public const int GL_TEXTURE_BINDING_RECTANGLE = 0x84F6; + public const int GL_PROXY_TEXTURE_RECTANGLE = 0x84F7; + public const int GL_MAX_RECTANGLE_TEXTURE_SIZE = 0x84F8; + public const int GL_R8_SNORM = 0x8F94; + public const int GL_RG8_SNORM = 0x8F95; + public const int GL_RGB8_SNORM = 0x8F96; + public const int GL_RGBA8_SNORM = 0x8F97; + public const int GL_R16_SNORM = 0x8F98; + public const int GL_RG16_SNORM = 0x8F99; + public const int GL_RGB16_SNORM = 0x8F9A; + public const int GL_RGBA16_SNORM = 0x8F9B; + public const int GL_SIGNED_NORMALIZED = 0x8F9C; + public const int GL_PRIMITIVE_RESTART = 0x8F9D; + public const int GL_PRIMITIVE_RESTART_INDEX = 0x8F9E; + public const int GL_COPY_READ_BUFFER = 0x8F36; + public const int GL_COPY_WRITE_BUFFER = 0x8F37; + public const int GL_UNIFORM_BUFFER = 0x8A11; + public const int GL_UNIFORM_BUFFER_BINDING = 0x8A28; + public const int GL_UNIFORM_BUFFER_START = 0x8A29; + public const int GL_UNIFORM_BUFFER_SIZE = 0x8A2A; + public const int GL_MAX_VERTEX_UNIFORM_BLOCKS = 0x8A2B; + public const int GL_MAX_GEOMETRY_UNIFORM_BLOCKS = 0x8A2C; + public const int GL_MAX_FRAGMENT_UNIFORM_BLOCKS = 0x8A2D; + public const int GL_MAX_COMBINED_UNIFORM_BLOCKS = 0x8A2E; + public const int GL_MAX_UNIFORM_BUFFER_BINDINGS = 0x8A2F; + public const int GL_MAX_UNIFORM_BLOCK_SIZE = 0x8A30; + public const int GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS = 0x8A31; + public const int GL_MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS = 0x8A32; + public const int GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS = 0x8A33; + public const int GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT = 0x8A34; + public const int GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH = 0x8A35; + public const int GL_ACTIVE_UNIFORM_BLOCKS = 0x8A36; + public const int GL_UNIFORM_TYPE = 0x8A37; + public const int GL_UNIFORM_SIZE = 0x8A38; + public const int GL_UNIFORM_NAME_LENGTH = 0x8A39; + public const int GL_UNIFORM_BLOCK_INDEX = 0x8A3A; + public const int GL_UNIFORM_OFFSET = 0x8A3B; + public const int GL_UNIFORM_ARRAY_STRIDE = 0x8A3C; + public const int GL_UNIFORM_MATRIX_STRIDE = 0x8A3D; + public const int GL_UNIFORM_IS_ROW_MAJOR = 0x8A3E; + public const int GL_UNIFORM_BLOCK_BINDING = 0x8A3F; + public const int GL_UNIFORM_BLOCK_DATA_SIZE = 0x8A40; + public const int GL_UNIFORM_BLOCK_NAME_LENGTH = 0x8A41; + public const int GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS = 0x8A42; + public const int GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES = 0x8A43; + public const int GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER = 0x8A44; + public const int GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER = 0x8A45; + public const int GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER = 0x8A46; + public const int GL_INVALID_INDEX = -1; + public const int GL_VERSION_3_2 = 1; + public const int GL_CONTEXT_CORE_PROFILE_BIT = 0x00000001; + public const int GL_CONTEXT_COMPATIBILITY_PROFILE_BIT = 0x00000002; + public const int GL_LINES_ADJACENCY = 0x000A; + public const int GL_LINE_STRIP_ADJACENCY = 0x000B; + public const int GL_TRIANGLES_ADJACENCY = 0x000C; + public const int GL_TRIANGLE_STRIP_ADJACENCY = 0x000D; + public const int GL_PROGRAM_POINT_SIZE = 0x8642; + public const int GL_MAX_GEOMETRY_TEXTURE_IMAGE_UNITS = 0x8C29; + public const int GL_FRAMEBUFFER_ATTACHMENT_LAYERED = 0x8DA7; + public const int GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS = 0x8DA8; + public const int GL_GEOMETRY_SHADER = 0x8DD9; + public const int GL_GEOMETRY_VERTICES_OUT = 0x8916; + public const int GL_GEOMETRY_INPUT_TYPE = 0x8917; + public const int GL_GEOMETRY_OUTPUT_TYPE = 0x8918; + public const int GL_MAX_GEOMETRY_UNIFORM_COMPONENTS = 0x8DDF; + public const int GL_MAX_GEOMETRY_OUTPUT_VERTICES = 0x8DE0; + public const int GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS = 0x8DE1; + public const int GL_MAX_VERTEX_OUTPUT_COMPONENTS = 0x9122; + public const int GL_MAX_GEOMETRY_INPUT_COMPONENTS = 0x9123; + public const int GL_MAX_GEOMETRY_OUTPUT_COMPONENTS = 0x9124; + public const int GL_MAX_FRAGMENT_INPUT_COMPONENTS = 0x9125; + public const int GL_CONTEXT_PROFILE_MASK = 0x9126; + public const int GL_DEPTH_CLAMP = 0x864F; + public const int GL_QUADS_FOLLOW_PROVOKING_VERTEX_CONVENTION = 0x8E4C; + public const int GL_FIRST_VERTEX_CONVENTION = 0x8E4D; + public const int GL_LAST_VERTEX_CONVENTION = 0x8E4E; + public const int GL_PROVOKING_VERTEX = 0x8E4F; + public const int GL_TEXTURE_CUBE_MAP_SEAMLESS = 0x884F; + public const int GL_MAX_SERVER_WAIT_TIMEOUT = 0x9111; + public const int GL_OBJECT_TYPE = 0x9112; + public const int GL_SYNC_CONDITION = 0x9113; + public const int GL_SYNC_STATUS = 0x9114; + public const int GL_SYNC_FLAGS = 0x9115; + public const int GL_SYNC_FENCE = 0x9116; + public const int GL_SYNC_GPU_COMMANDS_COMPLETE = 0x9117; + public const int GL_UNSIGNALED = 0x9118; + public const int GL_SIGNALED = 0x9119; + public const int GL_ALREADY_SIGNALED = 0x911A; + public const int GL_TIMEOUT_EXPIRED = 0x911B; + public const int GL_CONDITION_SATISFIED = 0x911C; + public const int GL_WAIT_FAILED = 0x911D; + public const int GL_SYNC_FLUSH_COMMANDS_BIT = 0x00000001; + public const int GL_SAMPLE_POSITION = 0x8E50; + public const int GL_SAMPLE_MASK = 0x8E51; + public const int GL_SAMPLE_MASK_VALUE = 0x8E52; + public const int GL_MAX_SAMPLE_MASK_WORDS = 0x8E59; + public const int GL_TEXTURE_2D_MULTISAMPLE = 0x9100; + public const int GL_PROXY_TEXTURE_2D_MULTISAMPLE = 0x9101; + public const int GL_TEXTURE_2D_MULTISAMPLE_ARRAY = 0x9102; + public const int GL_PROXY_TEXTURE_2D_MULTISAMPLE_ARRAY = 0x9103; + public const int GL_TEXTURE_BINDING_2D_MULTISAMPLE = 0x9104; + public const int GL_TEXTURE_BINDING_2D_MULTISAMPLE_ARRAY = 0x9105; + public const int GL_TEXTURE_SAMPLES = 0x9106; + public const int GL_TEXTURE_FIXED_SAMPLE_LOCATIONS = 0x9107; + public const int GL_SAMPLER_2D_MULTISAMPLE = 0x9108; + public const int GL_INT_SAMPLER_2D_MULTISAMPLE = 0x9109; + public const int GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE = 0x910A; + public const int GL_SAMPLER_2D_MULTISAMPLE_ARRAY = 0x910B; + public const int GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY = 0x910C; + public const int GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY = 0x910D; + public const int GL_MAX_COLOR_TEXTURE_SAMPLES = 0x910E; + public const int GL_MAX_DEPTH_TEXTURE_SAMPLES = 0x910F; + public const int GL_MAX_INTEGER_SAMPLES = 0x9110; + public const int GL_VERSION_3_3 = 1; + public const int GL_VERTEX_ATTRIB_ARRAY_DIVISOR = 0x88FE; + public const int GL_SRC1_COLOR = 0x88F9; + public const int GL_ONE_MINUS_SRC1_COLOR = 0x88FA; + public const int GL_ONE_MINUS_SRC1_ALPHA = 0x88FB; + public const int GL_MAX_DUAL_SOURCE_DRAW_BUFFERS = 0x88FC; + public const int GL_ANY_SAMPLES_PASSED = 0x8C2F; + public const int GL_SAMPLER_BINDING = 0x8919; + public const int GL_RGB10_A2UI = 0x906F; + public const int GL_TEXTURE_SWIZZLE_R = 0x8E42; + public const int GL_TEXTURE_SWIZZLE_G = 0x8E43; + public const int GL_TEXTURE_SWIZZLE_B = 0x8E44; + public const int GL_TEXTURE_SWIZZLE_A = 0x8E45; + public const int GL_TEXTURE_SWIZZLE_RGBA = 0x8E46; + public const int GL_TIME_ELAPSED = 0x88BF; + public const int GL_TIMESTAMP = 0x8E28; + public const int GL_INT_2_10_10_10_REV = 0x8D9F; + public const int GL_VERSION_4_0 = 1; + public const int GL_SAMPLE_SHADING = 0x8C36; + public const int GL_MIN_SAMPLE_SHADING_VALUE = 0x8C37; + public const int GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET = 0x8E5E; + public const int GL_MAX_PROGRAM_TEXTURE_GATHER_OFFSET = 0x8E5F; + public const int GL_TEXTURE_CUBE_MAP_ARRAY = 0x9009; + public const int GL_TEXTURE_BINDING_CUBE_MAP_ARRAY = 0x900A; + public const int GL_PROXY_TEXTURE_CUBE_MAP_ARRAY = 0x900B; + public const int GL_SAMPLER_CUBE_MAP_ARRAY = 0x900C; + public const int GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW = 0x900D; + public const int GL_INT_SAMPLER_CUBE_MAP_ARRAY = 0x900E; + public const int GL_UNSIGNED_INT_SAMPLER_CUBE_MAP_ARRAY = 0x900F; + public const int GL_DRAW_INDIRECT_BUFFER = 0x8F3F; + public const int GL_DRAW_INDIRECT_BUFFER_BINDING = 0x8F43; + public const int GL_GEOMETRY_SHADER_INVOCATIONS = 0x887F; + public const int GL_MAX_GEOMETRY_SHADER_INVOCATIONS = 0x8E5A; + public const int GL_MIN_FRAGMENT_INTERPOLATION_OFFSET = 0x8E5B; + public const int GL_MAX_FRAGMENT_INTERPOLATION_OFFSET = 0x8E5C; + public const int GL_FRAGMENT_INTERPOLATION_OFFSET_BITS = 0x8E5D; + public const int GL_MAX_VERTEX_STREAMS = 0x8E71; + public const int GL_DOUBLE_VEC2 = 0x8FFC; + public const int GL_DOUBLE_VEC3 = 0x8FFD; + public const int GL_DOUBLE_VEC4 = 0x8FFE; + public const int GL_DOUBLE_MAT2 = 0x8F46; + public const int GL_DOUBLE_MAT3 = 0x8F47; + public const int GL_DOUBLE_MAT4 = 0x8F48; + public const int GL_DOUBLE_MAT2x3 = 0x8F49; + public const int GL_DOUBLE_MAT2x4 = 0x8F4A; + public const int GL_DOUBLE_MAT3x2 = 0x8F4B; + public const int GL_DOUBLE_MAT3x4 = 0x8F4C; + public const int GL_DOUBLE_MAT4x2 = 0x8F4D; + public const int GL_DOUBLE_MAT4x3 = 0x8F4E; + public const int GL_ACTIVE_SUBROUTINES = 0x8DE5; + public const int GL_ACTIVE_SUBROUTINE_UNIFORMS = 0x8DE6; + public const int GL_ACTIVE_SUBROUTINE_UNIFORM_LOCATIONS = 0x8E47; + public const int GL_ACTIVE_SUBROUTINE_MAX_LENGTH = 0x8E48; + public const int GL_ACTIVE_SUBROUTINE_UNIFORM_MAX_LENGTH = 0x8E49; + public const int GL_MAX_SUBROUTINES = 0x8DE7; + public const int GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS = 0x8DE8; + public const int GL_NUM_COMPATIBLE_SUBROUTINES = 0x8E4A; + public const int GL_COMPATIBLE_SUBROUTINES = 0x8E4B; + public const int GL_PATCHES = 0x000E; + public const int GL_PATCH_VERTICES = 0x8E72; + public const int GL_PATCH_DEFAULT_INNER_LEVEL = 0x8E73; + public const int GL_PATCH_DEFAULT_OUTER_LEVEL = 0x8E74; + public const int GL_TESS_CONTROL_OUTPUT_VERTICES = 0x8E75; + public const int GL_TESS_GEN_MODE = 0x8E76; + public const int GL_TESS_GEN_SPACING = 0x8E77; + public const int GL_TESS_GEN_VERTEX_ORDER = 0x8E78; + public const int GL_TESS_GEN_POINT_MODE = 0x8E79; + public const int GL_ISOLINES = 0x8E7A; + public const int GL_FRACTIONAL_ODD = 0x8E7B; + public const int GL_FRACTIONAL_EVEN = 0x8E7C; + public const int GL_MAX_PATCH_VERTICES = 0x8E7D; + public const int GL_MAX_TESS_GEN_LEVEL = 0x8E7E; + public const int GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS = 0x8E7F; + public const int GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS = 0x8E80; + public const int GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS = 0x8E81; + public const int GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS = 0x8E82; + public const int GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS = 0x8E83; + public const int GL_MAX_TESS_PATCH_COMPONENTS = 0x8E84; + public const int GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS = 0x8E85; + public const int GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS = 0x8E86; + public const int GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS = 0x8E89; + public const int GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS = 0x8E8A; + public const int GL_MAX_TESS_CONTROL_INPUT_COMPONENTS = 0x886C; + public const int GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS = 0x886D; + public const int GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS = 0x8E1E; + public const int GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS = 0x8E1F; + public const int GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_CONTROL_SHADER = 0x84F0; + public const int GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_EVALUATION_SHADER = 0x84F1; + public const int GL_TESS_EVALUATION_SHADER = 0x8E87; + public const int GL_TESS_CONTROL_SHADER = 0x8E88; + public const int GL_TRANSFORM_FEEDBACK = 0x8E22; + public const int GL_TRANSFORM_FEEDBACK_BUFFER_PAUSED = 0x8E23; + public const int GL_TRANSFORM_FEEDBACK_BUFFER_ACTIVE = 0x8E24; + public const int GL_TRANSFORM_FEEDBACK_BINDING = 0x8E25; + public const int GL_MAX_TRANSFORM_FEEDBACK_BUFFERS = 0x8E70; + public const int GL_VERSION_4_1 = 1; + public const int GL_FIXED = 0x140C; + public const int GL_IMPLEMENTATION_COLOR_READ_TYPE = 0x8B9A; + public const int GL_IMPLEMENTATION_COLOR_READ_FORMAT = 0x8B9B; + public const int GL_LOW_FLOAT = 0x8DF0; + public const int GL_MEDIUM_FLOAT = 0x8DF1; + public const int GL_HIGH_FLOAT = 0x8DF2; + public const int GL_LOW_INT = 0x8DF3; + public const int GL_MEDIUM_INT = 0x8DF4; + public const int GL_HIGH_INT = 0x8DF5; + public const int GL_SHADER_COMPILER = 0x8DFA; + public const int GL_SHADER_BINARY_FORMATS = 0x8DF8; + public const int GL_NUM_SHADER_BINARY_FORMATS = 0x8DF9; + public const int GL_MAX_VERTEX_UNIFORM_VECTORS = 0x8DFB; + public const int GL_MAX_VARYING_VECTORS = 0x8DFC; + public const int GL_MAX_FRAGMENT_UNIFORM_VECTORS = 0x8DFD; + public const int GL_RGB565 = 0x8D62; + public const int GL_PROGRAM_BINARY_RETRIEVABLE_HINT = 0x8257; + public const int GL_PROGRAM_BINARY_LENGTH = 0x8741; + public const int GL_NUM_PROGRAM_BINARY_FORMATS = 0x87FE; + public const int GL_PROGRAM_BINARY_FORMATS = 0x87FF; + public const int GL_VERTEX_SHADER_BIT = 0x00000001; + public const int GL_FRAGMENT_SHADER_BIT = 0x00000002; + public const int GL_GEOMETRY_SHADER_BIT = 0x00000004; + public const int GL_TESS_CONTROL_SHADER_BIT = 0x00000008; + public const int GL_TESS_EVALUATION_SHADER_BIT = 0x00000010; + public const int GL_ALL_SHADER_BITS = -1; + public const int GL_PROGRAM_SEPARABLE = 0x8258; + public const int GL_ACTIVE_PROGRAM = 0x8259; + public const int GL_PROGRAM_PIPELINE_BINDING = 0x825A; + public const int GL_MAX_VIEWPORTS = 0x825B; + public const int GL_VIEWPORT_SUBPIXEL_BITS = 0x825C; + public const int GL_VIEWPORT_BOUNDS_RANGE = 0x825D; + public const int GL_LAYER_PROVOKING_VERTEX = 0x825E; + public const int GL_VIEWPORT_INDEX_PROVOKING_VERTEX = 0x825F; + public const int GL_UNDEFINED_VERTEX = 0x8260; + public const int GL_VERSION_4_2 = 1; + public const int GL_COPY_READ_BUFFER_BINDING = 0x8F36; + public const int GL_COPY_WRITE_BUFFER_BINDING = 0x8F37; + public const int GL_TRANSFORM_FEEDBACK_ACTIVE = 0x8E24; + public const int GL_TRANSFORM_FEEDBACK_PAUSED = 0x8E23; + public const int GL_UNPACK_COMPRESSED_BLOCK_WIDTH = 0x9127; + public const int GL_UNPACK_COMPRESSED_BLOCK_HEIGHT = 0x9128; + public const int GL_UNPACK_COMPRESSED_BLOCK_DEPTH = 0x9129; + public const int GL_UNPACK_COMPRESSED_BLOCK_SIZE = 0x912A; + public const int GL_PACK_COMPRESSED_BLOCK_WIDTH = 0x912B; + public const int GL_PACK_COMPRESSED_BLOCK_HEIGHT = 0x912C; + public const int GL_PACK_COMPRESSED_BLOCK_DEPTH = 0x912D; + public const int GL_PACK_COMPRESSED_BLOCK_SIZE = 0x912E; + public const int GL_NUM_SAMPLE_COUNTS = 0x9380; + public const int GL_MIN_MAP_BUFFER_ALIGNMENT = 0x90BC; + public const int GL_ATOMIC_COUNTER_BUFFER = 0x92C0; + public const int GL_ATOMIC_COUNTER_BUFFER_BINDING = 0x92C1; + public const int GL_ATOMIC_COUNTER_BUFFER_START = 0x92C2; + public const int GL_ATOMIC_COUNTER_BUFFER_SIZE = 0x92C3; + public const int GL_ATOMIC_COUNTER_BUFFER_DATA_SIZE = 0x92C4; + public const int GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTERS = 0x92C5; + public const int GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES = 0x92C6; + public const int GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_VERTEX_SHADER = 0x92C7; + public const int GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_CONTROL_SHADER = 0x92C8; + public const int GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_EVALUATION_SHADER = 0x92C9; + public const int GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_GEOMETRY_SHADER = 0x92CA; + public const int GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_FRAGMENT_SHADER = 0x92CB; + public const int GL_MAX_VERTEX_ATOMIC_COUNTER_BUFFERS = 0x92CC; + public const int GL_MAX_TESS_CONTROL_ATOMIC_COUNTER_BUFFERS = 0x92CD; + public const int GL_MAX_TESS_EVALUATION_ATOMIC_COUNTER_BUFFERS = 0x92CE; + public const int GL_MAX_GEOMETRY_ATOMIC_COUNTER_BUFFERS = 0x92CF; + public const int GL_MAX_FRAGMENT_ATOMIC_COUNTER_BUFFERS = 0x92D0; + public const int GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS = 0x92D1; + public const int GL_MAX_VERTEX_ATOMIC_COUNTERS = 0x92D2; + public const int GL_MAX_TESS_CONTROL_ATOMIC_COUNTERS = 0x92D3; + public const int GL_MAX_TESS_EVALUATION_ATOMIC_COUNTERS = 0x92D4; + public const int GL_MAX_GEOMETRY_ATOMIC_COUNTERS = 0x92D5; + public const int GL_MAX_FRAGMENT_ATOMIC_COUNTERS = 0x92D6; + public const int GL_MAX_COMBINED_ATOMIC_COUNTERS = 0x92D7; + public const int GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE = 0x92D8; + public const int GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS = 0x92DC; + public const int GL_ACTIVE_ATOMIC_COUNTER_BUFFERS = 0x92D9; + public const int GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX = 0x92DA; + public const int GL_UNSIGNED_INT_ATOMIC_COUNTER = 0x92DB; + public const int GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT = 0x00000001; + public const int GL_ELEMENT_ARRAY_BARRIER_BIT = 0x00000002; + public const int GL_UNIFORM_BARRIER_BIT = 0x00000004; + public const int GL_TEXTURE_FETCH_BARRIER_BIT = 0x00000008; + public const int GL_SHADER_IMAGE_ACCESS_BARRIER_BIT = 0x00000020; + public const int GL_COMMAND_BARRIER_BIT = 0x00000040; + public const int GL_PIXEL_BUFFER_BARRIER_BIT = 0x00000080; + public const int GL_TEXTURE_UPDATE_BARRIER_BIT = 0x00000100; + public const int GL_BUFFER_UPDATE_BARRIER_BIT = 0x00000200; + public const int GL_FRAMEBUFFER_BARRIER_BIT = 0x00000400; + public const int GL_TRANSFORM_FEEDBACK_BARRIER_BIT = 0x00000800; + public const int GL_ATOMIC_COUNTER_BARRIER_BIT = 0x00001000; + public const int GL_ALL_BARRIER_BITS = -1; + public const int GL_MAX_IMAGE_UNITS = 0x8F38; + public const int GL_MAX_COMBINED_IMAGE_UNITS_AND_FRAGMENT_OUTPUTS = 0x8F39; + public const int GL_IMAGE_BINDING_NAME = 0x8F3A; + public const int GL_IMAGE_BINDING_LEVEL = 0x8F3B; + public const int GL_IMAGE_BINDING_LAYERED = 0x8F3C; + public const int GL_IMAGE_BINDING_LAYER = 0x8F3D; + public const int GL_IMAGE_BINDING_ACCESS = 0x8F3E; + public const int GL_IMAGE_1D = 0x904C; + public const int GL_IMAGE_2D = 0x904D; + public const int GL_IMAGE_3D = 0x904E; + public const int GL_IMAGE_2D_RECT = 0x904F; + public const int GL_IMAGE_CUBE = 0x9050; + public const int GL_IMAGE_BUFFER = 0x9051; + public const int GL_IMAGE_1D_ARRAY = 0x9052; + public const int GL_IMAGE_2D_ARRAY = 0x9053; + public const int GL_IMAGE_CUBE_MAP_ARRAY = 0x9054; + public const int GL_IMAGE_2D_MULTISAMPLE = 0x9055; + public const int GL_IMAGE_2D_MULTISAMPLE_ARRAY = 0x9056; + public const int GL_INT_IMAGE_1D = 0x9057; + public const int GL_INT_IMAGE_2D = 0x9058; + public const int GL_INT_IMAGE_3D = 0x9059; + public const int GL_INT_IMAGE_2D_RECT = 0x905A; + public const int GL_INT_IMAGE_CUBE = 0x905B; + public const int GL_INT_IMAGE_BUFFER = 0x905C; + public const int GL_INT_IMAGE_1D_ARRAY = 0x905D; + public const int GL_INT_IMAGE_2D_ARRAY = 0x905E; + public const int GL_INT_IMAGE_CUBE_MAP_ARRAY = 0x905F; + public const int GL_INT_IMAGE_2D_MULTISAMPLE = 0x9060; + public const int GL_INT_IMAGE_2D_MULTISAMPLE_ARRAY = 0x9061; + public const int GL_UNSIGNED_INT_IMAGE_1D = 0x9062; + public const int GL_UNSIGNED_INT_IMAGE_2D = 0x9063; + public const int GL_UNSIGNED_INT_IMAGE_3D = 0x9064; + public const int GL_UNSIGNED_INT_IMAGE_2D_RECT = 0x9065; + public const int GL_UNSIGNED_INT_IMAGE_CUBE = 0x9066; + public const int GL_UNSIGNED_INT_IMAGE_BUFFER = 0x9067; + public const int GL_UNSIGNED_INT_IMAGE_1D_ARRAY = 0x9068; + public const int GL_UNSIGNED_INT_IMAGE_2D_ARRAY = 0x9069; + public const int GL_UNSIGNED_INT_IMAGE_CUBE_MAP_ARRAY = 0x906A; + public const int GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE = 0x906B; + public const int GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY = 0x906C; + public const int GL_MAX_IMAGE_SAMPLES = 0x906D; + public const int GL_IMAGE_BINDING_FORMAT = 0x906E; + public const int GL_IMAGE_FORMAT_COMPATIBILITY_TYPE = 0x90C7; + public const int GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE = 0x90C8; + public const int GL_IMAGE_FORMAT_COMPATIBILITY_BY_CLASS = 0x90C9; + public const int GL_MAX_VERTEX_IMAGE_UNIFORMS = 0x90CA; + public const int GL_MAX_TESS_CONTROL_IMAGE_UNIFORMS = 0x90CB; + public const int GL_MAX_TESS_EVALUATION_IMAGE_UNIFORMS = 0x90CC; + public const int GL_MAX_GEOMETRY_IMAGE_UNIFORMS = 0x90CD; + public const int GL_MAX_FRAGMENT_IMAGE_UNIFORMS = 0x90CE; + public const int GL_MAX_COMBINED_IMAGE_UNIFORMS = 0x90CF; + public const int GL_COMPRESSED_RGBA_BPTC_UNORM = 0x8E8C; + public const int GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM = 0x8E8D; + public const int GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT = 0x8E8E; + public const int GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT = 0x8E8F; + public const int GL_TEXTURE_IMMUTABLE_FORMAT = 0x912F; + public const int GL_VERSION_4_3 = 1; + public const int GL_NUM_SHADING_LANGUAGE_VERSIONS = 0x82E9; + public const int GL_VERTEX_ATTRIB_ARRAY_LONG = 0x874E; + public const int GL_COMPRESSED_RGB8_ETC2 = 0x9274; + public const int GL_COMPRESSED_SRGB8_ETC2 = 0x9275; + public const int GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9276; + public const int GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9277; + public const int GL_COMPRESSED_RGBA8_ETC2_EAC = 0x9278; + public const int GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC = 0x9279; + public const int GL_COMPRESSED_R11_EAC = 0x9270; + public const int GL_COMPRESSED_SIGNED_R11_EAC = 0x9271; + public const int GL_COMPRESSED_RG11_EAC = 0x9272; + public const int GL_COMPRESSED_SIGNED_RG11_EAC = 0x9273; + public const int GL_PRIMITIVE_RESTART_FIXED_INDEX = 0x8D69; + public const int GL_ANY_SAMPLES_PASSED_CONSERVATIVE = 0x8D6A; + public const int GL_MAX_ELEMENT_INDEX = 0x8D6B; + public const int GL_COMPUTE_SHADER = 0x91B9; + public const int GL_MAX_COMPUTE_UNIFORM_BLOCKS = 0x91BB; + public const int GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS = 0x91BC; + public const int GL_MAX_COMPUTE_IMAGE_UNIFORMS = 0x91BD; + public const int GL_MAX_COMPUTE_SHARED_MEMORY_SIZE = 0x8262; + public const int GL_MAX_COMPUTE_UNIFORM_COMPONENTS = 0x8263; + public const int GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS = 0x8264; + public const int GL_MAX_COMPUTE_ATOMIC_COUNTERS = 0x8265; + public const int GL_MAX_COMBINED_COMPUTE_UNIFORM_COMPONENTS = 0x8266; + public const int GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS = 0x90EB; + public const int GL_MAX_COMPUTE_WORK_GROUP_COUNT = 0x91BE; + public const int GL_MAX_COMPUTE_WORK_GROUP_SIZE = 0x91BF; + public const int GL_COMPUTE_WORK_GROUP_SIZE = 0x8267; + public const int GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER = 0x90EC; + public const int GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_COMPUTE_SHADER = 0x90ED; + public const int GL_DISPATCH_INDIRECT_BUFFER = 0x90EE; + public const int GL_DISPATCH_INDIRECT_BUFFER_BINDING = 0x90EF; + public const int GL_COMPUTE_SHADER_BIT = 0x00000020; + public const int GL_DEBUG_OUTPUT_SYNCHRONOUS = 0x8242; + public const int GL_DEBUG_NEXT_LOGGED_MESSAGE_LENGTH = 0x8243; + public const int GL_DEBUG_CALLBACK_FUNCTION = 0x8244; + public const int GL_DEBUG_CALLBACK_USER_PARAM = 0x8245; + public const int GL_DEBUG_SOURCE_API = 0x8246; + public const int GL_DEBUG_SOURCE_WINDOW_SYSTEM = 0x8247; + public const int GL_DEBUG_SOURCE_SHADER_COMPILER = 0x8248; + public const int GL_DEBUG_SOURCE_THIRD_PARTY = 0x8249; + public const int GL_DEBUG_SOURCE_APPLICATION = 0x824A; + public const int GL_DEBUG_SOURCE_OTHER = 0x824B; + public const int GL_DEBUG_TYPE_ERROR = 0x824C; + public const int GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR = 0x824D; + public const int GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR = 0x824E; + public const int GL_DEBUG_TYPE_PORTABILITY = 0x824F; + public const int GL_DEBUG_TYPE_PERFORMANCE = 0x8250; + public const int GL_DEBUG_TYPE_OTHER = 0x8251; + public const int GL_MAX_DEBUG_MESSAGE_LENGTH = 0x9143; + public const int GL_MAX_DEBUG_LOGGED_MESSAGES = 0x9144; + public const int GL_DEBUG_LOGGED_MESSAGES = 0x9145; + public const int GL_DEBUG_SEVERITY_HIGH = 0x9146; + public const int GL_DEBUG_SEVERITY_MEDIUM = 0x9147; + public const int GL_DEBUG_SEVERITY_LOW = 0x9148; + public const int GL_DEBUG_TYPE_MARKER = 0x8268; + public const int GL_DEBUG_TYPE_PUSH_GROUP = 0x8269; + public const int GL_DEBUG_TYPE_POP_GROUP = 0x826A; + public const int GL_DEBUG_SEVERITY_NOTIFICATION = 0x826B; + public const int GL_MAX_DEBUG_GROUP_STACK_DEPTH = 0x826C; + public const int GL_DEBUG_GROUP_STACK_DEPTH = 0x826D; + public const int GL_BUFFER = 0x82E0; + public const int GL_SHADER = 0x82E1; + public const int GL_PROGRAM = 0x82E2; + public const int GL_QUERY = 0x82E3; + public const int GL_PROGRAM_PIPELINE = 0x82E4; + public const int GL_SAMPLER = 0x82E6; + public const int GL_MAX_LABEL_LENGTH = 0x82E8; + public const int GL_DEBUG_OUTPUT = 0x92E0; + public const int GL_CONTEXT_FLAG_DEBUG_BIT = 0x00000002; + public const int GL_MAX_UNIFORM_LOCATIONS = 0x826E; + public const int GL_FRAMEBUFFER_DEFAULT_WIDTH = 0x9310; + public const int GL_FRAMEBUFFER_DEFAULT_HEIGHT = 0x9311; + public const int GL_FRAMEBUFFER_DEFAULT_LAYERS = 0x9312; + public const int GL_FRAMEBUFFER_DEFAULT_SAMPLES = 0x9313; + public const int GL_FRAMEBUFFER_DEFAULT_FIXED_SAMPLE_LOCATIONS = 0x9314; + public const int GL_MAX_FRAMEBUFFER_WIDTH = 0x9315; + public const int GL_MAX_FRAMEBUFFER_HEIGHT = 0x9316; + public const int GL_MAX_FRAMEBUFFER_LAYERS = 0x9317; + public const int GL_MAX_FRAMEBUFFER_SAMPLES = 0x9318; + public const int GL_INTERNALFORMAT_SUPPORTED = 0x826F; + public const int GL_INTERNALFORMAT_PREFERRED = 0x8270; + public const int GL_INTERNALFORMAT_RED_SIZE = 0x8271; + public const int GL_INTERNALFORMAT_GREEN_SIZE = 0x8272; + public const int GL_INTERNALFORMAT_BLUE_SIZE = 0x8273; + public const int GL_INTERNALFORMAT_ALPHA_SIZE = 0x8274; + public const int GL_INTERNALFORMAT_DEPTH_SIZE = 0x8275; + public const int GL_INTERNALFORMAT_STENCIL_SIZE = 0x8276; + public const int GL_INTERNALFORMAT_SHARED_SIZE = 0x8277; + public const int GL_INTERNALFORMAT_RED_TYPE = 0x8278; + public const int GL_INTERNALFORMAT_GREEN_TYPE = 0x8279; + public const int GL_INTERNALFORMAT_BLUE_TYPE = 0x827A; + public const int GL_INTERNALFORMAT_ALPHA_TYPE = 0x827B; + public const int GL_INTERNALFORMAT_DEPTH_TYPE = 0x827C; + public const int GL_INTERNALFORMAT_STENCIL_TYPE = 0x827D; + public const int GL_MAX_WIDTH = 0x827E; + public const int GL_MAX_HEIGHT = 0x827F; + public const int GL_MAX_DEPTH = 0x8280; + public const int GL_MAX_LAYERS = 0x8281; + public const int GL_MAX_COMBINED_DIMENSIONS = 0x8282; + public const int GL_COLOR_COMPONENTS = 0x8283; + public const int GL_DEPTH_COMPONENTS = 0x8284; + public const int GL_STENCIL_COMPONENTS = 0x8285; + public const int GL_COLOR_RENDERABLE = 0x8286; + public const int GL_DEPTH_RENDERABLE = 0x8287; + public const int GL_STENCIL_RENDERABLE = 0x8288; + public const int GL_FRAMEBUFFER_RENDERABLE = 0x8289; + public const int GL_FRAMEBUFFER_RENDERABLE_LAYERED = 0x828A; + public const int GL_FRAMEBUFFER_BLEND = 0x828B; + public const int GL_READ_PIXELS = 0x828C; + public const int GL_READ_PIXELS_FORMAT = 0x828D; + public const int GL_READ_PIXELS_TYPE = 0x828E; + public const int GL_TEXTURE_IMAGE_FORMAT = 0x828F; + public const int GL_TEXTURE_IMAGE_TYPE = 0x8290; + public const int GL_GET_TEXTURE_IMAGE_FORMAT = 0x8291; + public const int GL_GET_TEXTURE_IMAGE_TYPE = 0x8292; + public const int GL_MIPMAP = 0x8293; + public const int GL_MANUAL_GENERATE_MIPMAP = 0x8294; + public const int GL_AUTO_GENERATE_MIPMAP = 0x8295; + public const int GL_COLOR_ENCODING = 0x8296; + public const int GL_SRGB_READ = 0x8297; + public const int GL_SRGB_WRITE = 0x8298; + public const int GL_FILTER = 0x829A; + public const int GL_VERTEX_TEXTURE = 0x829B; + public const int GL_TESS_CONTROL_TEXTURE = 0x829C; + public const int GL_TESS_EVALUATION_TEXTURE = 0x829D; + public const int GL_GEOMETRY_TEXTURE = 0x829E; + public const int GL_FRAGMENT_TEXTURE = 0x829F; + public const int GL_COMPUTE_TEXTURE = 0x82A0; + public const int GL_TEXTURE_SHADOW = 0x82A1; + public const int GL_TEXTURE_GATHER = 0x82A2; + public const int GL_TEXTURE_GATHER_SHADOW = 0x82A3; + public const int GL_SHADER_IMAGE_LOAD = 0x82A4; + public const int GL_SHADER_IMAGE_STORE = 0x82A5; + public const int GL_SHADER_IMAGE_ATOMIC = 0x82A6; + public const int GL_IMAGE_TEXEL_SIZE = 0x82A7; + public const int GL_IMAGE_COMPATIBILITY_CLASS = 0x82A8; + public const int GL_IMAGE_PIXEL_FORMAT = 0x82A9; + public const int GL_IMAGE_PIXEL_TYPE = 0x82AA; + public const int GL_SIMULTANEOUS_TEXTURE_AND_DEPTH_TEST = 0x82AC; + public const int GL_SIMULTANEOUS_TEXTURE_AND_STENCIL_TEST = 0x82AD; + public const int GL_SIMULTANEOUS_TEXTURE_AND_DEPTH_WRITE = 0x82AE; + public const int GL_SIMULTANEOUS_TEXTURE_AND_STENCIL_WRITE = 0x82AF; + public const int GL_TEXTURE_COMPRESSED_BLOCK_WIDTH = 0x82B1; + public const int GL_TEXTURE_COMPRESSED_BLOCK_HEIGHT = 0x82B2; + public const int GL_TEXTURE_COMPRESSED_BLOCK_SIZE = 0x82B3; + public const int GL_CLEAR_BUFFER = 0x82B4; + public const int GL_TEXTURE_VIEW = 0x82B5; + public const int GL_VIEW_COMPATIBILITY_CLASS = 0x82B6; + public const int GL_FULL_SUPPORT = 0x82B7; + public const int GL_CAVEAT_SUPPORT = 0x82B8; + public const int GL_IMAGE_CLASS_4_X_32 = 0x82B9; + public const int GL_IMAGE_CLASS_2_X_32 = 0x82BA; + public const int GL_IMAGE_CLASS_1_X_32 = 0x82BB; + public const int GL_IMAGE_CLASS_4_X_16 = 0x82BC; + public const int GL_IMAGE_CLASS_2_X_16 = 0x82BD; + public const int GL_IMAGE_CLASS_1_X_16 = 0x82BE; + public const int GL_IMAGE_CLASS_4_X_8 = 0x82BF; + public const int GL_IMAGE_CLASS_2_X_8 = 0x82C0; + public const int GL_IMAGE_CLASS_1_X_8 = 0x82C1; + public const int GL_IMAGE_CLASS_11_11_10 = 0x82C2; + public const int GL_IMAGE_CLASS_10_10_10_2 = 0x82C3; + public const int GL_VIEW_CLASS_128_BITS = 0x82C4; + public const int GL_VIEW_CLASS_96_BITS = 0x82C5; + public const int GL_VIEW_CLASS_64_BITS = 0x82C6; + public const int GL_VIEW_CLASS_48_BITS = 0x82C7; + public const int GL_VIEW_CLASS_32_BITS = 0x82C8; + public const int GL_VIEW_CLASS_24_BITS = 0x82C9; + public const int GL_VIEW_CLASS_16_BITS = 0x82CA; + public const int GL_VIEW_CLASS_8_BITS = 0x82CB; + public const int GL_VIEW_CLASS_S3TC_DXT1_RGB = 0x82CC; + public const int GL_VIEW_CLASS_S3TC_DXT1_RGBA = 0x82CD; + public const int GL_VIEW_CLASS_S3TC_DXT3_RGBA = 0x82CE; + public const int GL_VIEW_CLASS_S3TC_DXT5_RGBA = 0x82CF; + public const int GL_VIEW_CLASS_RGTC1_RED = 0x82D0; + public const int GL_VIEW_CLASS_RGTC2_RG = 0x82D1; + public const int GL_VIEW_CLASS_BPTC_UNORM = 0x82D2; + public const int GL_VIEW_CLASS_BPTC_FLOAT = 0x82D3; + public const int GL_UNIFORM = 0x92E1; + public const int GL_UNIFORM_BLOCK = 0x92E2; + public const int GL_PROGRAM_INPUT = 0x92E3; + public const int GL_PROGRAM_OUTPUT = 0x92E4; + public const int GL_BUFFER_VARIABLE = 0x92E5; + public const int GL_SHADER_STORAGE_BLOCK = 0x92E6; + public const int GL_VERTEX_SUBROUTINE = 0x92E8; + public const int GL_TESS_CONTROL_SUBROUTINE = 0x92E9; + public const int GL_TESS_EVALUATION_SUBROUTINE = 0x92EA; + public const int GL_GEOMETRY_SUBROUTINE = 0x92EB; + public const int GL_FRAGMENT_SUBROUTINE = 0x92EC; + public const int GL_COMPUTE_SUBROUTINE = 0x92ED; + public const int GL_VERTEX_SUBROUTINE_UNIFORM = 0x92EE; + public const int GL_TESS_CONTROL_SUBROUTINE_UNIFORM = 0x92EF; + public const int GL_TESS_EVALUATION_SUBROUTINE_UNIFORM = 0x92F0; + public const int GL_GEOMETRY_SUBROUTINE_UNIFORM = 0x92F1; + public const int GL_FRAGMENT_SUBROUTINE_UNIFORM = 0x92F2; + public const int GL_COMPUTE_SUBROUTINE_UNIFORM = 0x92F3; + public const int GL_TRANSFORM_FEEDBACK_VARYING = 0x92F4; + public const int GL_ACTIVE_RESOURCES = 0x92F5; + public const int GL_MAX_NAME_LENGTH = 0x92F6; + public const int GL_MAX_NUM_ACTIVE_VARIABLES = 0x92F7; + public const int GL_MAX_NUM_COMPATIBLE_SUBROUTINES = 0x92F8; + public const int GL_NAME_LENGTH = 0x92F9; + public const int GL_TYPE = 0x92FA; + public const int GL_ARRAY_SIZE = 0x92FB; + public const int GL_OFFSET = 0x92FC; + public const int GL_BLOCK_INDEX = 0x92FD; + public const int GL_ARRAY_STRIDE = 0x92FE; + public const int GL_MATRIX_STRIDE = 0x92FF; + public const int GL_IS_ROW_MAJOR = 0x9300; + public const int GL_ATOMIC_COUNTER_BUFFER_INDEX = 0x9301; + public const int GL_BUFFER_BINDING = 0x9302; + public const int GL_BUFFER_DATA_SIZE = 0x9303; + public const int GL_NUM_ACTIVE_VARIABLES = 0x9304; + public const int GL_ACTIVE_VARIABLES = 0x9305; + public const int GL_REFERENCED_BY_VERTEX_SHADER = 0x9306; + public const int GL_REFERENCED_BY_TESS_CONTROL_SHADER = 0x9307; + public const int GL_REFERENCED_BY_TESS_EVALUATION_SHADER = 0x9308; + public const int GL_REFERENCED_BY_GEOMETRY_SHADER = 0x9309; + public const int GL_REFERENCED_BY_FRAGMENT_SHADER = 0x930A; + public const int GL_REFERENCED_BY_COMPUTE_SHADER = 0x930B; + public const int GL_TOP_LEVEL_ARRAY_SIZE = 0x930C; + public const int GL_TOP_LEVEL_ARRAY_STRIDE = 0x930D; + public const int GL_LOCATION = 0x930E; + public const int GL_LOCATION_INDEX = 0x930F; + public const int GL_IS_PER_PATCH = 0x92E7; + public const int GL_SHADER_STORAGE_BUFFER = 0x90D2; + public const int GL_SHADER_STORAGE_BUFFER_BINDING = 0x90D3; + public const int GL_SHADER_STORAGE_BUFFER_START = 0x90D4; + public const int GL_SHADER_STORAGE_BUFFER_SIZE = 0x90D5; + public const int GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = 0x90D6; + public const int GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS = 0x90D7; + public const int GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS = 0x90D8; + public const int GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS = 0x90D9; + public const int GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS = 0x90DA; + public const int GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS = 0x90DB; + public const int GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS = 0x90DC; + public const int GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS = 0x90DD; + public const int GL_MAX_SHADER_STORAGE_BLOCK_SIZE = 0x90DE; + public const int GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT = 0x90DF; + public const int GL_SHADER_STORAGE_BARRIER_BIT = 0x00002000; + public const int GL_MAX_COMBINED_SHADER_OUTPUT_RESOURCES = 0x8F39; + public const int GL_DEPTH_STENCIL_TEXTURE_MODE = 0x90EA; + public const int GL_TEXTURE_BUFFER_OFFSET = 0x919D; + public const int GL_TEXTURE_BUFFER_SIZE = 0x919E; + public const int GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT = 0x919F; + public const int GL_TEXTURE_VIEW_MIN_LEVEL = 0x82DB; + public const int GL_TEXTURE_VIEW_NUM_LEVELS = 0x82DC; + public const int GL_TEXTURE_VIEW_MIN_LAYER = 0x82DD; + public const int GL_TEXTURE_VIEW_NUM_LAYERS = 0x82DE; + public const int GL_TEXTURE_IMMUTABLE_LEVELS = 0x82DF; + public const int GL_VERTEX_ATTRIB_BINDING = 0x82D4; + public const int GL_VERTEX_ATTRIB_RELATIVE_OFFSET = 0x82D5; + public const int GL_VERTEX_BINDING_DIVISOR = 0x82D6; + public const int GL_VERTEX_BINDING_OFFSET = 0x82D7; + public const int GL_VERTEX_BINDING_STRIDE = 0x82D8; + public const int GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET = 0x82D9; + public const int GL_MAX_VERTEX_ATTRIB_BINDINGS = 0x82DA; + public const int GL_VERTEX_BINDING_BUFFER = 0x8F4F; + public const int GL_DISPLAY_LIST = 0x82E7; + public const int GL_VERSION_4_4 = 1; + public const int GL_MAX_VERTEX_ATTRIB_STRIDE = 0x82E5; + public const int GL_PRIMITIVE_RESTART_FOR_PATCHES_SUPPORTED = 0x8221; + public const int GL_TEXTURE_BUFFER_BINDING = 0x8C2A; + public const int GL_MAP_PERSISTENT_BIT = 0x0040; + public const int GL_MAP_COHERENT_BIT = 0x0080; + public const int GL_DYNAMIC_STORAGE_BIT = 0x0100; + public const int GL_CLIENT_STORAGE_BIT = 0x0200; + public const int GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT = 0x00004000; + public const int GL_BUFFER_IMMUTABLE_STORAGE = 0x821F; + public const int GL_BUFFER_STORAGE_FLAGS = 0x8220; + public const int GL_CLEAR_TEXTURE = 0x9365; + public const int GL_LOCATION_COMPONENT = 0x934A; + public const int GL_TRANSFORM_FEEDBACK_BUFFER_INDEX = 0x934B; + public const int GL_TRANSFORM_FEEDBACK_BUFFER_STRIDE = 0x934C; + public const int GL_QUERY_BUFFER = 0x9192; + public const int GL_QUERY_BUFFER_BARRIER_BIT = 0x00008000; + public const int GL_QUERY_BUFFER_BINDING = 0x9193; + public const int GL_QUERY_RESULT_NO_WAIT = 0x9194; + public const int GL_MIRROR_CLAMP_TO_EDGE = 0x8743; + public const int GL_NEGATIVE_ONE_TO_ONE = 0x935E; + public const int GL_ZERO_TO_ONE = 0x935F; + public const int GL_CLIP_ORIGIN = 0x935C; + public const int GL_CLIP_DEPTH_MODE = 0x935D; + public const int GL_QUERY_WAIT_INVERTED = 0x8E17; + public const int GL_QUERY_NO_WAIT_INVERTED = 0x8E18; + public const int GL_QUERY_BY_REGION_WAIT_INVERTED = 0x8E19; + public const int GL_QUERY_BY_REGION_NO_WAIT_INVERTED = 0x8E1A; + public const int GL_MAX_CULL_DISTANCES = 0x82F9; + public const int GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES = 0x82FA; + public const int GL_TEXTURE_TARGET = 0x1006; + public const int GL_QUERY_TARGET = 0x82EA; + public const int GL_GUILTY_CONTEXT_RESET = 0x8253; + public const int GL_INNOCENT_CONTEXT_RESET = 0x8254; + public const int GL_UNKNOWN_CONTEXT_RESET = 0x8255; + public const int GL_RESET_NOTIFICATION_STRATEGY = 0x8256; + public const int GL_LOSE_CONTEXT_ON_RESET = 0x8252; + public const int GL_NO_RESET_NOTIFICATION = 0x8261; + public const int GL_CONTEXT_FLAG_ROBUST_ACCESS_BIT = 0x00000004; + public const int GL_CONTEXT_RELEASE_BEHAVIOR = 0x82FB; + public const int GL_CONTEXT_RELEASE_BEHAVIOR_FLUSH = 0x82FC; + public const int GL_VERSION_4_6 = 1; + public const int GL_SHADER_BINARY_FORMAT_SPIR_V = 0x9551; + public const int GL_SPIR_V_BINARY = 0x9552; + public const int GL_PARAMETER_BUFFER = 0x80EE; + public const int GL_PARAMETER_BUFFER_BINDING = 0x80EF; + public const int GL_CONTEXT_FLAG_NO_ERROR_BIT = 0x00000008; + public const int GL_VERTICES_SUBMITTED = 0x82EE; + public const int GL_PRIMITIVES_SUBMITTED = 0x82EF; + public const int GL_VERTEX_SHADER_INVOCATIONS = 0x82F0; + public const int GL_TESS_CONTROL_SHADER_PATCHES = 0x82F1; + public const int GL_TESS_EVALUATION_SHADER_INVOCATIONS = 0x82F2; + public const int GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED = 0x82F3; + public const int GL_FRAGMENT_SHADER_INVOCATIONS = 0x82F4; + public const int GL_COMPUTE_SHADER_INVOCATIONS = 0x82F5; + public const int GL_CLIPPING_INPUT_PRIMITIVES = 0x82F6; + public const int GL_CLIPPING_OUTPUT_PRIMITIVES = 0x82F7; + public const int GL_POLYGON_OFFSET_CLAMP = 0x8E1B; + public const int GL_SPIR_V_EXTENSIONS = 0x9553; + public const int GL_NUM_SPIR_V_EXTENSIONS = 0x9554; + public const int GL_TEXTURE_MAX_ANISOTROPY = 0x84FE; + public const int GL_MAX_TEXTURE_MAX_ANISOTROPY = 0x84FF; + public const int GL_TRANSFORM_FEEDBACK_OVERFLOW = 0x82EC; + public const int GL_TRANSFORM_FEEDBACK_STREAM_OVERFLOW = 0x82ED; + public const int GL_ARB_ES2_compatibility = 1; + public const int GL_ARB_ES3_1_compatibility = 1; + public const int GL_ARB_ES3_2_compatibility = 1; + public const int GL_PRIMITIVE_BOUNDING_BOX_ARB = 0x92BE; + public const int GL_MULTISAMPLE_LINE_WIDTH_RANGE_ARB = 0x9381; + public const int GL_MULTISAMPLE_LINE_WIDTH_GRANULARITY_ARB = 0x9382; + public const int GL_ARB_ES3_compatibility = 1; + public const int GL_ARB_arrays_of_arrays = 1; + public const int GL_ARB_base_instance = 1; + public const int GL_ARB_bindless_texture = 1; + public const int GL_UNSIGNED_INT64_ARB = 0x140F; + public const int GL_ARB_blend_func_extended = 1; + public const int GL_ARB_buffer_storage = 1; + public const int GL_ARB_cl_event = 1; + public const int GL_SYNC_CL_EVENT_ARB = 0x8240; + public const int GL_SYNC_CL_EVENT_COMPLETE_ARB = 0x8241; + public const int GL_ARB_clear_buffer_object = 1; + public const int GL_ARB_clear_texture = 1; + public const int GL_ARB_clip_control = 1; + public const int GL_ARB_color_buffer_float = 1; + public const int GL_RGBA_FLOAT_MODE_ARB = 0x8820; + public const int GL_CLAMP_VERTEX_COLOR_ARB = 0x891A; + public const int GL_CLAMP_FRAGMENT_COLOR_ARB = 0x891B; + public const int GL_CLAMP_READ_COLOR_ARB = 0x891C; + public const int GL_FIXED_ONLY_ARB = 0x891D; + public const int GL_ARB_compatibility = 1; + public const int GL_ARB_compressed_texture_pixel_storage = 1; + public const int GL_ARB_compute_shader = 1; + public const int GL_ARB_compute_variable_group_size = 1; + public const int GL_MAX_COMPUTE_VARIABLE_GROUP_INVOCATIONS_ARB = 0x9344; + public const int GL_MAX_COMPUTE_FIXED_GROUP_INVOCATIONS_ARB = 0x90EB; + public const int GL_MAX_COMPUTE_VARIABLE_GROUP_SIZE_ARB = 0x9345; + public const int GL_MAX_COMPUTE_FIXED_GROUP_SIZE_ARB = 0x91BF; + public const int GL_ARB_conditional_render_inverted = 1; + public const int GL_ARB_conservative_depth = 1; + public const int GL_ARB_copy_buffer = 1; + public const int GL_ARB_copy_image = 1; + public const int GL_ARB_cull_distance = 1; + public const int GL_ARB_debug_output = 1; + public const int GL_DEBUG_OUTPUT_SYNCHRONOUS_ARB = 0x8242; + public const int GL_DEBUG_NEXT_LOGGED_MESSAGE_LENGTH_ARB = 0x8243; + public const int GL_DEBUG_CALLBACK_FUNCTION_ARB = 0x8244; + public const int GL_DEBUG_CALLBACK_USER_PARAM_ARB = 0x8245; + public const int GL_DEBUG_SOURCE_API_ARB = 0x8246; + public const int GL_DEBUG_SOURCE_WINDOW_SYSTEM_ARB = 0x8247; + public const int GL_DEBUG_SOURCE_SHADER_COMPILER_ARB = 0x8248; + public const int GL_DEBUG_SOURCE_THIRD_PARTY_ARB = 0x8249; + public const int GL_DEBUG_SOURCE_APPLICATION_ARB = 0x824A; + public const int GL_DEBUG_SOURCE_OTHER_ARB = 0x824B; + public const int GL_DEBUG_TYPE_ERROR_ARB = 0x824C; + public const int GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR_ARB = 0x824D; + public const int GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR_ARB = 0x824E; + public const int GL_DEBUG_TYPE_PORTABILITY_ARB = 0x824F; + public const int GL_DEBUG_TYPE_PERFORMANCE_ARB = 0x8250; + public const int GL_DEBUG_TYPE_OTHER_ARB = 0x8251; + public const int GL_MAX_DEBUG_MESSAGE_LENGTH_ARB = 0x9143; + public const int GL_MAX_DEBUG_LOGGED_MESSAGES_ARB = 0x9144; + public const int GL_DEBUG_LOGGED_MESSAGES_ARB = 0x9145; + public const int GL_DEBUG_SEVERITY_HIGH_ARB = 0x9146; + public const int GL_DEBUG_SEVERITY_MEDIUM_ARB = 0x9147; + public const int GL_DEBUG_SEVERITY_LOW_ARB = 0x9148; + public const int GL_ARB_depth_buffer_float = 1; + public const int GL_ARB_depth_clamp = 1; + public const int GL_ARB_depth_texture = 1; + public const int GL_DEPTH_COMPONENT16_ARB = 0x81A5; + public const int GL_DEPTH_COMPONENT24_ARB = 0x81A6; + public const int GL_DEPTH_COMPONENT32_ARB = 0x81A7; + public const int GL_TEXTURE_DEPTH_SIZE_ARB = 0x884A; + public const int GL_DEPTH_TEXTURE_MODE_ARB = 0x884B; + public const int GL_ARB_derivative_control = 1; + public const int GL_ARB_direct_state_access = 1; + public const int GL_ARB_draw_buffers = 1; + public const int GL_MAX_DRAW_BUFFERS_ARB = 0x8824; + public const int GL_DRAW_BUFFER0_ARB = 0x8825; + public const int GL_DRAW_BUFFER1_ARB = 0x8826; + public const int GL_DRAW_BUFFER2_ARB = 0x8827; + public const int GL_DRAW_BUFFER3_ARB = 0x8828; + public const int GL_DRAW_BUFFER4_ARB = 0x8829; + public const int GL_DRAW_BUFFER5_ARB = 0x882A; + public const int GL_DRAW_BUFFER6_ARB = 0x882B; + public const int GL_DRAW_BUFFER7_ARB = 0x882C; + public const int GL_DRAW_BUFFER8_ARB = 0x882D; + public const int GL_DRAW_BUFFER9_ARB = 0x882E; + public const int GL_DRAW_BUFFER10_ARB = 0x882F; + public const int GL_DRAW_BUFFER11_ARB = 0x8830; + public const int GL_DRAW_BUFFER12_ARB = 0x8831; + public const int GL_DRAW_BUFFER13_ARB = 0x8832; + public const int GL_DRAW_BUFFER14_ARB = 0x8833; + public const int GL_DRAW_BUFFER15_ARB = 0x8834; + public const int GL_ARB_draw_buffers_blend = 1; + public const int GL_ARB_draw_elements_base_vertex = 1; + public const int GL_ARB_draw_indirect = 1; + public const int GL_ARB_draw_instanced = 1; + public const int GL_ARB_enhanced_layouts = 1; + public const int GL_ARB_explicit_attrib_location = 1; + public const int GL_ARB_explicit_uniform_location = 1; + public const int GL_ARB_fragment_coord_conventions = 1; + public const int GL_ARB_fragment_layer_viewport = 1; + public const int GL_ARB_fragment_program = 1; + public const int GL_FRAGMENT_PROGRAM_ARB = 0x8804; + public const int GL_PROGRAM_FORMAT_ASCII_ARB = 0x8875; + public const int GL_PROGRAM_LENGTH_ARB = 0x8627; + public const int GL_PROGRAM_FORMAT_ARB = 0x8876; + public const int GL_PROGRAM_BINDING_ARB = 0x8677; + public const int GL_PROGRAM_INSTRUCTIONS_ARB = 0x88A0; + public const int GL_MAX_PROGRAM_INSTRUCTIONS_ARB = 0x88A1; + public const int GL_PROGRAM_NATIVE_INSTRUCTIONS_ARB = 0x88A2; + public const int GL_MAX_PROGRAM_NATIVE_INSTRUCTIONS_ARB = 0x88A3; + public const int GL_PROGRAM_TEMPORARIES_ARB = 0x88A4; + public const int GL_MAX_PROGRAM_TEMPORARIES_ARB = 0x88A5; + public const int GL_PROGRAM_NATIVE_TEMPORARIES_ARB = 0x88A6; + public const int GL_MAX_PROGRAM_NATIVE_TEMPORARIES_ARB = 0x88A7; + public const int GL_PROGRAM_PARAMETERS_ARB = 0x88A8; + public const int GL_MAX_PROGRAM_PARAMETERS_ARB = 0x88A9; + public const int GL_PROGRAM_NATIVE_PARAMETERS_ARB = 0x88AA; + public const int GL_MAX_PROGRAM_NATIVE_PARAMETERS_ARB = 0x88AB; + public const int GL_PROGRAM_ATTRIBS_ARB = 0x88AC; + public const int GL_MAX_PROGRAM_ATTRIBS_ARB = 0x88AD; + public const int GL_PROGRAM_NATIVE_ATTRIBS_ARB = 0x88AE; + public const int GL_MAX_PROGRAM_NATIVE_ATTRIBS_ARB = 0x88AF; + public const int GL_MAX_PROGRAM_LOCAL_PARAMETERS_ARB = 0x88B4; + public const int GL_MAX_PROGRAM_ENV_PARAMETERS_ARB = 0x88B5; + public const int GL_PROGRAM_UNDER_NATIVE_LIMITS_ARB = 0x88B6; + public const int GL_PROGRAM_ALU_INSTRUCTIONS_ARB = 0x8805; + public const int GL_PROGRAM_TEX_INSTRUCTIONS_ARB = 0x8806; + public const int GL_PROGRAM_TEX_INDIRECTIONS_ARB = 0x8807; + public const int GL_PROGRAM_NATIVE_ALU_INSTRUCTIONS_ARB = 0x8808; + public const int GL_PROGRAM_NATIVE_TEX_INSTRUCTIONS_ARB = 0x8809; + public const int GL_PROGRAM_NATIVE_TEX_INDIRECTIONS_ARB = 0x880A; + public const int GL_MAX_PROGRAM_ALU_INSTRUCTIONS_ARB = 0x880B; + public const int GL_MAX_PROGRAM_TEX_INSTRUCTIONS_ARB = 0x880C; + public const int GL_MAX_PROGRAM_TEX_INDIRECTIONS_ARB = 0x880D; + public const int GL_MAX_PROGRAM_NATIVE_ALU_INSTRUCTIONS_ARB = 0x880E; + public const int GL_MAX_PROGRAM_NATIVE_TEX_INSTRUCTIONS_ARB = 0x880F; + public const int GL_MAX_PROGRAM_NATIVE_TEX_INDIRECTIONS_ARB = 0x8810; + public const int GL_PROGRAM_STRING_ARB = 0x8628; + public const int GL_PROGRAM_ERROR_POSITION_ARB = 0x864B; + public const int GL_CURRENT_MATRIX_ARB = 0x8641; + public const int GL_TRANSPOSE_CURRENT_MATRIX_ARB = 0x88B7; + public const int GL_CURRENT_MATRIX_STACK_DEPTH_ARB = 0x8640; + public const int GL_MAX_PROGRAM_MATRICES_ARB = 0x862F; + public const int GL_MAX_PROGRAM_MATRIX_STACK_DEPTH_ARB = 0x862E; + public const int GL_MAX_TEXTURE_COORDS_ARB = 0x8871; + public const int GL_MAX_TEXTURE_IMAGE_UNITS_ARB = 0x8872; + public const int GL_PROGRAM_ERROR_STRING_ARB = 0x8874; + public const int GL_MATRIX0_ARB = 0x88C0; + public const int GL_MATRIX1_ARB = 0x88C1; + public const int GL_MATRIX2_ARB = 0x88C2; + public const int GL_MATRIX3_ARB = 0x88C3; + public const int GL_MATRIX4_ARB = 0x88C4; + public const int GL_MATRIX5_ARB = 0x88C5; + public const int GL_MATRIX6_ARB = 0x88C6; + public const int GL_MATRIX7_ARB = 0x88C7; + public const int GL_MATRIX8_ARB = 0x88C8; + public const int GL_MATRIX9_ARB = 0x88C9; + public const int GL_MATRIX10_ARB = 0x88CA; + public const int GL_MATRIX11_ARB = 0x88CB; + public const int GL_MATRIX12_ARB = 0x88CC; + public const int GL_MATRIX13_ARB = 0x88CD; + public const int GL_MATRIX14_ARB = 0x88CE; + public const int GL_MATRIX15_ARB = 0x88CF; + public const int GL_MATRIX16_ARB = 0x88D0; + public const int GL_MATRIX17_ARB = 0x88D1; + public const int GL_MATRIX18_ARB = 0x88D2; + public const int GL_MATRIX19_ARB = 0x88D3; + public const int GL_MATRIX20_ARB = 0x88D4; + public const int GL_MATRIX21_ARB = 0x88D5; + public const int GL_MATRIX22_ARB = 0x88D6; + public const int GL_MATRIX23_ARB = 0x88D7; + public const int GL_MATRIX24_ARB = 0x88D8; + public const int GL_MATRIX25_ARB = 0x88D9; + public const int GL_MATRIX26_ARB = 0x88DA; + public const int GL_MATRIX27_ARB = 0x88DB; + public const int GL_MATRIX28_ARB = 0x88DC; + public const int GL_MATRIX29_ARB = 0x88DD; + public const int GL_MATRIX30_ARB = 0x88DE; + public const int GL_MATRIX31_ARB = 0x88DF; + public const int GL_ARB_fragment_program_shadow = 1; + public const int GL_ARB_fragment_shader = 1; + public const int GL_FRAGMENT_SHADER_ARB = 0x8B30; + public const int GL_MAX_FRAGMENT_UNIFORM_COMPONENTS_ARB = 0x8B49; + public const int GL_FRAGMENT_SHADER_DERIVATIVE_HINT_ARB = 0x8B8B; + public const int GL_ARB_fragment_shader_interlock = 1; + public const int GL_ARB_framebuffer_no_attachments = 1; + public const int GL_ARB_framebuffer_object = 1; + public const int GL_ARB_framebuffer_sRGB = 1; + public const int GL_ARB_geometry_shader4 = 1; + public const int GL_LINES_ADJACENCY_ARB = 0x000A; + public const int GL_LINE_STRIP_ADJACENCY_ARB = 0x000B; + public const int GL_TRIANGLES_ADJACENCY_ARB = 0x000C; + public const int GL_TRIANGLE_STRIP_ADJACENCY_ARB = 0x000D; + public const int GL_PROGRAM_POINT_SIZE_ARB = 0x8642; + public const int GL_MAX_GEOMETRY_TEXTURE_IMAGE_UNITS_ARB = 0x8C29; + public const int GL_FRAMEBUFFER_ATTACHMENT_LAYERED_ARB = 0x8DA7; + public const int GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS_ARB = 0x8DA8; + public const int GL_FRAMEBUFFER_INCOMPLETE_LAYER_COUNT_ARB = 0x8DA9; + public const int GL_GEOMETRY_SHADER_ARB = 0x8DD9; + public const int GL_GEOMETRY_VERTICES_OUT_ARB = 0x8DDA; + public const int GL_GEOMETRY_INPUT_TYPE_ARB = 0x8DDB; + public const int GL_GEOMETRY_OUTPUT_TYPE_ARB = 0x8DDC; + public const int GL_MAX_GEOMETRY_VARYING_COMPONENTS_ARB = 0x8DDD; + public const int GL_MAX_VERTEX_VARYING_COMPONENTS_ARB = 0x8DDE; + public const int GL_MAX_GEOMETRY_UNIFORM_COMPONENTS_ARB = 0x8DDF; + public const int GL_MAX_GEOMETRY_OUTPUT_VERTICES_ARB = 0x8DE0; + public const int GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS_ARB = 0x8DE1; + public const int GL_ARB_get_program_binary = 1; + public const int GL_ARB_get_texture_sub_image = 1; + public const int GL_ARB_gl_spirv = 1; + public const int GL_SHADER_BINARY_FORMAT_SPIR_V_ARB = 0x9551; + public const int GL_SPIR_V_BINARY_ARB = 0x9552; + public const int GL_ARB_gpu_shader5 = 1; + public const int GL_ARB_gpu_shader_fp64 = 1; + public const int GL_ARB_gpu_shader_int64 = 1; + public const int GL_INT64_ARB = 0x140E; + public const int GL_INT64_VEC2_ARB = 0x8FE9; + public const int GL_INT64_VEC3_ARB = 0x8FEA; + public const int GL_INT64_VEC4_ARB = 0x8FEB; + public const int GL_UNSIGNED_INT64_VEC2_ARB = 0x8FF5; + public const int GL_UNSIGNED_INT64_VEC3_ARB = 0x8FF6; + public const int GL_UNSIGNED_INT64_VEC4_ARB = 0x8FF7; + public const int GL_ARB_half_float_pixel = 1; + public const int GL_HALF_FLOAT_ARB = 0x140B; + public const int GL_ARB_half_float_vertex = 1; + public const int GL_ARB_imaging = 1; + public const int GL_ARB_indirect_parameters = 1; + public const int GL_PARAMETER_BUFFER_ARB = 0x80EE; + public const int GL_PARAMETER_BUFFER_BINDING_ARB = 0x80EF; + public const int GL_ARB_instanced_arrays = 1; + public const int GL_VERTEX_ATTRIB_ARRAY_DIVISOR_ARB = 0x88FE; + public const int GL_ARB_internalformat_query = 1; + public const int GL_ARB_internalformat_query2 = 1; + public const int GL_SRGB_DECODE_ARB = 0x8299; + public const int GL_ARB_invalidate_subdata = 1; + public const int GL_ARB_map_buffer_alignment = 1; + public const int GL_ARB_map_buffer_range = 1; + public const int GL_ARB_matrix_palette = 1; + public const int GL_MATRIX_PALETTE_ARB = 0x8840; + public const int GL_MAX_MATRIX_PALETTE_STACK_DEPTH_ARB = 0x8841; + public const int GL_MAX_PALETTE_MATRICES_ARB = 0x8842; + public const int GL_CURRENT_PALETTE_MATRIX_ARB = 0x8843; + public const int GL_MATRIX_INDEX_ARRAY_ARB = 0x8844; + public const int GL_CURRENT_MATRIX_INDEX_ARB = 0x8845; + public const int GL_MATRIX_INDEX_ARRAY_SIZE_ARB = 0x8846; + public const int GL_MATRIX_INDEX_ARRAY_TYPE_ARB = 0x8847; + public const int GL_MATRIX_INDEX_ARRAY_STRIDE_ARB = 0x8848; + public const int GL_MATRIX_INDEX_ARRAY_POINTER_ARB = 0x8849; + public const int GL_ARB_multi_bind = 1; + public const int GL_ARB_multi_draw_indirect = 1; + public const int GL_ARB_multisample = 1; + public const int GL_MULTISAMPLE_ARB = 0x809D; + public const int GL_SAMPLE_ALPHA_TO_COVERAGE_ARB = 0x809E; + public const int GL_SAMPLE_ALPHA_TO_ONE_ARB = 0x809F; + public const int GL_SAMPLE_COVERAGE_ARB = 0x80A0; + public const int GL_SAMPLE_BUFFERS_ARB = 0x80A8; + public const int GL_SAMPLES_ARB = 0x80A9; + public const int GL_SAMPLE_COVERAGE_VALUE_ARB = 0x80AA; + public const int GL_SAMPLE_COVERAGE_INVERT_ARB = 0x80AB; + public const int GL_MULTISAMPLE_BIT_ARB = 0x20000000; + public const int GL_ARB_occlusion_query = 1; + public const int GL_QUERY_COUNTER_BITS_ARB = 0x8864; + public const int GL_CURRENT_QUERY_ARB = 0x8865; + public const int GL_QUERY_RESULT_ARB = 0x8866; + public const int GL_QUERY_RESULT_AVAILABLE_ARB = 0x8867; + public const int GL_SAMPLES_PASSED_ARB = 0x8914; + public const int GL_ARB_occlusion_query2 = 1; + public const int GL_ARB_parallel_shader_compile = 1; + public const int GL_MAX_SHADER_COMPILER_THREADS_ARB = 0x91B0; + public const int GL_COMPLETION_STATUS_ARB = 0x91B1; + public const int GL_ARB_pipeline_statistics_query = 1; + public const int GL_VERTICES_SUBMITTED_ARB = 0x82EE; + public const int GL_PRIMITIVES_SUBMITTED_ARB = 0x82EF; + public const int GL_VERTEX_SHADER_INVOCATIONS_ARB = 0x82F0; + public const int GL_TESS_CONTROL_SHADER_PATCHES_ARB = 0x82F1; + public const int GL_TESS_EVALUATION_SHADER_INVOCATIONS_ARB = 0x82F2; + public const int GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED_ARB = 0x82F3; + public const int GL_FRAGMENT_SHADER_INVOCATIONS_ARB = 0x82F4; + public const int GL_COMPUTE_SHADER_INVOCATIONS_ARB = 0x82F5; + public const int GL_CLIPPING_INPUT_PRIMITIVES_ARB = 0x82F6; + public const int GL_CLIPPING_OUTPUT_PRIMITIVES_ARB = 0x82F7; + public const int GL_ARB_pixel_buffer_object = 1; + public const int GL_PIXEL_PACK_BUFFER_ARB = 0x88EB; + public const int GL_PIXEL_UNPACK_BUFFER_ARB = 0x88EC; + public const int GL_PIXEL_PACK_BUFFER_BINDING_ARB = 0x88ED; + public const int GL_PIXEL_UNPACK_BUFFER_BINDING_ARB = 0x88EF; + public const int GL_ARB_point_parameters = 1; + public const int GL_POINT_SIZE_MIN_ARB = 0x8126; + public const int GL_POINT_SIZE_MAX_ARB = 0x8127; + public const int GL_POINT_FADE_THRESHOLD_SIZE_ARB = 0x8128; + public const int GL_POINT_DISTANCE_ATTENUATION_ARB = 0x8129; + public const int GL_ARB_point_sprite = 1; + public const int GL_POINT_SPRITE_ARB = 0x8861; + public const int GL_COORD_REPLACE_ARB = 0x8862; + public const int GL_ARB_polygon_offset_clamp = 1; + public const int GL_ARB_post_depth_coverage = 1; + public const int GL_ARB_program_interface_query = 1; + public const int GL_ARB_provoking_vertex = 1; + public const int GL_ARB_query_buffer_object = 1; + public const int GL_ARB_robust_buffer_access_behavior = 1; + public const int GL_ARB_robustness = 1; + public const int GL_CONTEXT_FLAG_ROBUST_ACCESS_BIT_ARB = 0x00000004; + public const int GL_LOSE_CONTEXT_ON_RESET_ARB = 0x8252; + public const int GL_GUILTY_CONTEXT_RESET_ARB = 0x8253; + public const int GL_INNOCENT_CONTEXT_RESET_ARB = 0x8254; + public const int GL_UNKNOWN_CONTEXT_RESET_ARB = 0x8255; + public const int GL_RESET_NOTIFICATION_STRATEGY_ARB = 0x8256; + public const int GL_NO_RESET_NOTIFICATION_ARB = 0x8261; + public const int GL_ARB_robustness_isolation = 1; + public const int GL_ARB_sample_locations = 1; + public const int GL_SAMPLE_LOCATION_SUBPIXEL_BITS_ARB = 0x933D; + public const int GL_SAMPLE_LOCATION_PIXEL_GRID_WIDTH_ARB = 0x933E; + public const int GL_SAMPLE_LOCATION_PIXEL_GRID_HEIGHT_ARB = 0x933F; + public const int GL_PROGRAMMABLE_SAMPLE_LOCATION_TABLE_SIZE_ARB = 0x9340; + public const int GL_SAMPLE_LOCATION_ARB = 0x8E50; + public const int GL_PROGRAMMABLE_SAMPLE_LOCATION_ARB = 0x9341; + public const int GL_FRAMEBUFFER_PROGRAMMABLE_SAMPLE_LOCATIONS_ARB = 0x9342; + public const int GL_FRAMEBUFFER_SAMPLE_LOCATION_PIXEL_GRID_ARB = 0x9343; + public const int GL_ARB_sample_shading = 1; + public const int GL_SAMPLE_SHADING_ARB = 0x8C36; + public const int GL_MIN_SAMPLE_SHADING_VALUE_ARB = 0x8C37; + public const int GL_ARB_sampler_objects = 1; + public const int GL_ARB_seamless_cube_map = 1; + public const int GL_ARB_seamless_cubemap_per_texture = 1; + public const int GL_ARB_separate_shader_objects = 1; + public const int GL_ARB_shader_atomic_counter_ops = 1; + public const int GL_ARB_shader_atomic_counters = 1; + public const int GL_ARB_shader_ballot = 1; + public const int GL_ARB_shader_bit_encoding = 1; + public const int GL_ARB_shader_clock = 1; + public const int GL_ARB_shader_draw_parameters = 1; + public const int GL_ARB_shader_group_vote = 1; + public const int GL_ARB_shader_image_load_store = 1; + public const int GL_ARB_shader_image_size = 1; + public const int GL_ARB_shader_objects = 1; + public const int GL_PROGRAM_OBJECT_ARB = 0x8B40; + public const int GL_SHADER_OBJECT_ARB = 0x8B48; + public const int GL_OBJECT_TYPE_ARB = 0x8B4E; + public const int GL_OBJECT_SUBTYPE_ARB = 0x8B4F; + public const int GL_FLOAT_VEC2_ARB = 0x8B50; + public const int GL_FLOAT_VEC3_ARB = 0x8B51; + public const int GL_FLOAT_VEC4_ARB = 0x8B52; + public const int GL_INT_VEC2_ARB = 0x8B53; + public const int GL_INT_VEC3_ARB = 0x8B54; + public const int GL_INT_VEC4_ARB = 0x8B55; + public const int GL_BOOL_ARB = 0x8B56; + public const int GL_BOOL_VEC2_ARB = 0x8B57; + public const int GL_BOOL_VEC3_ARB = 0x8B58; + public const int GL_BOOL_VEC4_ARB = 0x8B59; + public const int GL_FLOAT_MAT2_ARB = 0x8B5A; + public const int GL_FLOAT_MAT3_ARB = 0x8B5B; + public const int GL_FLOAT_MAT4_ARB = 0x8B5C; + public const int GL_SAMPLER_1D_ARB = 0x8B5D; + public const int GL_SAMPLER_2D_ARB = 0x8B5E; + public const int GL_SAMPLER_3D_ARB = 0x8B5F; + public const int GL_SAMPLER_CUBE_ARB = 0x8B60; + public const int GL_SAMPLER_1D_SHADOW_ARB = 0x8B61; + public const int GL_SAMPLER_2D_SHADOW_ARB = 0x8B62; + public const int GL_SAMPLER_2D_RECT_ARB = 0x8B63; + public const int GL_SAMPLER_2D_RECT_SHADOW_ARB = 0x8B64; + public const int GL_OBJECT_DELETE_STATUS_ARB = 0x8B80; + public const int GL_OBJECT_COMPILE_STATUS_ARB = 0x8B81; + public const int GL_OBJECT_LINK_STATUS_ARB = 0x8B82; + public const int GL_OBJECT_VALIDATE_STATUS_ARB = 0x8B83; + public const int GL_OBJECT_INFO_LOG_LENGTH_ARB = 0x8B84; + public const int GL_OBJECT_ATTACHED_OBJECTS_ARB = 0x8B85; + public const int GL_OBJECT_ACTIVE_UNIFORMS_ARB = 0x8B86; + public const int GL_OBJECT_ACTIVE_UNIFORM_MAX_LENGTH_ARB = 0x8B87; + public const int GL_OBJECT_SHADER_SOURCE_LENGTH_ARB = 0x8B88; + public const int GL_ARB_shader_precision = 1; + public const int GL_ARB_shader_stencil_export = 1; + public const int GL_ARB_shader_storage_buffer_object = 1; + public const int GL_ARB_shader_subroutine = 1; + public const int GL_ARB_shader_texture_image_samples = 1; + public const int GL_ARB_shader_texture_lod = 1; + public const int GL_ARB_shader_viewport_layer_array = 1; + public const int GL_ARB_shading_language_100 = 1; + public const int GL_SHADING_LANGUAGE_VERSION_ARB = 0x8B8C; + public const int GL_ARB_shading_language_420pack = 1; + public const int GL_ARB_shading_language_include = 1; + public const int GL_SHADER_INCLUDE_ARB = 0x8DAE; + public const int GL_NAMED_STRING_LENGTH_ARB = 0x8DE9; + public const int GL_NAMED_STRING_TYPE_ARB = 0x8DEA; + public const int GL_ARB_shading_language_packing = 1; + public const int GL_ARB_shadow = 1; + public const int GL_TEXTURE_COMPARE_MODE_ARB = 0x884C; + public const int GL_TEXTURE_COMPARE_FUNC_ARB = 0x884D; + public const int GL_COMPARE_R_TO_TEXTURE_ARB = 0x884E; + public const int GL_ARB_shadow_ambient = 1; + public const int GL_TEXTURE_COMPARE_FAIL_VALUE_ARB = 0x80BF; + public const int GL_ARB_sparse_buffer = 1; + public const int GL_SPARSE_STORAGE_BIT_ARB = 0x0400; + public const int GL_SPARSE_BUFFER_PAGE_SIZE_ARB = 0x82F8; + public const int GL_ARB_sparse_texture = 1; + public const int GL_TEXTURE_SPARSE_ARB = 0x91A6; + public const int GL_VIRTUAL_PAGE_SIZE_INDEX_ARB = 0x91A7; + public const int GL_NUM_SPARSE_LEVELS_ARB = 0x91AA; + public const int GL_NUM_VIRTUAL_PAGE_SIZES_ARB = 0x91A8; + public const int GL_VIRTUAL_PAGE_SIZE_X_ARB = 0x9195; + public const int GL_VIRTUAL_PAGE_SIZE_Y_ARB = 0x9196; + public const int GL_VIRTUAL_PAGE_SIZE_Z_ARB = 0x9197; + public const int GL_MAX_SPARSE_TEXTURE_SIZE_ARB = 0x9198; + public const int GL_MAX_SPARSE_3D_TEXTURE_SIZE_ARB = 0x9199; + public const int GL_MAX_SPARSE_ARRAY_TEXTURE_LAYERS_ARB = 0x919A; + public const int GL_SPARSE_TEXTURE_FULL_ARRAY_CUBE_MIPMAPS_ARB = 0x91A9; + public const int GL_ARB_sparse_texture2 = 1; + public const int GL_ARB_sparse_texture_clamp = 1; + public const int GL_ARB_spirv_extensions = 1; + public const int GL_ARB_stencil_texturing = 1; + public const int GL_ARB_sync = 1; + public const int GL_ARB_tessellation_shader = 1; + public const int GL_ARB_texture_barrier = 1; + public const int GL_ARB_texture_border_clamp = 1; + public const int GL_CLAMP_TO_BORDER_ARB = 0x812D; + public const int GL_ARB_texture_buffer_object = 1; + public const int GL_TEXTURE_BUFFER_ARB = 0x8C2A; + public const int GL_MAX_TEXTURE_BUFFER_SIZE_ARB = 0x8C2B; + public const int GL_TEXTURE_BINDING_BUFFER_ARB = 0x8C2C; + public const int GL_TEXTURE_BUFFER_DATA_STORE_BINDING_ARB = 0x8C2D; + public const int GL_TEXTURE_BUFFER_FORMAT_ARB = 0x8C2E; + public const int GL_ARB_texture_buffer_object_rgb32 = 1; + public const int GL_ARB_texture_buffer_range = 1; + public const int GL_ARB_texture_compression = 1; + public const int GL_COMPRESSED_ALPHA_ARB = 0x84E9; + public const int GL_COMPRESSED_LUMINANCE_ARB = 0x84EA; + public const int GL_COMPRESSED_LUMINANCE_ALPHA_ARB = 0x84EB; + public const int GL_COMPRESSED_INTENSITY_ARB = 0x84EC; + public const int GL_COMPRESSED_RGB_ARB = 0x84ED; + public const int GL_COMPRESSED_RGBA_ARB = 0x84EE; + public const int GL_TEXTURE_COMPRESSION_HINT_ARB = 0x84EF; + public const int GL_TEXTURE_COMPRESSED_IMAGE_SIZE_ARB = 0x86A0; + public const int GL_TEXTURE_COMPRESSED_ARB = 0x86A1; + public const int GL_NUM_COMPRESSED_TEXTURE_FORMATS_ARB = 0x86A2; + public const int GL_COMPRESSED_TEXTURE_FORMATS_ARB = 0x86A3; + public const int GL_ARB_texture_compression_bptc = 1; + public const int GL_COMPRESSED_RGBA_BPTC_UNORM_ARB = 0x8E8C; + public const int GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM_ARB = 0x8E8D; + public const int GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT_ARB = 0x8E8E; + public const int GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_ARB = 0x8E8F; + public const int GL_ARB_texture_compression_rgtc = 1; + public const int GL_ARB_texture_cube_map = 1; + public const int GL_NORMAL_MAP_ARB = 0x8511; + public const int GL_REFLECTION_MAP_ARB = 0x8512; + public const int GL_TEXTURE_CUBE_MAP_ARB = 0x8513; + public const int GL_TEXTURE_BINDING_CUBE_MAP_ARB = 0x8514; + public const int GL_TEXTURE_CUBE_MAP_POSITIVE_X_ARB = 0x8515; + public const int GL_TEXTURE_CUBE_MAP_NEGATIVE_X_ARB = 0x8516; + public const int GL_TEXTURE_CUBE_MAP_POSITIVE_Y_ARB = 0x8517; + public const int GL_TEXTURE_CUBE_MAP_NEGATIVE_Y_ARB = 0x8518; + public const int GL_TEXTURE_CUBE_MAP_POSITIVE_Z_ARB = 0x8519; + public const int GL_TEXTURE_CUBE_MAP_NEGATIVE_Z_ARB = 0x851A; + public const int GL_PROXY_TEXTURE_CUBE_MAP_ARB = 0x851B; + public const int GL_MAX_CUBE_MAP_TEXTURE_SIZE_ARB = 0x851C; + public const int GL_ARB_texture_cube_map_array = 1; + public const int GL_TEXTURE_CUBE_MAP_ARRAY_ARB = 0x9009; + public const int GL_TEXTURE_BINDING_CUBE_MAP_ARRAY_ARB = 0x900A; + public const int GL_PROXY_TEXTURE_CUBE_MAP_ARRAY_ARB = 0x900B; + public const int GL_SAMPLER_CUBE_MAP_ARRAY_ARB = 0x900C; + public const int GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW_ARB = 0x900D; + public const int GL_INT_SAMPLER_CUBE_MAP_ARRAY_ARB = 0x900E; + public const int GL_UNSIGNED_INT_SAMPLER_CUBE_MAP_ARRAY_ARB = 0x900F; + public const int GL_ARB_texture_env_add = 1; + public const int GL_ARB_texture_env_combine = 1; + public const int GL_COMBINE_ARB = 0x8570; + public const int GL_COMBINE_RGB_ARB = 0x8571; + public const int GL_COMBINE_ALPHA_ARB = 0x8572; + public const int GL_SOURCE0_RGB_ARB = 0x8580; + public const int GL_SOURCE1_RGB_ARB = 0x8581; + public const int GL_SOURCE2_RGB_ARB = 0x8582; + public const int GL_SOURCE0_ALPHA_ARB = 0x8588; + public const int GL_SOURCE1_ALPHA_ARB = 0x8589; + public const int GL_SOURCE2_ALPHA_ARB = 0x858A; + public const int GL_OPERAND0_RGB_ARB = 0x8590; + public const int GL_OPERAND1_RGB_ARB = 0x8591; + public const int GL_OPERAND2_RGB_ARB = 0x8592; + public const int GL_OPERAND0_ALPHA_ARB = 0x8598; + public const int GL_OPERAND1_ALPHA_ARB = 0x8599; + public const int GL_OPERAND2_ALPHA_ARB = 0x859A; + public const int GL_RGB_SCALE_ARB = 0x8573; + public const int GL_ADD_SIGNED_ARB = 0x8574; + public const int GL_INTERPOLATE_ARB = 0x8575; + public const int GL_SUBTRACT_ARB = 0x84E7; + public const int GL_CONSTANT_ARB = 0x8576; + public const int GL_PRIMARY_COLOR_ARB = 0x8577; + public const int GL_PREVIOUS_ARB = 0x8578; + public const int GL_ARB_texture_env_crossbar = 1; + public const int GL_ARB_texture_env_dot3 = 1; + public const int GL_DOT3_RGB_ARB = 0x86AE; + public const int GL_DOT3_RGBA_ARB = 0x86AF; + public const int GL_ARB_texture_filter_anisotropic = 1; + public const int GL_ARB_texture_filter_minmax = 1; + public const int GL_TEXTURE_REDUCTION_MODE_ARB = 0x9366; + public const int GL_WEIGHTED_AVERAGE_ARB = 0x9367; + public const int GL_ARB_texture_float = 1; + public const int GL_TEXTURE_RED_TYPE_ARB = 0x8C10; + public const int GL_TEXTURE_GREEN_TYPE_ARB = 0x8C11; + public const int GL_TEXTURE_BLUE_TYPE_ARB = 0x8C12; + public const int GL_TEXTURE_ALPHA_TYPE_ARB = 0x8C13; + public const int GL_TEXTURE_LUMINANCE_TYPE_ARB = 0x8C14; + public const int GL_TEXTURE_INTENSITY_TYPE_ARB = 0x8C15; + public const int GL_TEXTURE_DEPTH_TYPE_ARB = 0x8C16; + public const int GL_UNSIGNED_NORMALIZED_ARB = 0x8C17; + public const int GL_RGBA32F_ARB = 0x8814; + public const int GL_RGB32F_ARB = 0x8815; + public const int GL_ALPHA32F_ARB = 0x8816; + public const int GL_INTENSITY32F_ARB = 0x8817; + public const int GL_LUMINANCE32F_ARB = 0x8818; + public const int GL_LUMINANCE_ALPHA32F_ARB = 0x8819; + public const int GL_RGBA16F_ARB = 0x881A; + public const int GL_RGB16F_ARB = 0x881B; + public const int GL_ALPHA16F_ARB = 0x881C; + public const int GL_INTENSITY16F_ARB = 0x881D; + public const int GL_LUMINANCE16F_ARB = 0x881E; + public const int GL_LUMINANCE_ALPHA16F_ARB = 0x881F; + public const int GL_ARB_texture_gather = 1; + public const int GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET_ARB = 0x8E5E; + public const int GL_MAX_PROGRAM_TEXTURE_GATHER_OFFSET_ARB = 0x8E5F; + public const int GL_MAX_PROGRAM_TEXTURE_GATHER_COMPONENTS_ARB = 0x8F9F; + public const int GL_ARB_texture_mirror_clamp_to_edge = 1; + public const int GL_ARB_texture_mirrored_repeat = 1; + public const int GL_MIRRORED_REPEAT_ARB = 0x8370; + public const int GL_ARB_texture_multisample = 1; + public const int GL_ARB_texture_non_power_of_two = 1; + public const int GL_ARB_texture_query_levels = 1; + public const int GL_ARB_texture_query_lod = 1; + public const int GL_ARB_texture_rectangle = 1; + public const int GL_TEXTURE_RECTANGLE_ARB = 0x84F5; + public const int GL_TEXTURE_BINDING_RECTANGLE_ARB = 0x84F6; + public const int GL_PROXY_TEXTURE_RECTANGLE_ARB = 0x84F7; + public const int GL_MAX_RECTANGLE_TEXTURE_SIZE_ARB = 0x84F8; + public const int GL_ARB_texture_rg = 1; + public const int GL_ARB_texture_rgb10_a2ui = 1; + public const int GL_ARB_texture_stencil8 = 1; + public const int GL_ARB_texture_storage = 1; + public const int GL_ARB_texture_storage_multisample = 1; + public const int GL_ARB_texture_swizzle = 1; + public const int GL_ARB_texture_view = 1; + public const int GL_ARB_timer_query = 1; + public const int GL_ARB_transform_feedback2 = 1; + public const int GL_ARB_transform_feedback3 = 1; + public const int GL_ARB_transform_feedback_instanced = 1; + public const int GL_ARB_transform_feedback_overflow_query = 1; + public const int GL_TRANSFORM_FEEDBACK_OVERFLOW_ARB = 0x82EC; + public const int GL_TRANSFORM_FEEDBACK_STREAM_OVERFLOW_ARB = 0x82ED; + public const int GL_ARB_transpose_matrix = 1; + public const int GL_TRANSPOSE_MODELVIEW_MATRIX_ARB = 0x84E3; + public const int GL_TRANSPOSE_PROJECTION_MATRIX_ARB = 0x84E4; + public const int GL_TRANSPOSE_TEXTURE_MATRIX_ARB = 0x84E5; + public const int GL_TRANSPOSE_COLOR_MATRIX_ARB = 0x84E6; + public const int GL_ARB_uniform_buffer_object = 1; + public const int GL_ARB_vertex_array_bgra = 1; + public const int GL_ARB_vertex_array_object = 1; + public const int GL_ARB_vertex_attrib_64bit = 1; + public const int GL_ARB_vertex_attrib_binding = 1; + public const int GL_ARB_vertex_blend = 1; + public const int GL_MAX_VERTEX_UNITS_ARB = 0x86A4; + public const int GL_ACTIVE_VERTEX_UNITS_ARB = 0x86A5; + public const int GL_WEIGHT_SUM_UNITY_ARB = 0x86A6; + public const int GL_VERTEX_BLEND_ARB = 0x86A7; + public const int GL_CURRENT_WEIGHT_ARB = 0x86A8; + public const int GL_WEIGHT_ARRAY_TYPE_ARB = 0x86A9; + public const int GL_WEIGHT_ARRAY_STRIDE_ARB = 0x86AA; + public const int GL_WEIGHT_ARRAY_SIZE_ARB = 0x86AB; + public const int GL_WEIGHT_ARRAY_POINTER_ARB = 0x86AC; + public const int GL_WEIGHT_ARRAY_ARB = 0x86AD; + public const int GL_MODELVIEW0_ARB = 0x1700; + public const int GL_MODELVIEW1_ARB = 0x850A; + public const int GL_MODELVIEW2_ARB = 0x8722; + public const int GL_MODELVIEW3_ARB = 0x8723; + public const int GL_MODELVIEW4_ARB = 0x8724; + public const int GL_MODELVIEW5_ARB = 0x8725; + public const int GL_MODELVIEW6_ARB = 0x8726; + public const int GL_MODELVIEW7_ARB = 0x8727; + public const int GL_MODELVIEW8_ARB = 0x8728; + public const int GL_MODELVIEW9_ARB = 0x8729; + public const int GL_MODELVIEW10_ARB = 0x872A; + public const int GL_MODELVIEW11_ARB = 0x872B; + public const int GL_MODELVIEW12_ARB = 0x872C; + public const int GL_MODELVIEW13_ARB = 0x872D; + public const int GL_MODELVIEW14_ARB = 0x872E; + public const int GL_MODELVIEW15_ARB = 0x872F; + public const int GL_MODELVIEW16_ARB = 0x8730; + public const int GL_MODELVIEW17_ARB = 0x8731; + public const int GL_MODELVIEW18_ARB = 0x8732; + public const int GL_MODELVIEW19_ARB = 0x8733; + public const int GL_MODELVIEW20_ARB = 0x8734; + public const int GL_MODELVIEW21_ARB = 0x8735; + public const int GL_MODELVIEW22_ARB = 0x8736; + public const int GL_MODELVIEW23_ARB = 0x8737; + public const int GL_MODELVIEW24_ARB = 0x8738; + public const int GL_MODELVIEW25_ARB = 0x8739; + public const int GL_MODELVIEW26_ARB = 0x873A; + public const int GL_MODELVIEW27_ARB = 0x873B; + public const int GL_MODELVIEW28_ARB = 0x873C; + public const int GL_MODELVIEW29_ARB = 0x873D; + public const int GL_MODELVIEW30_ARB = 0x873E; + public const int GL_MODELVIEW31_ARB = 0x873F; + public const int GL_ARB_vertex_buffer_object = 1; + public const int GL_BUFFER_SIZE_ARB = 0x8764; + public const int GL_BUFFER_USAGE_ARB = 0x8765; + public const int GL_ARRAY_BUFFER_ARB = 0x8892; + public const int GL_ELEMENT_ARRAY_BUFFER_ARB = 0x8893; + public const int GL_ARRAY_BUFFER_BINDING_ARB = 0x8894; + public const int GL_ELEMENT_ARRAY_BUFFER_BINDING_ARB = 0x8895; + public const int GL_VERTEX_ARRAY_BUFFER_BINDING_ARB = 0x8896; + public const int GL_NORMAL_ARRAY_BUFFER_BINDING_ARB = 0x8897; + public const int GL_COLOR_ARRAY_BUFFER_BINDING_ARB = 0x8898; + public const int GL_INDEX_ARRAY_BUFFER_BINDING_ARB = 0x8899; + public const int GL_TEXTURE_COORD_ARRAY_BUFFER_BINDING_ARB = 0x889A; + public const int GL_EDGE_FLAG_ARRAY_BUFFER_BINDING_ARB = 0x889B; + public const int GL_SECONDARY_COLOR_ARRAY_BUFFER_BINDING_ARB = 0x889C; + public const int GL_FOG_COORDINATE_ARRAY_BUFFER_BINDING_ARB = 0x889D; + public const int GL_WEIGHT_ARRAY_BUFFER_BINDING_ARB = 0x889E; + public const int GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING_ARB = 0x889F; + public const int GL_READ_ONLY_ARB = 0x88B8; + public const int GL_WRITE_ONLY_ARB = 0x88B9; + public const int GL_READ_WRITE_ARB = 0x88BA; + public const int GL_BUFFER_ACCESS_ARB = 0x88BB; + public const int GL_BUFFER_MAPPED_ARB = 0x88BC; + public const int GL_BUFFER_MAP_POINTER_ARB = 0x88BD; + public const int GL_STREAM_DRAW_ARB = 0x88E0; + public const int GL_STREAM_READ_ARB = 0x88E1; + public const int GL_STREAM_COPY_ARB = 0x88E2; + public const int GL_STATIC_DRAW_ARB = 0x88E4; + public const int GL_STATIC_READ_ARB = 0x88E5; + public const int GL_STATIC_COPY_ARB = 0x88E6; + public const int GL_DYNAMIC_DRAW_ARB = 0x88E8; + public const int GL_DYNAMIC_READ_ARB = 0x88E9; + public const int GL_DYNAMIC_COPY_ARB = 0x88EA; + public const int GL_ARB_vertex_program = 1; + public const int GL_COLOR_SUM_ARB = 0x8458; + public const int GL_VERTEX_PROGRAM_ARB = 0x8620; + public const int GL_VERTEX_ATTRIB_ARRAY_ENABLED_ARB = 0x8622; + public const int GL_VERTEX_ATTRIB_ARRAY_SIZE_ARB = 0x8623; + public const int GL_VERTEX_ATTRIB_ARRAY_STRIDE_ARB = 0x8624; + public const int GL_VERTEX_ATTRIB_ARRAY_TYPE_ARB = 0x8625; + public const int GL_CURRENT_VERTEX_ATTRIB_ARB = 0x8626; + public const int GL_VERTEX_PROGRAM_POINT_SIZE_ARB = 0x8642; + public const int GL_VERTEX_PROGRAM_TWO_SIDE_ARB = 0x8643; + public const int GL_VERTEX_ATTRIB_ARRAY_POINTER_ARB = 0x8645; + public const int GL_MAX_VERTEX_ATTRIBS_ARB = 0x8869; + public const int GL_VERTEX_ATTRIB_ARRAY_NORMALIZED_ARB = 0x886A; + public const int GL_PROGRAM_ADDRESS_REGISTERS_ARB = 0x88B0; + public const int GL_MAX_PROGRAM_ADDRESS_REGISTERS_ARB = 0x88B1; + public const int GL_PROGRAM_NATIVE_ADDRESS_REGISTERS_ARB = 0x88B2; + public const int GL_MAX_PROGRAM_NATIVE_ADDRESS_REGISTERS_ARB = 0x88B3; + public const int GL_ARB_vertex_shader = 1; + public const int GL_VERTEX_SHADER_ARB = 0x8B31; + public const int GL_MAX_VERTEX_UNIFORM_COMPONENTS_ARB = 0x8B4A; + public const int GL_MAX_VARYING_FLOATS_ARB = 0x8B4B; + public const int GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS_ARB = 0x8B4C; + public const int GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS_ARB = 0x8B4D; + public const int GL_OBJECT_ACTIVE_ATTRIBUTES_ARB = 0x8B89; + public const int GL_OBJECT_ACTIVE_ATTRIBUTE_MAX_LENGTH_ARB = 0x8B8A; + public const int GL_ARB_vertex_type_10f_11f_11f_rev = 1; + public const int GL_ARB_vertex_type_2_10_10_10_rev = 1; + public const int GL_ARB_viewport_array = 1; + public const int GL_ARB_window_pos = 1; + public const int GL_KHR_blend_equation_advanced = 1; + public const int GL_MULTIPLY_KHR = 0x9294; + public const int GL_SCREEN_KHR = 0x9295; + public const int GL_OVERLAY_KHR = 0x9296; + public const int GL_DARKEN_KHR = 0x9297; + public const int GL_LIGHTEN_KHR = 0x9298; + public const int GL_COLORDODGE_KHR = 0x9299; + public const int GL_COLORBURN_KHR = 0x929A; + public const int GL_HARDLIGHT_KHR = 0x929B; + public const int GL_SOFTLIGHT_KHR = 0x929C; + public const int GL_DIFFERENCE_KHR = 0x929E; + public const int GL_EXCLUSION_KHR = 0x92A0; + public const int GL_HSL_HUE_KHR = 0x92AD; + public const int GL_HSL_SATURATION_KHR = 0x92AE; + public const int GL_HSL_COLOR_KHR = 0x92AF; + public const int GL_HSL_LUMINOSITY_KHR = 0x92B0; + public const int GL_KHR_blend_equation_advanced_coherent = 1; + public const int GL_BLEND_ADVANCED_COHERENT_KHR = 0x9285; + public const int GL_KHR_context_flush_control = 1; + public const int GL_KHR_debug = 1; + public const int GL_KHR_no_error = 1; + public const int GL_CONTEXT_FLAG_NO_ERROR_BIT_KHR = 0x00000008; + public const int GL_KHR_parallel_shader_compile = 1; + public const int GL_MAX_SHADER_COMPILER_THREADS_KHR = 0x91B0; + public const int GL_COMPLETION_STATUS_KHR = 0x91B1; + public const int GL_KHR_robust_buffer_access_behavior = 1; + public const int GL_KHR_robustness = 1; + public const int GL_CONTEXT_ROBUST_ACCESS = 0x90F3; + public const int GL_KHR_texture_compression_astc_hdr = 1; + public const int GL_COMPRESSED_RGBA_ASTC_4x4_KHR = 0x93B0; + public const int GL_COMPRESSED_RGBA_ASTC_5x4_KHR = 0x93B1; + public const int GL_COMPRESSED_RGBA_ASTC_5x5_KHR = 0x93B2; + public const int GL_COMPRESSED_RGBA_ASTC_6x5_KHR = 0x93B3; + public const int GL_COMPRESSED_RGBA_ASTC_6x6_KHR = 0x93B4; + public const int GL_COMPRESSED_RGBA_ASTC_8x5_KHR = 0x93B5; + public const int GL_COMPRESSED_RGBA_ASTC_8x6_KHR = 0x93B6; + public const int GL_COMPRESSED_RGBA_ASTC_8x8_KHR = 0x93B7; + public const int GL_COMPRESSED_RGBA_ASTC_10x5_KHR = 0x93B8; + public const int GL_COMPRESSED_RGBA_ASTC_10x6_KHR = 0x93B9; + public const int GL_COMPRESSED_RGBA_ASTC_10x8_KHR = 0x93BA; + public const int GL_COMPRESSED_RGBA_ASTC_10x10_KHR = 0x93BB; + public const int GL_COMPRESSED_RGBA_ASTC_12x10_KHR = 0x93BC; + public const int GL_COMPRESSED_RGBA_ASTC_12x12_KHR = 0x93BD; + public const int GL_COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR = 0x93D0; + public const int GL_COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR = 0x93D1; + public const int GL_COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR = 0x93D2; + public const int GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR = 0x93D3; + public const int GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR = 0x93D4; + public const int GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR = 0x93D5; + public const int GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR = 0x93D6; + public const int GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR = 0x93D7; + public const int GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR = 0x93D8; + public const int GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR = 0x93D9; + public const int GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR = 0x93DA; + public const int GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR = 0x93DB; + public const int GL_COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR = 0x93DC; + public const int GL_COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR = 0x93DD; + public const int GL_KHR_texture_compression_astc_ldr = 1; + public const int GL_KHR_texture_compression_astc_sliced_3d = 1; + public const int GL_OES_byte_coordinates = 1; + public const int GL_OES_compressed_paletted_texture = 1; + public const int GL_PALETTE4_RGB8_OES = 0x8B90; + public const int GL_PALETTE4_RGBA8_OES = 0x8B91; + public const int GL_PALETTE4_R5_G6_B5_OES = 0x8B92; + public const int GL_PALETTE4_RGBA4_OES = 0x8B93; + public const int GL_PALETTE4_RGB5_A1_OES = 0x8B94; + public const int GL_PALETTE8_RGB8_OES = 0x8B95; + public const int GL_PALETTE8_RGBA8_OES = 0x8B96; + public const int GL_PALETTE8_R5_G6_B5_OES = 0x8B97; + public const int GL_PALETTE8_RGBA4_OES = 0x8B98; + public const int GL_PALETTE8_RGB5_A1_OES = 0x8B99; + public const int GL_OES_fixed_point = 1; + public const int GL_FIXED_OES = 0x140C; + public const int GL_OES_query_matrix = 1; + public const int GL_OES_read_format = 1; + public const int GL_IMPLEMENTATION_COLOR_READ_TYPE_OES = 0x8B9A; + public const int GL_IMPLEMENTATION_COLOR_READ_FORMAT_OES = 0x8B9B; + public const int GL_OES_single_precision = 1; + public const int GL_3DFX_multisample = 1; + public const int GL_MULTISAMPLE_3DFX = 0x86B2; + public const int GL_SAMPLE_BUFFERS_3DFX = 0x86B3; + public const int GL_SAMPLES_3DFX = 0x86B4; + public const int GL_MULTISAMPLE_BIT_3DFX = 0x20000000; + public const int GL_3DFX_tbuffer = 1; + public const int GL_3DFX_texture_compression_FXT1 = 1; + public const int GL_COMPRESSED_RGB_FXT1_3DFX = 0x86B0; + public const int GL_COMPRESSED_RGBA_FXT1_3DFX = 0x86B1; + public const int GL_AMD_blend_minmax_factor = 1; + public const int GL_FACTOR_MIN_AMD = 0x901C; + public const int GL_FACTOR_MAX_AMD = 0x901D; + public const int GL_AMD_conservative_depth = 1; + public const int GL_AMD_debug_output = 1; + public const int GL_MAX_DEBUG_MESSAGE_LENGTH_AMD = 0x9143; + public const int GL_MAX_DEBUG_LOGGED_MESSAGES_AMD = 0x9144; + public const int GL_DEBUG_LOGGED_MESSAGES_AMD = 0x9145; + public const int GL_DEBUG_SEVERITY_HIGH_AMD = 0x9146; + public const int GL_DEBUG_SEVERITY_MEDIUM_AMD = 0x9147; + public const int GL_DEBUG_SEVERITY_LOW_AMD = 0x9148; + public const int GL_DEBUG_CATEGORY_API_ERROR_AMD = 0x9149; + public const int GL_DEBUG_CATEGORY_WINDOW_SYSTEM_AMD = 0x914A; + public const int GL_DEBUG_CATEGORY_DEPRECATION_AMD = 0x914B; + public const int GL_DEBUG_CATEGORY_UNDEFINED_BEHAVIOR_AMD = 0x914C; + public const int GL_DEBUG_CATEGORY_PERFORMANCE_AMD = 0x914D; + public const int GL_DEBUG_CATEGORY_SHADER_COMPILER_AMD = 0x914E; + public const int GL_DEBUG_CATEGORY_APPLICATION_AMD = 0x914F; + public const int GL_DEBUG_CATEGORY_OTHER_AMD = 0x9150; + public const int GL_AMD_depth_clamp_separate = 1; + public const int GL_DEPTH_CLAMP_NEAR_AMD = 0x901E; + public const int GL_DEPTH_CLAMP_FAR_AMD = 0x901F; + public const int GL_AMD_draw_buffers_blend = 1; + public const int GL_AMD_framebuffer_multisample_advanced = 1; + public const int GL_RENDERBUFFER_STORAGE_SAMPLES_AMD = 0x91B2; + public const int GL_MAX_COLOR_FRAMEBUFFER_SAMPLES_AMD = 0x91B3; + public const int GL_MAX_COLOR_FRAMEBUFFER_STORAGE_SAMPLES_AMD = 0x91B4; + public const int GL_MAX_DEPTH_STENCIL_FRAMEBUFFER_SAMPLES_AMD = 0x91B5; + public const int GL_NUM_SUPPORTED_MULTISAMPLE_MODES_AMD = 0x91B6; + public const int GL_SUPPORTED_MULTISAMPLE_MODES_AMD = 0x91B7; + public const int GL_AMD_framebuffer_sample_positions = 1; + public const int GL_SUBSAMPLE_DISTANCE_AMD = 0x883F; + public const int GL_PIXELS_PER_SAMPLE_PATTERN_X_AMD = 0x91AE; + public const int GL_PIXELS_PER_SAMPLE_PATTERN_Y_AMD = 0x91AF; + public const int GL_ALL_PIXELS_AMD = -1; + public const int GL_AMD_gcn_shader = 1; + public const int GL_AMD_gpu_shader_half_float = 1; + public const int GL_FLOAT16_NV = 0x8FF8; + public const int GL_FLOAT16_VEC2_NV = 0x8FF9; + public const int GL_FLOAT16_VEC3_NV = 0x8FFA; + public const int GL_FLOAT16_VEC4_NV = 0x8FFB; + public const int GL_FLOAT16_MAT2_AMD = 0x91C5; + public const int GL_FLOAT16_MAT3_AMD = 0x91C6; + public const int GL_FLOAT16_MAT4_AMD = 0x91C7; + public const int GL_FLOAT16_MAT2x3_AMD = 0x91C8; + public const int GL_FLOAT16_MAT2x4_AMD = 0x91C9; + public const int GL_FLOAT16_MAT3x2_AMD = 0x91CA; + public const int GL_FLOAT16_MAT3x4_AMD = 0x91CB; + public const int GL_FLOAT16_MAT4x2_AMD = 0x91CC; + public const int GL_FLOAT16_MAT4x3_AMD = 0x91CD; + public const int GL_AMD_gpu_shader_int16 = 1; + public const int GL_AMD_gpu_shader_int64 = 1; + public const int GL_INT64_NV = 0x140E; + public const int GL_UNSIGNED_INT64_NV = 0x140F; + public const int GL_INT8_NV = 0x8FE0; + public const int GL_INT8_VEC2_NV = 0x8FE1; + public const int GL_INT8_VEC3_NV = 0x8FE2; + public const int GL_INT8_VEC4_NV = 0x8FE3; + public const int GL_INT16_NV = 0x8FE4; + public const int GL_INT16_VEC2_NV = 0x8FE5; + public const int GL_INT16_VEC3_NV = 0x8FE6; + public const int GL_INT16_VEC4_NV = 0x8FE7; + public const int GL_INT64_VEC2_NV = 0x8FE9; + public const int GL_INT64_VEC3_NV = 0x8FEA; + public const int GL_INT64_VEC4_NV = 0x8FEB; + public const int GL_UNSIGNED_INT8_NV = 0x8FEC; + public const int GL_UNSIGNED_INT8_VEC2_NV = 0x8FED; + public const int GL_UNSIGNED_INT8_VEC3_NV = 0x8FEE; + public const int GL_UNSIGNED_INT8_VEC4_NV = 0x8FEF; + public const int GL_UNSIGNED_INT16_NV = 0x8FF0; + public const int GL_UNSIGNED_INT16_VEC2_NV = 0x8FF1; + public const int GL_UNSIGNED_INT16_VEC3_NV = 0x8FF2; + public const int GL_UNSIGNED_INT16_VEC4_NV = 0x8FF3; + public const int GL_UNSIGNED_INT64_VEC2_NV = 0x8FF5; + public const int GL_UNSIGNED_INT64_VEC3_NV = 0x8FF6; + public const int GL_UNSIGNED_INT64_VEC4_NV = 0x8FF7; + public const int GL_AMD_interleaved_elements = 1; + public const int GL_VERTEX_ELEMENT_SWIZZLE_AMD = 0x91A4; + public const int GL_VERTEX_ID_SWIZZLE_AMD = 0x91A5; + public const int GL_AMD_multi_draw_indirect = 1; + public const int GL_AMD_name_gen_delete = 1; + public const int GL_DATA_BUFFER_AMD = 0x9151; + public const int GL_PERFORMANCE_MONITOR_AMD = 0x9152; + public const int GL_QUERY_OBJECT_AMD = 0x9153; + public const int GL_VERTEX_ARRAY_OBJECT_AMD = 0x9154; + public const int GL_SAMPLER_OBJECT_AMD = 0x9155; + public const int GL_AMD_occlusion_query_event = 1; + public const int GL_OCCLUSION_QUERY_EVENT_MASK_AMD = 0x874F; + public const int GL_QUERY_DEPTH_PASS_EVENT_BIT_AMD = 0x00000001; + public const int GL_QUERY_DEPTH_FAIL_EVENT_BIT_AMD = 0x00000002; + public const int GL_QUERY_STENCIL_FAIL_EVENT_BIT_AMD = 0x00000004; + public const int GL_QUERY_DEPTH_BOUNDS_FAIL_EVENT_BIT_AMD = 0x00000008; + public const int GL_QUERY_ALL_EVENT_BITS_AMD = -1; + public const int GL_AMD_performance_monitor = 1; + public const int GL_COUNTER_TYPE_AMD = 0x8BC0; + public const int GL_COUNTER_RANGE_AMD = 0x8BC1; + public const int GL_UNSIGNED_INT64_AMD = 0x8BC2; + public const int GL_PERCENTAGE_AMD = 0x8BC3; + public const int GL_PERFMON_RESULT_AVAILABLE_AMD = 0x8BC4; + public const int GL_PERFMON_RESULT_SIZE_AMD = 0x8BC5; + public const int GL_PERFMON_RESULT_AMD = 0x8BC6; + public const int GL_AMD_pinned_memory = 1; + public const int GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD = 0x9160; + public const int GL_AMD_query_buffer_object = 1; + public const int GL_QUERY_BUFFER_AMD = 0x9192; + public const int GL_QUERY_BUFFER_BINDING_AMD = 0x9193; + public const int GL_QUERY_RESULT_NO_WAIT_AMD = 0x9194; + public const int GL_AMD_sample_positions = 1; + public const int GL_AMD_seamless_cubemap_per_texture = 1; + public const int GL_AMD_shader_atomic_counter_ops = 1; + public const int GL_AMD_shader_ballot = 1; + public const int GL_AMD_shader_explicit_vertex_parameter = 1; + public const int GL_AMD_shader_gpu_shader_half_float_fetch = 1; + public const int GL_AMD_shader_image_load_store_lod = 1; + public const int GL_AMD_shader_stencil_export = 1; + public const int GL_AMD_shader_trinary_minmax = 1; + public const int GL_AMD_sparse_texture = 1; + public const int GL_VIRTUAL_PAGE_SIZE_X_AMD = 0x9195; + public const int GL_VIRTUAL_PAGE_SIZE_Y_AMD = 0x9196; + public const int GL_VIRTUAL_PAGE_SIZE_Z_AMD = 0x9197; + public const int GL_MAX_SPARSE_TEXTURE_SIZE_AMD = 0x9198; + public const int GL_MAX_SPARSE_3D_TEXTURE_SIZE_AMD = 0x9199; + public const int GL_MAX_SPARSE_ARRAY_TEXTURE_LAYERS = 0x919A; + public const int GL_MIN_SPARSE_LEVEL_AMD = 0x919B; + public const int GL_MIN_LOD_WARNING_AMD = 0x919C; + public const int GL_TEXTURE_STORAGE_SPARSE_BIT_AMD = 0x00000001; + public const int GL_AMD_stencil_operation_extended = 1; + public const int GL_SET_AMD = 0x874A; + public const int GL_REPLACE_VALUE_AMD = 0x874B; + public const int GL_STENCIL_OP_VALUE_AMD = 0x874C; + public const int GL_STENCIL_BACK_OP_VALUE_AMD = 0x874D; + public const int GL_AMD_texture_gather_bias_lod = 1; + public const int GL_AMD_texture_texture4 = 1; + public const int GL_AMD_transform_feedback3_lines_triangles = 1; + public const int GL_AMD_transform_feedback4 = 1; + public const int GL_STREAM_RASTERIZATION_AMD = 0x91A0; + public const int GL_AMD_vertex_shader_layer = 1; + public const int GL_AMD_vertex_shader_tessellator = 1; + public const int GL_SAMPLER_BUFFER_AMD = 0x9001; + public const int GL_INT_SAMPLER_BUFFER_AMD = 0x9002; + public const int GL_UNSIGNED_INT_SAMPLER_BUFFER_AMD = 0x9003; + public const int GL_TESSELLATION_MODE_AMD = 0x9004; + public const int GL_TESSELLATION_FACTOR_AMD = 0x9005; + public const int GL_DISCRETE_AMD = 0x9006; + public const int GL_CONTINUOUS_AMD = 0x9007; + public const int GL_AMD_vertex_shader_viewport_index = 1; + public const int GL_APPLE_aux_depth_stencil = 1; + public const int GL_AUX_DEPTH_STENCIL_APPLE = 0x8A14; + public const int GL_APPLE_client_storage = 1; + public const int GL_UNPACK_CLIENT_STORAGE_APPLE = 0x85B2; + public const int GL_APPLE_element_array = 1; + public const int GL_ELEMENT_ARRAY_APPLE = 0x8A0C; + public const int GL_ELEMENT_ARRAY_TYPE_APPLE = 0x8A0D; + public const int GL_ELEMENT_ARRAY_POINTER_APPLE = 0x8A0E; + public const int GL_APPLE_fence = 1; + public const int GL_DRAW_PIXELS_APPLE = 0x8A0A; + public const int GL_FENCE_APPLE = 0x8A0B; + public const int GL_APPLE_float_pixels = 1; + public const int GL_HALF_APPLE = 0x140B; + public const int GL_RGBA_FLOAT32_APPLE = 0x8814; + public const int GL_RGB_FLOAT32_APPLE = 0x8815; + public const int GL_ALPHA_FLOAT32_APPLE = 0x8816; + public const int GL_INTENSITY_FLOAT32_APPLE = 0x8817; + public const int GL_LUMINANCE_FLOAT32_APPLE = 0x8818; + public const int GL_LUMINANCE_ALPHA_FLOAT32_APPLE = 0x8819; + public const int GL_RGBA_FLOAT16_APPLE = 0x881A; + public const int GL_RGB_FLOAT16_APPLE = 0x881B; + public const int GL_ALPHA_FLOAT16_APPLE = 0x881C; + public const int GL_INTENSITY_FLOAT16_APPLE = 0x881D; + public const int GL_LUMINANCE_FLOAT16_APPLE = 0x881E; + public const int GL_LUMINANCE_ALPHA_FLOAT16_APPLE = 0x881F; + public const int GL_COLOR_FLOAT_APPLE = 0x8A0F; + public const int GL_APPLE_flush_buffer_range = 1; + public const int GL_BUFFER_SERIALIZED_MODIFY_APPLE = 0x8A12; + public const int GL_BUFFER_FLUSHING_UNMAP_APPLE = 0x8A13; + public const int GL_APPLE_object_purgeable = 1; + public const int GL_BUFFER_OBJECT_APPLE = 0x85B3; + public const int GL_RELEASED_APPLE = 0x8A19; + public const int GL_VOLATILE_APPLE = 0x8A1A; + public const int GL_RETAINED_APPLE = 0x8A1B; + public const int GL_UNDEFINED_APPLE = 0x8A1C; + public const int GL_PURGEABLE_APPLE = 0x8A1D; + public const int GL_APPLE_rgb_422 = 1; + public const int GL_RGB_422_APPLE = 0x8A1F; + public const int GL_UNSIGNED_SHORT_8_8_APPLE = 0x85BA; + public const int GL_UNSIGNED_SHORT_8_8_REV_APPLE = 0x85BB; + public const int GL_RGB_RAW_422_APPLE = 0x8A51; + public const int GL_APPLE_row_bytes = 1; + public const int GL_PACK_ROW_BYTES_APPLE = 0x8A15; + public const int GL_UNPACK_ROW_BYTES_APPLE = 0x8A16; + public const int GL_APPLE_specular_vector = 1; + public const int GL_LIGHT_MODEL_SPECULAR_VECTOR_APPLE = 0x85B0; + public const int GL_APPLE_texture_range = 1; + public const int GL_TEXTURE_RANGE_LENGTH_APPLE = 0x85B7; + public const int GL_TEXTURE_RANGE_POINTER_APPLE = 0x85B8; + public const int GL_TEXTURE_STORAGE_HINT_APPLE = 0x85BC; + public const int GL_STORAGE_PRIVATE_APPLE = 0x85BD; + public const int GL_STORAGE_CACHED_APPLE = 0x85BE; + public const int GL_STORAGE_SHARED_APPLE = 0x85BF; + public const int GL_APPLE_transform_hint = 1; + public const int GL_TRANSFORM_HINT_APPLE = 0x85B1; + public const int GL_APPLE_vertex_array_object = 1; + public const int GL_VERTEX_ARRAY_BINDING_APPLE = 0x85B5; + public const int GL_APPLE_vertex_array_range = 1; + public const int GL_VERTEX_ARRAY_RANGE_APPLE = 0x851D; + public const int GL_VERTEX_ARRAY_RANGE_LENGTH_APPLE = 0x851E; + public const int GL_VERTEX_ARRAY_STORAGE_HINT_APPLE = 0x851F; + public const int GL_VERTEX_ARRAY_RANGE_POINTER_APPLE = 0x8521; + public const int GL_STORAGE_CLIENT_APPLE = 0x85B4; + public const int GL_APPLE_vertex_program_evaluators = 1; + public const int GL_VERTEX_ATTRIB_MAP1_APPLE = 0x8A00; + public const int GL_VERTEX_ATTRIB_MAP2_APPLE = 0x8A01; + public const int GL_VERTEX_ATTRIB_MAP1_SIZE_APPLE = 0x8A02; + public const int GL_VERTEX_ATTRIB_MAP1_COEFF_APPLE = 0x8A03; + public const int GL_VERTEX_ATTRIB_MAP1_ORDER_APPLE = 0x8A04; + public const int GL_VERTEX_ATTRIB_MAP1_DOMAIN_APPLE = 0x8A05; + public const int GL_VERTEX_ATTRIB_MAP2_SIZE_APPLE = 0x8A06; + public const int GL_VERTEX_ATTRIB_MAP2_COEFF_APPLE = 0x8A07; + public const int GL_VERTEX_ATTRIB_MAP2_ORDER_APPLE = 0x8A08; + public const int GL_VERTEX_ATTRIB_MAP2_DOMAIN_APPLE = 0x8A09; + public const int GL_APPLE_ycbcr_422 = 1; + public const int GL_YCBCR_422_APPLE = 0x85B9; + public const int GL_ATI_draw_buffers = 1; + public const int GL_MAX_DRAW_BUFFERS_ATI = 0x8824; + public const int GL_DRAW_BUFFER0_ATI = 0x8825; + public const int GL_DRAW_BUFFER1_ATI = 0x8826; + public const int GL_DRAW_BUFFER2_ATI = 0x8827; + public const int GL_DRAW_BUFFER3_ATI = 0x8828; + public const int GL_DRAW_BUFFER4_ATI = 0x8829; + public const int GL_DRAW_BUFFER5_ATI = 0x882A; + public const int GL_DRAW_BUFFER6_ATI = 0x882B; + public const int GL_DRAW_BUFFER7_ATI = 0x882C; + public const int GL_DRAW_BUFFER8_ATI = 0x882D; + public const int GL_DRAW_BUFFER9_ATI = 0x882E; + public const int GL_DRAW_BUFFER10_ATI = 0x882F; + public const int GL_DRAW_BUFFER11_ATI = 0x8830; + public const int GL_DRAW_BUFFER12_ATI = 0x8831; + public const int GL_DRAW_BUFFER13_ATI = 0x8832; + public const int GL_DRAW_BUFFER14_ATI = 0x8833; + public const int GL_DRAW_BUFFER15_ATI = 0x8834; + public const int GL_ATI_element_array = 1; + public const int GL_ELEMENT_ARRAY_ATI = 0x8768; + public const int GL_ELEMENT_ARRAY_TYPE_ATI = 0x8769; + public const int GL_ELEMENT_ARRAY_POINTER_ATI = 0x876A; + public const int GL_ATI_envmap_bumpmap = 1; + public const int GL_BUMP_ROT_MATRIX_ATI = 0x8775; + public const int GL_BUMP_ROT_MATRIX_SIZE_ATI = 0x8776; + public const int GL_BUMP_NUM_TEX_UNITS_ATI = 0x8777; + public const int GL_BUMP_TEX_UNITS_ATI = 0x8778; + public const int GL_DUDV_ATI = 0x8779; + public const int GL_DU8DV8_ATI = 0x877A; + public const int GL_BUMP_ENVMAP_ATI = 0x877B; + public const int GL_BUMP_TARGET_ATI = 0x877C; + public const int GL_ATI_fragment_shader = 1; + public const int GL_FRAGMENT_SHADER_ATI = 0x8920; + public const int GL_REG_0_ATI = 0x8921; + public const int GL_REG_1_ATI = 0x8922; + public const int GL_REG_2_ATI = 0x8923; + public const int GL_REG_3_ATI = 0x8924; + public const int GL_REG_4_ATI = 0x8925; + public const int GL_REG_5_ATI = 0x8926; + public const int GL_REG_6_ATI = 0x8927; + public const int GL_REG_7_ATI = 0x8928; + public const int GL_REG_8_ATI = 0x8929; + public const int GL_REG_9_ATI = 0x892A; + public const int GL_REG_10_ATI = 0x892B; + public const int GL_REG_11_ATI = 0x892C; + public const int GL_REG_12_ATI = 0x892D; + public const int GL_REG_13_ATI = 0x892E; + public const int GL_REG_14_ATI = 0x892F; + public const int GL_REG_15_ATI = 0x8930; + public const int GL_REG_16_ATI = 0x8931; + public const int GL_REG_17_ATI = 0x8932; + public const int GL_REG_18_ATI = 0x8933; + public const int GL_REG_19_ATI = 0x8934; + public const int GL_REG_20_ATI = 0x8935; + public const int GL_REG_21_ATI = 0x8936; + public const int GL_REG_22_ATI = 0x8937; + public const int GL_REG_23_ATI = 0x8938; + public const int GL_REG_24_ATI = 0x8939; + public const int GL_REG_25_ATI = 0x893A; + public const int GL_REG_26_ATI = 0x893B; + public const int GL_REG_27_ATI = 0x893C; + public const int GL_REG_28_ATI = 0x893D; + public const int GL_REG_29_ATI = 0x893E; + public const int GL_REG_30_ATI = 0x893F; + public const int GL_REG_31_ATI = 0x8940; + public const int GL_CON_0_ATI = 0x8941; + public const int GL_CON_1_ATI = 0x8942; + public const int GL_CON_2_ATI = 0x8943; + public const int GL_CON_3_ATI = 0x8944; + public const int GL_CON_4_ATI = 0x8945; + public const int GL_CON_5_ATI = 0x8946; + public const int GL_CON_6_ATI = 0x8947; + public const int GL_CON_7_ATI = 0x8948; + public const int GL_CON_8_ATI = 0x8949; + public const int GL_CON_9_ATI = 0x894A; + public const int GL_CON_10_ATI = 0x894B; + public const int GL_CON_11_ATI = 0x894C; + public const int GL_CON_12_ATI = 0x894D; + public const int GL_CON_13_ATI = 0x894E; + public const int GL_CON_14_ATI = 0x894F; + public const int GL_CON_15_ATI = 0x8950; + public const int GL_CON_16_ATI = 0x8951; + public const int GL_CON_17_ATI = 0x8952; + public const int GL_CON_18_ATI = 0x8953; + public const int GL_CON_19_ATI = 0x8954; + public const int GL_CON_20_ATI = 0x8955; + public const int GL_CON_21_ATI = 0x8956; + public const int GL_CON_22_ATI = 0x8957; + public const int GL_CON_23_ATI = 0x8958; + public const int GL_CON_24_ATI = 0x8959; + public const int GL_CON_25_ATI = 0x895A; + public const int GL_CON_26_ATI = 0x895B; + public const int GL_CON_27_ATI = 0x895C; + public const int GL_CON_28_ATI = 0x895D; + public const int GL_CON_29_ATI = 0x895E; + public const int GL_CON_30_ATI = 0x895F; + public const int GL_CON_31_ATI = 0x8960; + public const int GL_MOV_ATI = 0x8961; + public const int GL_ADD_ATI = 0x8963; + public const int GL_MUL_ATI = 0x8964; + public const int GL_SUB_ATI = 0x8965; + public const int GL_DOT3_ATI = 0x8966; + public const int GL_DOT4_ATI = 0x8967; + public const int GL_MAD_ATI = 0x8968; + public const int GL_LERP_ATI = 0x8969; + public const int GL_CND_ATI = 0x896A; + public const int GL_CND0_ATI = 0x896B; + public const int GL_DOT2_ADD_ATI = 0x896C; + public const int GL_SECONDARY_INTERPOLATOR_ATI = 0x896D; + public const int GL_NUM_FRAGMENT_REGISTERS_ATI = 0x896E; + public const int GL_NUM_FRAGMENT_CONSTANTS_ATI = 0x896F; + public const int GL_NUM_PASSES_ATI = 0x8970; + public const int GL_NUM_INSTRUCTIONS_PER_PASS_ATI = 0x8971; + public const int GL_NUM_INSTRUCTIONS_TOTAL_ATI = 0x8972; + public const int GL_NUM_INPUT_INTERPOLATOR_COMPONENTS_ATI = 0x8973; + public const int GL_NUM_LOOPBACK_COMPONENTS_ATI = 0x8974; + public const int GL_COLOR_ALPHA_PAIRING_ATI = 0x8975; + public const int GL_SWIZZLE_STR_ATI = 0x8976; + public const int GL_SWIZZLE_STQ_ATI = 0x8977; + public const int GL_SWIZZLE_STR_DR_ATI = 0x8978; + public const int GL_SWIZZLE_STQ_DQ_ATI = 0x8979; + public const int GL_SWIZZLE_STRQ_ATI = 0x897A; + public const int GL_SWIZZLE_STRQ_DQ_ATI = 0x897B; + public const int GL_RED_BIT_ATI = 0x00000001; + public const int GL_GREEN_BIT_ATI = 0x00000002; + public const int GL_BLUE_BIT_ATI = 0x00000004; + public const int GL_2X_BIT_ATI = 0x00000001; + public const int GL_4X_BIT_ATI = 0x00000002; + public const int GL_8X_BIT_ATI = 0x00000004; + public const int GL_HALF_BIT_ATI = 0x00000008; + public const int GL_QUARTER_BIT_ATI = 0x00000010; + public const int GL_EIGHTH_BIT_ATI = 0x00000020; + public const int GL_SATURATE_BIT_ATI = 0x00000040; + public const int GL_COMP_BIT_ATI = 0x00000002; + public const int GL_NEGATE_BIT_ATI = 0x00000004; + public const int GL_BIAS_BIT_ATI = 0x00000008; + public const int GL_ATI_map_object_buffer = 1; + public const int GL_ATI_meminfo = 1; + public const int GL_VBO_FREE_MEMORY_ATI = 0x87FB; + public const int GL_TEXTURE_FREE_MEMORY_ATI = 0x87FC; + public const int GL_RENDERBUFFER_FREE_MEMORY_ATI = 0x87FD; + public const int GL_ATI_pixel_format_float = 1; + public const int GL_RGBA_FLOAT_MODE_ATI = 0x8820; + public const int GL_COLOR_CLEAR_UNCLAMPED_VALUE_ATI = 0x8835; + public const int GL_ATI_pn_triangles = 1; + public const int GL_PN_TRIANGLES_ATI = 0x87F0; + public const int GL_MAX_PN_TRIANGLES_TESSELATION_LEVEL_ATI = 0x87F1; + public const int GL_PN_TRIANGLES_POINT_MODE_ATI = 0x87F2; + public const int GL_PN_TRIANGLES_NORMAL_MODE_ATI = 0x87F3; + public const int GL_PN_TRIANGLES_TESSELATION_LEVEL_ATI = 0x87F4; + public const int GL_PN_TRIANGLES_POINT_MODE_LINEAR_ATI = 0x87F5; + public const int GL_PN_TRIANGLES_POINT_MODE_CUBIC_ATI = 0x87F6; + public const int GL_PN_TRIANGLES_NORMAL_MODE_LINEAR_ATI = 0x87F7; + public const int GL_PN_TRIANGLES_NORMAL_MODE_QUADRATIC_ATI = 0x87F8; + public const int GL_ATI_separate_stencil = 1; + public const int GL_STENCIL_BACK_FUNC_ATI = 0x8800; + public const int GL_STENCIL_BACK_FAIL_ATI = 0x8801; + public const int GL_STENCIL_BACK_PASS_DEPTH_FAIL_ATI = 0x8802; + public const int GL_STENCIL_BACK_PASS_DEPTH_PASS_ATI = 0x8803; + public const int GL_ATI_text_fragment_shader = 1; + public const int GL_TEXT_FRAGMENT_SHADER_ATI = 0x8200; + public const int GL_ATI_texture_env_combine3 = 1; + public const int GL_MODULATE_ADD_ATI = 0x8744; + public const int GL_MODULATE_SIGNED_ADD_ATI = 0x8745; + public const int GL_MODULATE_SUBTRACT_ATI = 0x8746; + public const int GL_ATI_texture_float = 1; + public const int GL_RGBA_FLOAT32_ATI = 0x8814; + public const int GL_RGB_FLOAT32_ATI = 0x8815; + public const int GL_ALPHA_FLOAT32_ATI = 0x8816; + public const int GL_INTENSITY_FLOAT32_ATI = 0x8817; + public const int GL_LUMINANCE_FLOAT32_ATI = 0x8818; + public const int GL_LUMINANCE_ALPHA_FLOAT32_ATI = 0x8819; + public const int GL_RGBA_FLOAT16_ATI = 0x881A; + public const int GL_RGB_FLOAT16_ATI = 0x881B; + public const int GL_ALPHA_FLOAT16_ATI = 0x881C; + public const int GL_INTENSITY_FLOAT16_ATI = 0x881D; + public const int GL_LUMINANCE_FLOAT16_ATI = 0x881E; + public const int GL_LUMINANCE_ALPHA_FLOAT16_ATI = 0x881F; + public const int GL_ATI_texture_mirror_once = 1; + public const int GL_MIRROR_CLAMP_ATI = 0x8742; + public const int GL_MIRROR_CLAMP_TO_EDGE_ATI = 0x8743; + public const int GL_ATI_vertex_array_object = 1; + public const int GL_STATIC_ATI = 0x8760; + public const int GL_DYNAMIC_ATI = 0x8761; + public const int GL_PRESERVE_ATI = 0x8762; + public const int GL_DISCARD_ATI = 0x8763; + public const int GL_OBJECT_BUFFER_SIZE_ATI = 0x8764; + public const int GL_OBJECT_BUFFER_USAGE_ATI = 0x8765; + public const int GL_ARRAY_OBJECT_BUFFER_ATI = 0x8766; + public const int GL_ARRAY_OBJECT_OFFSET_ATI = 0x8767; + public const int GL_ATI_vertex_attrib_array_object = 1; + public const int GL_ATI_vertex_streams = 1; + public const int GL_MAX_VERTEX_STREAMS_ATI = 0x876B; + public const int GL_VERTEX_STREAM0_ATI = 0x876C; + public const int GL_VERTEX_STREAM1_ATI = 0x876D; + public const int GL_VERTEX_STREAM2_ATI = 0x876E; + public const int GL_VERTEX_STREAM3_ATI = 0x876F; + public const int GL_VERTEX_STREAM4_ATI = 0x8770; + public const int GL_VERTEX_STREAM5_ATI = 0x8771; + public const int GL_VERTEX_STREAM6_ATI = 0x8772; + public const int GL_VERTEX_STREAM7_ATI = 0x8773; + public const int GL_VERTEX_SOURCE_ATI = 0x8774; + public const int GL_EXT_422_pixels = 1; + public const int GL_422_EXT = 0x80CC; + public const int GL_422_REV_EXT = 0x80CD; + public const int GL_422_AVERAGE_EXT = 0x80CE; + public const int GL_422_REV_AVERAGE_EXT = 0x80CF; + public const int GL_EXT_EGL_image_storage = 1; + public const int GL_EXT_abgr = 1; + public const int GL_ABGR_EXT = 0x8000; + public const int GL_EXT_bgra = 1; + public const int GL_BGR_EXT = 0x80E0; + public const int GL_BGRA_EXT = 0x80E1; + public const int GL_EXT_bindable_uniform = 1; + public const int GL_MAX_VERTEX_BINDABLE_UNIFORMS_EXT = 0x8DE2; + public const int GL_MAX_FRAGMENT_BINDABLE_UNIFORMS_EXT = 0x8DE3; + public const int GL_MAX_GEOMETRY_BINDABLE_UNIFORMS_EXT = 0x8DE4; + public const int GL_MAX_BINDABLE_UNIFORM_SIZE_EXT = 0x8DED; + public const int GL_UNIFORM_BUFFER_EXT = 0x8DEE; + public const int GL_UNIFORM_BUFFER_BINDING_EXT = 0x8DEF; + public const int GL_EXT_blend_color = 1; + public const int GL_CONSTANT_COLOR_EXT = 0x8001; + public const int GL_ONE_MINUS_CONSTANT_COLOR_EXT = 0x8002; + public const int GL_CONSTANT_ALPHA_EXT = 0x8003; + public const int GL_ONE_MINUS_CONSTANT_ALPHA_EXT = 0x8004; + public const int GL_BLEND_COLOR_EXT = 0x8005; + public const int GL_EXT_blend_equation_separate = 1; + public const int GL_BLEND_EQUATION_RGB_EXT = 0x8009; + public const int GL_BLEND_EQUATION_ALPHA_EXT = 0x883D; + public const int GL_EXT_blend_func_separate = 1; + public const int GL_BLEND_DST_RGB_EXT = 0x80C8; + public const int GL_BLEND_SRC_RGB_EXT = 0x80C9; + public const int GL_BLEND_DST_ALPHA_EXT = 0x80CA; + public const int GL_BLEND_SRC_ALPHA_EXT = 0x80CB; + public const int GL_EXT_blend_logic_op = 1; + public const int GL_EXT_blend_minmax = 1; + public const int GL_MIN_EXT = 0x8007; + public const int GL_MAX_EXT = 0x8008; + public const int GL_FUNC_ADD_EXT = 0x8006; + public const int GL_BLEND_EQUATION_EXT = 0x8009; + public const int GL_EXT_blend_subtract = 1; + public const int GL_FUNC_SUBTRACT_EXT = 0x800A; + public const int GL_FUNC_REVERSE_SUBTRACT_EXT = 0x800B; + public const int GL_EXT_clip_volume_hint = 1; + public const int GL_CLIP_VOLUME_CLIPPING_HINT_EXT = 0x80F0; + public const int GL_EXT_cmyka = 1; + public const int GL_CMYK_EXT = 0x800C; + public const int GL_CMYKA_EXT = 0x800D; + public const int GL_PACK_CMYK_HINT_EXT = 0x800E; + public const int GL_UNPACK_CMYK_HINT_EXT = 0x800F; + public const int GL_EXT_color_subtable = 1; + public const int GL_EXT_compiled_vertex_array = 1; + public const int GL_ARRAY_ELEMENT_LOCK_FIRST_EXT = 0x81A8; + public const int GL_ARRAY_ELEMENT_LOCK_COUNT_EXT = 0x81A9; + public const int GL_EXT_convolution = 1; + public const int GL_CONVOLUTION_1D_EXT = 0x8010; + public const int GL_CONVOLUTION_2D_EXT = 0x8011; + public const int GL_SEPARABLE_2D_EXT = 0x8012; + public const int GL_CONVOLUTION_BORDER_MODE_EXT = 0x8013; + public const int GL_CONVOLUTION_FILTER_SCALE_EXT = 0x8014; + public const int GL_CONVOLUTION_FILTER_BIAS_EXT = 0x8015; + public const int GL_REDUCE_EXT = 0x8016; + public const int GL_CONVOLUTION_FORMAT_EXT = 0x8017; + public const int GL_CONVOLUTION_WIDTH_EXT = 0x8018; + public const int GL_CONVOLUTION_HEIGHT_EXT = 0x8019; + public const int GL_MAX_CONVOLUTION_WIDTH_EXT = 0x801A; + public const int GL_MAX_CONVOLUTION_HEIGHT_EXT = 0x801B; + public const int GL_POST_CONVOLUTION_RED_SCALE_EXT = 0x801C; + public const int GL_POST_CONVOLUTION_GREEN_SCALE_EXT = 0x801D; + public const int GL_POST_CONVOLUTION_BLUE_SCALE_EXT = 0x801E; + public const int GL_POST_CONVOLUTION_ALPHA_SCALE_EXT = 0x801F; + public const int GL_POST_CONVOLUTION_RED_BIAS_EXT = 0x8020; + public const int GL_POST_CONVOLUTION_GREEN_BIAS_EXT = 0x8021; + public const int GL_POST_CONVOLUTION_BLUE_BIAS_EXT = 0x8022; + public const int GL_POST_CONVOLUTION_ALPHA_BIAS_EXT = 0x8023; + public const int GL_EXT_coordinate_frame = 1; + public const int GL_TANGENT_ARRAY_EXT = 0x8439; + public const int GL_BINORMAL_ARRAY_EXT = 0x843A; + public const int GL_CURRENT_TANGENT_EXT = 0x843B; + public const int GL_CURRENT_BINORMAL_EXT = 0x843C; + public const int GL_TANGENT_ARRAY_TYPE_EXT = 0x843E; + public const int GL_TANGENT_ARRAY_STRIDE_EXT = 0x843F; + public const int GL_BINORMAL_ARRAY_TYPE_EXT = 0x8440; + public const int GL_BINORMAL_ARRAY_STRIDE_EXT = 0x8441; + public const int GL_TANGENT_ARRAY_POINTER_EXT = 0x8442; + public const int GL_BINORMAL_ARRAY_POINTER_EXT = 0x8443; + public const int GL_MAP1_TANGENT_EXT = 0x8444; + public const int GL_MAP2_TANGENT_EXT = 0x8445; + public const int GL_MAP1_BINORMAL_EXT = 0x8446; + public const int GL_MAP2_BINORMAL_EXT = 0x8447; + public const int GL_EXT_copy_texture = 1; + public const int GL_EXT_cull_vertex = 1; + public const int GL_CULL_VERTEX_EXT = 0x81AA; + public const int GL_CULL_VERTEX_EYE_POSITION_EXT = 0x81AB; + public const int GL_CULL_VERTEX_OBJECT_POSITION_EXT = 0x81AC; + public const int GL_EXT_debug_label = 1; + public const int GL_PROGRAM_PIPELINE_OBJECT_EXT = 0x8A4F; + public const int GL_PROGRAM_OBJECT_EXT = 0x8B40; + public const int GL_SHADER_OBJECT_EXT = 0x8B48; + public const int GL_BUFFER_OBJECT_EXT = 0x9151; + public const int GL_QUERY_OBJECT_EXT = 0x9153; + public const int GL_VERTEX_ARRAY_OBJECT_EXT = 0x9154; + public const int GL_EXT_debug_marker = 1; + public const int GL_EXT_depth_bounds_test = 1; + public const int GL_DEPTH_BOUNDS_TEST_EXT = 0x8890; + public const int GL_DEPTH_BOUNDS_EXT = 0x8891; + public const int GL_EXT_direct_state_access = 1; + public const int GL_PROGRAM_MATRIX_EXT = 0x8E2D; + public const int GL_TRANSPOSE_PROGRAM_MATRIX_EXT = 0x8E2E; + public const int GL_PROGRAM_MATRIX_STACK_DEPTH_EXT = 0x8E2F; + public const int GL_EXT_draw_buffers2 = 1; + public const int GL_EXT_draw_instanced = 1; + public const int GL_EXT_draw_range_elements = 1; + public const int GL_MAX_ELEMENTS_VERTICES_EXT = 0x80E8; + public const int GL_MAX_ELEMENTS_INDICES_EXT = 0x80E9; + public const int GL_EXT_external_buffer = 1; + public const int GL_EXT_fog_coord = 1; + public const int GL_FOG_COORDINATE_SOURCE_EXT = 0x8450; + public const int GL_FOG_COORDINATE_EXT = 0x8451; + public const int GL_FRAGMENT_DEPTH_EXT = 0x8452; + public const int GL_CURRENT_FOG_COORDINATE_EXT = 0x8453; + public const int GL_FOG_COORDINATE_ARRAY_TYPE_EXT = 0x8454; + public const int GL_FOG_COORDINATE_ARRAY_STRIDE_EXT = 0x8455; + public const int GL_FOG_COORDINATE_ARRAY_POINTER_EXT = 0x8456; + public const int GL_FOG_COORDINATE_ARRAY_EXT = 0x8457; + public const int GL_EXT_framebuffer_blit = 1; + public const int GL_READ_FRAMEBUFFER_EXT = 0x8CA8; + public const int GL_DRAW_FRAMEBUFFER_EXT = 0x8CA9; + public const int GL_DRAW_FRAMEBUFFER_BINDING_EXT = 0x8CA6; + public const int GL_READ_FRAMEBUFFER_BINDING_EXT = 0x8CAA; + public const int GL_EXT_framebuffer_multisample = 1; + public const int GL_RENDERBUFFER_SAMPLES_EXT = 0x8CAB; + public const int GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE_EXT = 0x8D56; + public const int GL_MAX_SAMPLES_EXT = 0x8D57; + public const int GL_EXT_framebuffer_multisample_blit_scaled = 1; + public const int GL_SCALED_RESOLVE_FASTEST_EXT = 0x90BA; + public const int GL_SCALED_RESOLVE_NICEST_EXT = 0x90BB; + public const int GL_EXT_framebuffer_object = 1; + public const int GL_INVALID_FRAMEBUFFER_OPERATION_EXT = 0x0506; + public const int GL_MAX_RENDERBUFFER_SIZE_EXT = 0x84E8; + public const int GL_FRAMEBUFFER_BINDING_EXT = 0x8CA6; + public const int GL_RENDERBUFFER_BINDING_EXT = 0x8CA7; + public const int GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE_EXT = 0x8CD0; + public const int GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME_EXT = 0x8CD1; + public const int GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL_EXT = 0x8CD2; + public const int GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE_EXT = 0x8CD3; + public const int GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_3D_ZOFFSET_EXT = 0x8CD4; + public const int GL_FRAMEBUFFER_COMPLETE_EXT = 0x8CD5; + public const int GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT = 0x8CD6; + public const int GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT = 0x8CD7; + public const int GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT = 0x8CD9; + public const int GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT = 0x8CDA; + public const int GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT = 0x8CDB; + public const int GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT = 0x8CDC; + public const int GL_FRAMEBUFFER_UNSUPPORTED_EXT = 0x8CDD; + public const int GL_MAX_COLOR_ATTACHMENTS_EXT = 0x8CDF; + public const int GL_COLOR_ATTACHMENT0_EXT = 0x8CE0; + public const int GL_COLOR_ATTACHMENT1_EXT = 0x8CE1; + public const int GL_COLOR_ATTACHMENT2_EXT = 0x8CE2; + public const int GL_COLOR_ATTACHMENT3_EXT = 0x8CE3; + public const int GL_COLOR_ATTACHMENT4_EXT = 0x8CE4; + public const int GL_COLOR_ATTACHMENT5_EXT = 0x8CE5; + public const int GL_COLOR_ATTACHMENT6_EXT = 0x8CE6; + public const int GL_COLOR_ATTACHMENT7_EXT = 0x8CE7; + public const int GL_COLOR_ATTACHMENT8_EXT = 0x8CE8; + public const int GL_COLOR_ATTACHMENT9_EXT = 0x8CE9; + public const int GL_COLOR_ATTACHMENT10_EXT = 0x8CEA; + public const int GL_COLOR_ATTACHMENT11_EXT = 0x8CEB; + public const int GL_COLOR_ATTACHMENT12_EXT = 0x8CEC; + public const int GL_COLOR_ATTACHMENT13_EXT = 0x8CED; + public const int GL_COLOR_ATTACHMENT14_EXT = 0x8CEE; + public const int GL_COLOR_ATTACHMENT15_EXT = 0x8CEF; + public const int GL_DEPTH_ATTACHMENT_EXT = 0x8D00; + public const int GL_STENCIL_ATTACHMENT_EXT = 0x8D20; + public const int GL_FRAMEBUFFER_EXT = 0x8D40; + public const int GL_RENDERBUFFER_EXT = 0x8D41; + public const int GL_RENDERBUFFER_WIDTH_EXT = 0x8D42; + public const int GL_RENDERBUFFER_HEIGHT_EXT = 0x8D43; + public const int GL_RENDERBUFFER_INTERNAL_FORMAT_EXT = 0x8D44; + public const int GL_STENCIL_INDEX1_EXT = 0x8D46; + public const int GL_STENCIL_INDEX4_EXT = 0x8D47; + public const int GL_STENCIL_INDEX8_EXT = 0x8D48; + public const int GL_STENCIL_INDEX16_EXT = 0x8D49; + public const int GL_RENDERBUFFER_RED_SIZE_EXT = 0x8D50; + public const int GL_RENDERBUFFER_GREEN_SIZE_EXT = 0x8D51; + public const int GL_RENDERBUFFER_BLUE_SIZE_EXT = 0x8D52; + public const int GL_RENDERBUFFER_ALPHA_SIZE_EXT = 0x8D53; + public const int GL_RENDERBUFFER_DEPTH_SIZE_EXT = 0x8D54; + public const int GL_RENDERBUFFER_STENCIL_SIZE_EXT = 0x8D55; + public const int GL_EXT_framebuffer_sRGB = 1; + public const int GL_FRAMEBUFFER_SRGB_EXT = 0x8DB9; + public const int GL_FRAMEBUFFER_SRGB_CAPABLE_EXT = 0x8DBA; + public const int GL_EXT_geometry_shader4 = 1; + public const int GL_GEOMETRY_SHADER_EXT = 0x8DD9; + public const int GL_GEOMETRY_VERTICES_OUT_EXT = 0x8DDA; + public const int GL_GEOMETRY_INPUT_TYPE_EXT = 0x8DDB; + public const int GL_GEOMETRY_OUTPUT_TYPE_EXT = 0x8DDC; + public const int GL_MAX_GEOMETRY_TEXTURE_IMAGE_UNITS_EXT = 0x8C29; + public const int GL_MAX_GEOMETRY_VARYING_COMPONENTS_EXT = 0x8DDD; + public const int GL_MAX_VERTEX_VARYING_COMPONENTS_EXT = 0x8DDE; + public const int GL_MAX_VARYING_COMPONENTS_EXT = 0x8B4B; + public const int GL_MAX_GEOMETRY_UNIFORM_COMPONENTS_EXT = 0x8DDF; + public const int GL_MAX_GEOMETRY_OUTPUT_VERTICES_EXT = 0x8DE0; + public const int GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS_EXT = 0x8DE1; + public const int GL_LINES_ADJACENCY_EXT = 0x000A; + public const int GL_LINE_STRIP_ADJACENCY_EXT = 0x000B; + public const int GL_TRIANGLES_ADJACENCY_EXT = 0x000C; + public const int GL_TRIANGLE_STRIP_ADJACENCY_EXT = 0x000D; + public const int GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS_EXT = 0x8DA8; + public const int GL_FRAMEBUFFER_INCOMPLETE_LAYER_COUNT_EXT = 0x8DA9; + public const int GL_FRAMEBUFFER_ATTACHMENT_LAYERED_EXT = 0x8DA7; + public const int GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LAYER_EXT = 0x8CD4; + public const int GL_PROGRAM_POINT_SIZE_EXT = 0x8642; + public const int GL_EXT_gpu_program_parameters = 1; + public const int GL_EXT_gpu_shader4 = 1; + public const int GL_VERTEX_ATTRIB_ARRAY_INTEGER_EXT = 0x88FD; + public const int GL_SAMPLER_1D_ARRAY_EXT = 0x8DC0; + public const int GL_SAMPLER_2D_ARRAY_EXT = 0x8DC1; + public const int GL_SAMPLER_BUFFER_EXT = 0x8DC2; + public const int GL_SAMPLER_1D_ARRAY_SHADOW_EXT = 0x8DC3; + public const int GL_SAMPLER_2D_ARRAY_SHADOW_EXT = 0x8DC4; + public const int GL_SAMPLER_CUBE_SHADOW_EXT = 0x8DC5; + public const int GL_UNSIGNED_INT_VEC2_EXT = 0x8DC6; + public const int GL_UNSIGNED_INT_VEC3_EXT = 0x8DC7; + public const int GL_UNSIGNED_INT_VEC4_EXT = 0x8DC8; + public const int GL_INT_SAMPLER_1D_EXT = 0x8DC9; + public const int GL_INT_SAMPLER_2D_EXT = 0x8DCA; + public const int GL_INT_SAMPLER_3D_EXT = 0x8DCB; + public const int GL_INT_SAMPLER_CUBE_EXT = 0x8DCC; + public const int GL_INT_SAMPLER_2D_RECT_EXT = 0x8DCD; + public const int GL_INT_SAMPLER_1D_ARRAY_EXT = 0x8DCE; + public const int GL_INT_SAMPLER_2D_ARRAY_EXT = 0x8DCF; + public const int GL_INT_SAMPLER_BUFFER_EXT = 0x8DD0; + public const int GL_UNSIGNED_INT_SAMPLER_1D_EXT = 0x8DD1; + public const int GL_UNSIGNED_INT_SAMPLER_2D_EXT = 0x8DD2; + public const int GL_UNSIGNED_INT_SAMPLER_3D_EXT = 0x8DD3; + public const int GL_UNSIGNED_INT_SAMPLER_CUBE_EXT = 0x8DD4; + public const int GL_UNSIGNED_INT_SAMPLER_2D_RECT_EXT = 0x8DD5; + public const int GL_UNSIGNED_INT_SAMPLER_1D_ARRAY_EXT = 0x8DD6; + public const int GL_UNSIGNED_INT_SAMPLER_2D_ARRAY_EXT = 0x8DD7; + public const int GL_UNSIGNED_INT_SAMPLER_BUFFER_EXT = 0x8DD8; + public const int GL_MIN_PROGRAM_TEXEL_OFFSET_EXT = 0x8904; + public const int GL_MAX_PROGRAM_TEXEL_OFFSET_EXT = 0x8905; + public const int GL_EXT_histogram = 1; + public const int GL_HISTOGRAM_EXT = 0x8024; + public const int GL_PROXY_HISTOGRAM_EXT = 0x8025; + public const int GL_HISTOGRAM_WIDTH_EXT = 0x8026; + public const int GL_HISTOGRAM_FORMAT_EXT = 0x8027; + public const int GL_HISTOGRAM_RED_SIZE_EXT = 0x8028; + public const int GL_HISTOGRAM_GREEN_SIZE_EXT = 0x8029; + public const int GL_HISTOGRAM_BLUE_SIZE_EXT = 0x802A; + public const int GL_HISTOGRAM_ALPHA_SIZE_EXT = 0x802B; + public const int GL_HISTOGRAM_LUMINANCE_SIZE_EXT = 0x802C; + public const int GL_HISTOGRAM_SINK_EXT = 0x802D; + public const int GL_MINMAX_EXT = 0x802E; + public const int GL_MINMAX_FORMAT_EXT = 0x802F; + public const int GL_MINMAX_SINK_EXT = 0x8030; + public const int GL_TABLE_TOO_LARGE_EXT = 0x8031; + public const int GL_EXT_index_array_formats = 1; + public const int GL_IUI_V2F_EXT = 0x81AD; + public const int GL_IUI_V3F_EXT = 0x81AE; + public const int GL_IUI_N3F_V2F_EXT = 0x81AF; + public const int GL_IUI_N3F_V3F_EXT = 0x81B0; + public const int GL_T2F_IUI_V2F_EXT = 0x81B1; + public const int GL_T2F_IUI_V3F_EXT = 0x81B2; + public const int GL_T2F_IUI_N3F_V2F_EXT = 0x81B3; + public const int GL_T2F_IUI_N3F_V3F_EXT = 0x81B4; + public const int GL_EXT_index_func = 1; + public const int GL_INDEX_TEST_EXT = 0x81B5; + public const int GL_INDEX_TEST_FUNC_EXT = 0x81B6; + public const int GL_INDEX_TEST_REF_EXT = 0x81B7; + public const int GL_EXT_index_material = 1; + public const int GL_INDEX_MATERIAL_EXT = 0x81B8; + public const int GL_INDEX_MATERIAL_PARAMETER_EXT = 0x81B9; + public const int GL_INDEX_MATERIAL_FACE_EXT = 0x81BA; + public const int GL_EXT_index_texture = 1; + public const int GL_EXT_light_texture = 1; + public const int GL_FRAGMENT_MATERIAL_EXT = 0x8349; + public const int GL_FRAGMENT_NORMAL_EXT = 0x834A; + public const int GL_FRAGMENT_COLOR_EXT = 0x834C; + public const int GL_ATTENUATION_EXT = 0x834D; + public const int GL_SHADOW_ATTENUATION_EXT = 0x834E; + public const int GL_TEXTURE_APPLICATION_MODE_EXT = 0x834F; + public const int GL_TEXTURE_LIGHT_EXT = 0x8350; + public const int GL_TEXTURE_MATERIAL_FACE_EXT = 0x8351; + public const int GL_TEXTURE_MATERIAL_PARAMETER_EXT = 0x8352; + public const int GL_EXT_memory_object = 1; + public const int GL_TEXTURE_TILING_EXT = 0x9580; + public const int GL_DEDICATED_MEMORY_OBJECT_EXT = 0x9581; + public const int GL_PROTECTED_MEMORY_OBJECT_EXT = 0x959B; + public const int GL_NUM_TILING_TYPES_EXT = 0x9582; + public const int GL_TILING_TYPES_EXT = 0x9583; + public const int GL_OPTIMAL_TILING_EXT = 0x9584; + public const int GL_LINEAR_TILING_EXT = 0x9585; + public const int GL_NUM_DEVICE_UUIDS_EXT = 0x9596; + public const int GL_DEVICE_UUID_EXT = 0x9597; + public const int GL_DRIVER_UUID_EXT = 0x9598; + public const int GL_UUID_SIZE_EXT = 16; + public const int GL_EXT_memory_object_fd = 1; + public const int GL_HANDLE_TYPE_OPAQUE_FD_EXT = 0x9586; + public const int GL_EXT_memory_object_win32 = 1; + public const int GL_HANDLE_TYPE_OPAQUE_WIN32_EXT = 0x9587; + public const int GL_HANDLE_TYPE_OPAQUE_WIN32_KMT_EXT = 0x9588; + public const int GL_DEVICE_LUID_EXT = 0x9599; + public const int GL_DEVICE_NODE_MASK_EXT = 0x959A; + public const int GL_LUID_SIZE_EXT = 8; + public const int GL_HANDLE_TYPE_D3D12_TILEPOOL_EXT = 0x9589; + public const int GL_HANDLE_TYPE_D3D12_RESOURCE_EXT = 0x958A; + public const int GL_HANDLE_TYPE_D3D11_IMAGE_EXT = 0x958B; + public const int GL_HANDLE_TYPE_D3D11_IMAGE_KMT_EXT = 0x958C; + public const int GL_EXT_misc_attribute = 1; + public const int GL_EXT_multi_draw_arrays = 1; + public const int GL_EXT_multisample = 1; + public const int GL_MULTISAMPLE_EXT = 0x809D; + public const int GL_SAMPLE_ALPHA_TO_MASK_EXT = 0x809E; + public const int GL_SAMPLE_ALPHA_TO_ONE_EXT = 0x809F; + public const int GL_SAMPLE_MASK_EXT = 0x80A0; + public const int GL_1PASS_EXT = 0x80A1; + public const int GL_2PASS_0_EXT = 0x80A2; + public const int GL_2PASS_1_EXT = 0x80A3; + public const int GL_4PASS_0_EXT = 0x80A4; + public const int GL_4PASS_1_EXT = 0x80A5; + public const int GL_4PASS_2_EXT = 0x80A6; + public const int GL_4PASS_3_EXT = 0x80A7; + public const int GL_SAMPLE_BUFFERS_EXT = 0x80A8; + public const int GL_SAMPLES_EXT = 0x80A9; + public const int GL_SAMPLE_MASK_VALUE_EXT = 0x80AA; + public const int GL_SAMPLE_MASK_INVERT_EXT = 0x80AB; + public const int GL_SAMPLE_PATTERN_EXT = 0x80AC; + public const int GL_MULTISAMPLE_BIT_EXT = 0x20000000; + public const int GL_EXT_packed_depth_stencil = 1; + public const int GL_DEPTH_STENCIL_EXT = 0x84F9; + public const int GL_UNSIGNED_INT_24_8_EXT = 0x84FA; + public const int GL_DEPTH24_STENCIL8_EXT = 0x88F0; + public const int GL_TEXTURE_STENCIL_SIZE_EXT = 0x88F1; + public const int GL_EXT_packed_float = 1; + public const int GL_R11F_G11F_B10F_EXT = 0x8C3A; + public const int GL_UNSIGNED_INT_10F_11F_11F_REV_EXT = 0x8C3B; + public const int GL_RGBA_SIGNED_COMPONENTS_EXT = 0x8C3C; + public const int GL_EXT_packed_pixels = 1; + public const int GL_UNSIGNED_BYTE_3_3_2_EXT = 0x8032; + public const int GL_UNSIGNED_SHORT_4_4_4_4_EXT = 0x8033; + public const int GL_UNSIGNED_SHORT_5_5_5_1_EXT = 0x8034; + public const int GL_UNSIGNED_INT_8_8_8_8_EXT = 0x8035; + public const int GL_UNSIGNED_INT_10_10_10_2_EXT = 0x8036; + public const int GL_EXT_paletted_texture = 1; + public const int GL_COLOR_INDEX1_EXT = 0x80E2; + public const int GL_COLOR_INDEX2_EXT = 0x80E3; + public const int GL_COLOR_INDEX4_EXT = 0x80E4; + public const int GL_COLOR_INDEX8_EXT = 0x80E5; + public const int GL_COLOR_INDEX12_EXT = 0x80E6; + public const int GL_COLOR_INDEX16_EXT = 0x80E7; + public const int GL_TEXTURE_INDEX_SIZE_EXT = 0x80ED; + public const int GL_EXT_pixel_buffer_object = 1; + public const int GL_PIXEL_PACK_BUFFER_EXT = 0x88EB; + public const int GL_PIXEL_UNPACK_BUFFER_EXT = 0x88EC; + public const int GL_PIXEL_PACK_BUFFER_BINDING_EXT = 0x88ED; + public const int GL_PIXEL_UNPACK_BUFFER_BINDING_EXT = 0x88EF; + public const int GL_EXT_pixel_transform = 1; + public const int GL_PIXEL_TRANSFORM_2D_EXT = 0x8330; + public const int GL_PIXEL_MAG_FILTER_EXT = 0x8331; + public const int GL_PIXEL_MIN_FILTER_EXT = 0x8332; + public const int GL_PIXEL_CUBIC_WEIGHT_EXT = 0x8333; + public const int GL_CUBIC_EXT = 0x8334; + public const int GL_AVERAGE_EXT = 0x8335; + public const int GL_PIXEL_TRANSFORM_2D_STACK_DEPTH_EXT = 0x8336; + public const int GL_MAX_PIXEL_TRANSFORM_2D_STACK_DEPTH_EXT = 0x8337; + public const int GL_PIXEL_TRANSFORM_2D_MATRIX_EXT = 0x8338; + public const int GL_EXT_pixel_transform_color_table = 1; + public const int GL_EXT_point_parameters = 1; + public const int GL_POINT_SIZE_MIN_EXT = 0x8126; + public const int GL_POINT_SIZE_MAX_EXT = 0x8127; + public const int GL_POINT_FADE_THRESHOLD_SIZE_EXT = 0x8128; + public const int GL_DISTANCE_ATTENUATION_EXT = 0x8129; + public const int GL_EXT_polygon_offset = 1; + public const int GL_POLYGON_OFFSET_EXT = 0x8037; + public const int GL_POLYGON_OFFSET_FACTOR_EXT = 0x8038; + public const int GL_POLYGON_OFFSET_BIAS_EXT = 0x8039; + public const int GL_EXT_polygon_offset_clamp = 1; + public const int GL_POLYGON_OFFSET_CLAMP_EXT = 0x8E1B; + public const int GL_EXT_post_depth_coverage = 1; + public const int GL_EXT_provoking_vertex = 1; + public const int GL_QUADS_FOLLOW_PROVOKING_VERTEX_CONVENTION_EXT = 0x8E4C; + public const int GL_FIRST_VERTEX_CONVENTION_EXT = 0x8E4D; + public const int GL_LAST_VERTEX_CONVENTION_EXT = 0x8E4E; + public const int GL_PROVOKING_VERTEX_EXT = 0x8E4F; + public const int GL_EXT_raster_multisample = 1; + public const int GL_RASTER_MULTISAMPLE_EXT = 0x9327; + public const int GL_RASTER_SAMPLES_EXT = 0x9328; + public const int GL_MAX_RASTER_SAMPLES_EXT = 0x9329; + public const int GL_RASTER_FIXED_SAMPLE_LOCATIONS_EXT = 0x932A; + public const int GL_MULTISAMPLE_RASTERIZATION_ALLOWED_EXT = 0x932B; + public const int GL_EFFECTIVE_RASTER_SAMPLES_EXT = 0x932C; + public const int GL_EXT_rescale_normal = 1; + public const int GL_RESCALE_NORMAL_EXT = 0x803A; + public const int GL_EXT_secondary_color = 1; + public const int GL_COLOR_SUM_EXT = 0x8458; + public const int GL_CURRENT_SECONDARY_COLOR_EXT = 0x8459; + public const int GL_SECONDARY_COLOR_ARRAY_SIZE_EXT = 0x845A; + public const int GL_SECONDARY_COLOR_ARRAY_TYPE_EXT = 0x845B; + public const int GL_SECONDARY_COLOR_ARRAY_STRIDE_EXT = 0x845C; + public const int GL_SECONDARY_COLOR_ARRAY_POINTER_EXT = 0x845D; + public const int GL_SECONDARY_COLOR_ARRAY_EXT = 0x845E; + public const int GL_EXT_semaphore = 1; + public const int GL_LAYOUT_GENERAL_EXT = 0x958D; + public const int GL_LAYOUT_COLOR_ATTACHMENT_EXT = 0x958E; + public const int GL_LAYOUT_DEPTH_STENCIL_ATTACHMENT_EXT = 0x958F; + public const int GL_LAYOUT_DEPTH_STENCIL_READ_ONLY_EXT = 0x9590; + public const int GL_LAYOUT_SHADER_READ_ONLY_EXT = 0x9591; + public const int GL_LAYOUT_TRANSFER_SRC_EXT = 0x9592; + public const int GL_LAYOUT_TRANSFER_DST_EXT = 0x9593; + public const int GL_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_EXT = 0x9530; + public const int GL_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_EXT = 0x9531; + public const int GL_EXT_semaphore_fd = 1; + public const int GL_EXT_semaphore_win32 = 1; + public const int GL_HANDLE_TYPE_D3D12_FENCE_EXT = 0x9594; + public const int GL_D3D12_FENCE_VALUE_EXT = 0x9595; + public const int GL_EXT_separate_shader_objects = 1; + public const int GL_ACTIVE_PROGRAM_EXT = 0x8B8D; + public const int GL_EXT_separate_specular_color = 1; + public const int GL_LIGHT_MODEL_COLOR_CONTROL_EXT = 0x81F8; + public const int GL_SINGLE_COLOR_EXT = 0x81F9; + public const int GL_SEPARATE_SPECULAR_COLOR_EXT = 0x81FA; + public const int GL_EXT_shader_framebuffer_fetch = 1; + public const int GL_FRAGMENT_SHADER_DISCARDS_SAMPLES_EXT = 0x8A52; + public const int GL_EXT_shader_framebuffer_fetch_non_coherent = 1; + public const int GL_EXT_shader_image_load_formatted = 1; + public const int GL_EXT_shader_image_load_store = 1; + public const int GL_MAX_IMAGE_UNITS_EXT = 0x8F38; + public const int GL_MAX_COMBINED_IMAGE_UNITS_AND_FRAGMENT_OUTPUTS_EXT = 0x8F39; + public const int GL_IMAGE_BINDING_NAME_EXT = 0x8F3A; + public const int GL_IMAGE_BINDING_LEVEL_EXT = 0x8F3B; + public const int GL_IMAGE_BINDING_LAYERED_EXT = 0x8F3C; + public const int GL_IMAGE_BINDING_LAYER_EXT = 0x8F3D; + public const int GL_IMAGE_BINDING_ACCESS_EXT = 0x8F3E; + public const int GL_IMAGE_1D_EXT = 0x904C; + public const int GL_IMAGE_2D_EXT = 0x904D; + public const int GL_IMAGE_3D_EXT = 0x904E; + public const int GL_IMAGE_2D_RECT_EXT = 0x904F; + public const int GL_IMAGE_CUBE_EXT = 0x9050; + public const int GL_IMAGE_BUFFER_EXT = 0x9051; + public const int GL_IMAGE_1D_ARRAY_EXT = 0x9052; + public const int GL_IMAGE_2D_ARRAY_EXT = 0x9053; + public const int GL_IMAGE_CUBE_MAP_ARRAY_EXT = 0x9054; + public const int GL_IMAGE_2D_MULTISAMPLE_EXT = 0x9055; + public const int GL_IMAGE_2D_MULTISAMPLE_ARRAY_EXT = 0x9056; + public const int GL_INT_IMAGE_1D_EXT = 0x9057; + public const int GL_INT_IMAGE_2D_EXT = 0x9058; + public const int GL_INT_IMAGE_3D_EXT = 0x9059; + public const int GL_INT_IMAGE_2D_RECT_EXT = 0x905A; + public const int GL_INT_IMAGE_CUBE_EXT = 0x905B; + public const int GL_INT_IMAGE_BUFFER_EXT = 0x905C; + public const int GL_INT_IMAGE_1D_ARRAY_EXT = 0x905D; + public const int GL_INT_IMAGE_2D_ARRAY_EXT = 0x905E; + public const int GL_INT_IMAGE_CUBE_MAP_ARRAY_EXT = 0x905F; + public const int GL_INT_IMAGE_2D_MULTISAMPLE_EXT = 0x9060; + public const int GL_INT_IMAGE_2D_MULTISAMPLE_ARRAY_EXT = 0x9061; + public const int GL_UNSIGNED_INT_IMAGE_1D_EXT = 0x9062; + public const int GL_UNSIGNED_INT_IMAGE_2D_EXT = 0x9063; + public const int GL_UNSIGNED_INT_IMAGE_3D_EXT = 0x9064; + public const int GL_UNSIGNED_INT_IMAGE_2D_RECT_EXT = 0x9065; + public const int GL_UNSIGNED_INT_IMAGE_CUBE_EXT = 0x9066; + public const int GL_UNSIGNED_INT_IMAGE_BUFFER_EXT = 0x9067; + public const int GL_UNSIGNED_INT_IMAGE_1D_ARRAY_EXT = 0x9068; + public const int GL_UNSIGNED_INT_IMAGE_2D_ARRAY_EXT = 0x9069; + public const int GL_UNSIGNED_INT_IMAGE_CUBE_MAP_ARRAY_EXT = 0x906A; + public const int GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_EXT = 0x906B; + public const int GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY_EXT = 0x906C; + public const int GL_MAX_IMAGE_SAMPLES_EXT = 0x906D; + public const int GL_IMAGE_BINDING_FORMAT_EXT = 0x906E; + public const int GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT_EXT = 0x00000001; + public const int GL_ELEMENT_ARRAY_BARRIER_BIT_EXT = 0x00000002; + public const int GL_UNIFORM_BARRIER_BIT_EXT = 0x00000004; + public const int GL_TEXTURE_FETCH_BARRIER_BIT_EXT = 0x00000008; + public const int GL_SHADER_IMAGE_ACCESS_BARRIER_BIT_EXT = 0x00000020; + public const int GL_COMMAND_BARRIER_BIT_EXT = 0x00000040; + public const int GL_PIXEL_BUFFER_BARRIER_BIT_EXT = 0x00000080; + public const int GL_TEXTURE_UPDATE_BARRIER_BIT_EXT = 0x00000100; + public const int GL_BUFFER_UPDATE_BARRIER_BIT_EXT = 0x00000200; + public const int GL_FRAMEBUFFER_BARRIER_BIT_EXT = 0x00000400; + public const int GL_TRANSFORM_FEEDBACK_BARRIER_BIT_EXT = 0x00000800; + public const int GL_ATOMIC_COUNTER_BARRIER_BIT_EXT = 0x00001000; + public const int GL_ALL_BARRIER_BITS_EXT = -1; + public const int GL_EXT_shader_integer_mix = 1; + public const int GL_EXT_shadow_funcs = 1; + public const int GL_EXT_shared_texture_palette = 1; + public const int GL_SHARED_TEXTURE_PALETTE_EXT = 0x81FB; + public const int GL_EXT_sparse_texture2 = 1; + public const int GL_EXT_stencil_clear_tag = 1; + public const int GL_STENCIL_TAG_BITS_EXT = 0x88F2; + public const int GL_STENCIL_CLEAR_TAG_VALUE_EXT = 0x88F3; + public const int GL_EXT_stencil_two_side = 1; + public const int GL_STENCIL_TEST_TWO_SIDE_EXT = 0x8910; + public const int GL_ACTIVE_STENCIL_FACE_EXT = 0x8911; + public const int GL_EXT_stencil_wrap = 1; + public const int GL_INCR_WRAP_EXT = 0x8507; + public const int GL_DECR_WRAP_EXT = 0x8508; + public const int GL_EXT_subtexture = 1; + public const int GL_EXT_texture = 1; + public const int GL_ALPHA4_EXT = 0x803B; + public const int GL_ALPHA8_EXT = 0x803C; + public const int GL_ALPHA12_EXT = 0x803D; + public const int GL_ALPHA16_EXT = 0x803E; + public const int GL_LUMINANCE4_EXT = 0x803F; + public const int GL_LUMINANCE8_EXT = 0x8040; + public const int GL_LUMINANCE12_EXT = 0x8041; + public const int GL_LUMINANCE16_EXT = 0x8042; + public const int GL_LUMINANCE4_ALPHA4_EXT = 0x8043; + public const int GL_LUMINANCE6_ALPHA2_EXT = 0x8044; + public const int GL_LUMINANCE8_ALPHA8_EXT = 0x8045; + public const int GL_LUMINANCE12_ALPHA4_EXT = 0x8046; + public const int GL_LUMINANCE12_ALPHA12_EXT = 0x8047; + public const int GL_LUMINANCE16_ALPHA16_EXT = 0x8048; + public const int GL_INTENSITY_EXT = 0x8049; + public const int GL_INTENSITY4_EXT = 0x804A; + public const int GL_INTENSITY8_EXT = 0x804B; + public const int GL_INTENSITY12_EXT = 0x804C; + public const int GL_INTENSITY16_EXT = 0x804D; + public const int GL_RGB2_EXT = 0x804E; + public const int GL_RGB4_EXT = 0x804F; + public const int GL_RGB5_EXT = 0x8050; + public const int GL_RGB8_EXT = 0x8051; + public const int GL_RGB10_EXT = 0x8052; + public const int GL_RGB12_EXT = 0x8053; + public const int GL_RGB16_EXT = 0x8054; + public const int GL_RGBA2_EXT = 0x8055; + public const int GL_RGBA4_EXT = 0x8056; + public const int GL_RGB5_A1_EXT = 0x8057; + public const int GL_RGBA8_EXT = 0x8058; + public const int GL_RGB10_A2_EXT = 0x8059; + public const int GL_RGBA12_EXT = 0x805A; + public const int GL_RGBA16_EXT = 0x805B; + public const int GL_TEXTURE_RED_SIZE_EXT = 0x805C; + public const int GL_TEXTURE_GREEN_SIZE_EXT = 0x805D; + public const int GL_TEXTURE_BLUE_SIZE_EXT = 0x805E; + public const int GL_TEXTURE_ALPHA_SIZE_EXT = 0x805F; + public const int GL_TEXTURE_LUMINANCE_SIZE_EXT = 0x8060; + public const int GL_TEXTURE_INTENSITY_SIZE_EXT = 0x8061; + public const int GL_REPLACE_EXT = 0x8062; + public const int GL_PROXY_TEXTURE_1D_EXT = 0x8063; + public const int GL_PROXY_TEXTURE_2D_EXT = 0x8064; + public const int GL_TEXTURE_TOO_LARGE_EXT = 0x8065; + public const int GL_EXT_texture3D = 1; + public const int GL_PACK_SKIP_IMAGES_EXT = 0x806B; + public const int GL_PACK_IMAGE_HEIGHT_EXT = 0x806C; + public const int GL_UNPACK_SKIP_IMAGES_EXT = 0x806D; + public const int GL_UNPACK_IMAGE_HEIGHT_EXT = 0x806E; + public const int GL_TEXTURE_3D_EXT = 0x806F; + public const int GL_PROXY_TEXTURE_3D_EXT = 0x8070; + public const int GL_TEXTURE_DEPTH_EXT = 0x8071; + public const int GL_TEXTURE_WRAP_R_EXT = 0x8072; + public const int GL_MAX_3D_TEXTURE_SIZE_EXT = 0x8073; + public const int GL_EXT_texture_array = 1; + public const int GL_TEXTURE_1D_ARRAY_EXT = 0x8C18; + public const int GL_PROXY_TEXTURE_1D_ARRAY_EXT = 0x8C19; + public const int GL_TEXTURE_2D_ARRAY_EXT = 0x8C1A; + public const int GL_PROXY_TEXTURE_2D_ARRAY_EXT = 0x8C1B; + public const int GL_TEXTURE_BINDING_1D_ARRAY_EXT = 0x8C1C; + public const int GL_TEXTURE_BINDING_2D_ARRAY_EXT = 0x8C1D; + public const int GL_MAX_ARRAY_TEXTURE_LAYERS_EXT = 0x88FF; + public const int GL_COMPARE_REF_DEPTH_TO_TEXTURE_EXT = 0x884E; + public const int GL_EXT_texture_buffer_object = 1; + public const int GL_TEXTURE_BUFFER_EXT = 0x8C2A; + public const int GL_MAX_TEXTURE_BUFFER_SIZE_EXT = 0x8C2B; + public const int GL_TEXTURE_BINDING_BUFFER_EXT = 0x8C2C; + public const int GL_TEXTURE_BUFFER_DATA_STORE_BINDING_EXT = 0x8C2D; + public const int GL_TEXTURE_BUFFER_FORMAT_EXT = 0x8C2E; + public const int GL_EXT_texture_compression_latc = 1; + public const int GL_COMPRESSED_LUMINANCE_LATC1_EXT = 0x8C70; + public const int GL_COMPRESSED_SIGNED_LUMINANCE_LATC1_EXT = 0x8C71; + public const int GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT = 0x8C72; + public const int GL_COMPRESSED_SIGNED_LUMINANCE_ALPHA_LATC2_EXT = 0x8C73; + public const int GL_EXT_texture_compression_rgtc = 1; + public const int GL_COMPRESSED_RED_RGTC1_EXT = 0x8DBB; + public const int GL_COMPRESSED_SIGNED_RED_RGTC1_EXT = 0x8DBC; + public const int GL_COMPRESSED_RED_GREEN_RGTC2_EXT = 0x8DBD; + public const int GL_COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT = 0x8DBE; + public const int GL_EXT_texture_compression_s3tc = 1; + public const int GL_COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0; + public const int GL_COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1; + public const int GL_COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2; + public const int GL_COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3; + public const int GL_EXT_texture_cube_map = 1; + public const int GL_NORMAL_MAP_EXT = 0x8511; + public const int GL_REFLECTION_MAP_EXT = 0x8512; + public const int GL_TEXTURE_CUBE_MAP_EXT = 0x8513; + public const int GL_TEXTURE_BINDING_CUBE_MAP_EXT = 0x8514; + public const int GL_TEXTURE_CUBE_MAP_POSITIVE_X_EXT = 0x8515; + public const int GL_TEXTURE_CUBE_MAP_NEGATIVE_X_EXT = 0x8516; + public const int GL_TEXTURE_CUBE_MAP_POSITIVE_Y_EXT = 0x8517; + public const int GL_TEXTURE_CUBE_MAP_NEGATIVE_Y_EXT = 0x8518; + public const int GL_TEXTURE_CUBE_MAP_POSITIVE_Z_EXT = 0x8519; + public const int GL_TEXTURE_CUBE_MAP_NEGATIVE_Z_EXT = 0x851A; + public const int GL_PROXY_TEXTURE_CUBE_MAP_EXT = 0x851B; + public const int GL_MAX_CUBE_MAP_TEXTURE_SIZE_EXT = 0x851C; + public const int GL_EXT_texture_env_add = 1; + public const int GL_EXT_texture_env_combine = 1; + public const int GL_COMBINE_EXT = 0x8570; + public const int GL_COMBINE_RGB_EXT = 0x8571; + public const int GL_COMBINE_ALPHA_EXT = 0x8572; + public const int GL_RGB_SCALE_EXT = 0x8573; + public const int GL_ADD_SIGNED_EXT = 0x8574; + public const int GL_INTERPOLATE_EXT = 0x8575; + public const int GL_CONSTANT_EXT = 0x8576; + public const int GL_PRIMARY_COLOR_EXT = 0x8577; + public const int GL_PREVIOUS_EXT = 0x8578; + public const int GL_SOURCE0_RGB_EXT = 0x8580; + public const int GL_SOURCE1_RGB_EXT = 0x8581; + public const int GL_SOURCE2_RGB_EXT = 0x8582; + public const int GL_SOURCE0_ALPHA_EXT = 0x8588; + public const int GL_SOURCE1_ALPHA_EXT = 0x8589; + public const int GL_SOURCE2_ALPHA_EXT = 0x858A; + public const int GL_OPERAND0_RGB_EXT = 0x8590; + public const int GL_OPERAND1_RGB_EXT = 0x8591; + public const int GL_OPERAND2_RGB_EXT = 0x8592; + public const int GL_OPERAND0_ALPHA_EXT = 0x8598; + public const int GL_OPERAND1_ALPHA_EXT = 0x8599; + public const int GL_OPERAND2_ALPHA_EXT = 0x859A; + public const int GL_EXT_texture_env_dot3 = 1; + public const int GL_DOT3_RGB_EXT = 0x8740; + public const int GL_DOT3_RGBA_EXT = 0x8741; + public const int GL_EXT_texture_filter_anisotropic = 1; + public const int GL_TEXTURE_MAX_ANISOTROPY_EXT = 0x84FE; + public const int GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT = 0x84FF; + public const int GL_EXT_texture_filter_minmax = 1; + public const int GL_TEXTURE_REDUCTION_MODE_EXT = 0x9366; + public const int GL_WEIGHTED_AVERAGE_EXT = 0x9367; + public const int GL_EXT_texture_integer = 1; + public const int GL_RGBA32UI_EXT = 0x8D70; + public const int GL_RGB32UI_EXT = 0x8D71; + public const int GL_ALPHA32UI_EXT = 0x8D72; + public const int GL_INTENSITY32UI_EXT = 0x8D73; + public const int GL_LUMINANCE32UI_EXT = 0x8D74; + public const int GL_LUMINANCE_ALPHA32UI_EXT = 0x8D75; + public const int GL_RGBA16UI_EXT = 0x8D76; + public const int GL_RGB16UI_EXT = 0x8D77; + public const int GL_ALPHA16UI_EXT = 0x8D78; + public const int GL_INTENSITY16UI_EXT = 0x8D79; + public const int GL_LUMINANCE16UI_EXT = 0x8D7A; + public const int GL_LUMINANCE_ALPHA16UI_EXT = 0x8D7B; + public const int GL_RGBA8UI_EXT = 0x8D7C; + public const int GL_RGB8UI_EXT = 0x8D7D; + public const int GL_ALPHA8UI_EXT = 0x8D7E; + public const int GL_INTENSITY8UI_EXT = 0x8D7F; + public const int GL_LUMINANCE8UI_EXT = 0x8D80; + public const int GL_LUMINANCE_ALPHA8UI_EXT = 0x8D81; + public const int GL_RGBA32I_EXT = 0x8D82; + public const int GL_RGB32I_EXT = 0x8D83; + public const int GL_ALPHA32I_EXT = 0x8D84; + public const int GL_INTENSITY32I_EXT = 0x8D85; + public const int GL_LUMINANCE32I_EXT = 0x8D86; + public const int GL_LUMINANCE_ALPHA32I_EXT = 0x8D87; + public const int GL_RGBA16I_EXT = 0x8D88; + public const int GL_RGB16I_EXT = 0x8D89; + public const int GL_ALPHA16I_EXT = 0x8D8A; + public const int GL_INTENSITY16I_EXT = 0x8D8B; + public const int GL_LUMINANCE16I_EXT = 0x8D8C; + public const int GL_LUMINANCE_ALPHA16I_EXT = 0x8D8D; + public const int GL_RGBA8I_EXT = 0x8D8E; + public const int GL_RGB8I_EXT = 0x8D8F; + public const int GL_ALPHA8I_EXT = 0x8D90; + public const int GL_INTENSITY8I_EXT = 0x8D91; + public const int GL_LUMINANCE8I_EXT = 0x8D92; + public const int GL_LUMINANCE_ALPHA8I_EXT = 0x8D93; + public const int GL_RED_INTEGER_EXT = 0x8D94; + public const int GL_GREEN_INTEGER_EXT = 0x8D95; + public const int GL_BLUE_INTEGER_EXT = 0x8D96; + public const int GL_ALPHA_INTEGER_EXT = 0x8D97; + public const int GL_RGB_INTEGER_EXT = 0x8D98; + public const int GL_RGBA_INTEGER_EXT = 0x8D99; + public const int GL_BGR_INTEGER_EXT = 0x8D9A; + public const int GL_BGRA_INTEGER_EXT = 0x8D9B; + public const int GL_LUMINANCE_INTEGER_EXT = 0x8D9C; + public const int GL_LUMINANCE_ALPHA_INTEGER_EXT = 0x8D9D; + public const int GL_RGBA_INTEGER_MODE_EXT = 0x8D9E; + public const int GL_EXT_texture_lod_bias = 1; + public const int GL_MAX_TEXTURE_LOD_BIAS_EXT = 0x84FD; + public const int GL_TEXTURE_FILTER_CONTROL_EXT = 0x8500; + public const int GL_TEXTURE_LOD_BIAS_EXT = 0x8501; + public const int GL_EXT_texture_mirror_clamp = 1; + public const int GL_MIRROR_CLAMP_EXT = 0x8742; + public const int GL_MIRROR_CLAMP_TO_EDGE_EXT = 0x8743; + public const int GL_MIRROR_CLAMP_TO_BORDER_EXT = 0x8912; + public const int GL_EXT_texture_object = 1; + public const int GL_TEXTURE_PRIORITY_EXT = 0x8066; + public const int GL_TEXTURE_RESIDENT_EXT = 0x8067; + public const int GL_TEXTURE_1D_BINDING_EXT = 0x8068; + public const int GL_TEXTURE_2D_BINDING_EXT = 0x8069; + public const int GL_TEXTURE_3D_BINDING_EXT = 0x806A; + public const int GL_EXT_texture_perturb_normal = 1; + public const int GL_PERTURB_EXT = 0x85AE; + public const int GL_TEXTURE_NORMAL_EXT = 0x85AF; + public const int GL_EXT_texture_sRGB = 1; + public const int GL_SRGB_EXT = 0x8C40; + public const int GL_SRGB8_EXT = 0x8C41; + public const int GL_SRGB_ALPHA_EXT = 0x8C42; + public const int GL_SRGB8_ALPHA8_EXT = 0x8C43; + public const int GL_SLUMINANCE_ALPHA_EXT = 0x8C44; + public const int GL_SLUMINANCE8_ALPHA8_EXT = 0x8C45; + public const int GL_SLUMINANCE_EXT = 0x8C46; + public const int GL_SLUMINANCE8_EXT = 0x8C47; + public const int GL_COMPRESSED_SRGB_EXT = 0x8C48; + public const int GL_COMPRESSED_SRGB_ALPHA_EXT = 0x8C49; + public const int GL_COMPRESSED_SLUMINANCE_EXT = 0x8C4A; + public const int GL_COMPRESSED_SLUMINANCE_ALPHA_EXT = 0x8C4B; + public const int GL_COMPRESSED_SRGB_S3TC_DXT1_EXT = 0x8C4C; + public const int GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT = 0x8C4D; + public const int GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT = 0x8C4E; + public const int GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT = 0x8C4F; + public const int GL_EXT_texture_sRGB_decode = 1; + public const int GL_TEXTURE_SRGB_DECODE_EXT = 0x8A48; + public const int GL_DECODE_EXT = 0x8A49; + public const int GL_SKIP_DECODE_EXT = 0x8A4A; + public const int GL_EXT_texture_shared_exponent = 1; + public const int GL_RGB9_E5_EXT = 0x8C3D; + public const int GL_UNSIGNED_INT_5_9_9_9_REV_EXT = 0x8C3E; + public const int GL_TEXTURE_SHARED_SIZE_EXT = 0x8C3F; + public const int GL_EXT_texture_snorm = 1; + public const int GL_ALPHA_SNORM = 0x9010; + public const int GL_LUMINANCE_SNORM = 0x9011; + public const int GL_LUMINANCE_ALPHA_SNORM = 0x9012; + public const int GL_INTENSITY_SNORM = 0x9013; + public const int GL_ALPHA8_SNORM = 0x9014; + public const int GL_LUMINANCE8_SNORM = 0x9015; + public const int GL_LUMINANCE8_ALPHA8_SNORM = 0x9016; + public const int GL_INTENSITY8_SNORM = 0x9017; + public const int GL_ALPHA16_SNORM = 0x9018; + public const int GL_LUMINANCE16_SNORM = 0x9019; + public const int GL_LUMINANCE16_ALPHA16_SNORM = 0x901A; + public const int GL_INTENSITY16_SNORM = 0x901B; + public const int GL_RED_SNORM = 0x8F90; + public const int GL_RG_SNORM = 0x8F91; + public const int GL_RGB_SNORM = 0x8F92; + public const int GL_RGBA_SNORM = 0x8F93; + public const int GL_EXT_texture_swizzle = 1; + public const int GL_TEXTURE_SWIZZLE_R_EXT = 0x8E42; + public const int GL_TEXTURE_SWIZZLE_G_EXT = 0x8E43; + public const int GL_TEXTURE_SWIZZLE_B_EXT = 0x8E44; + public const int GL_TEXTURE_SWIZZLE_A_EXT = 0x8E45; + public const int GL_TEXTURE_SWIZZLE_RGBA_EXT = 0x8E46; + public const int GL_EXT_timer_query = 1; + public const int GL_TIME_ELAPSED_EXT = 0x88BF; + public const int GL_EXT_transform_feedback = 1; + public const int GL_TRANSFORM_FEEDBACK_BUFFER_EXT = 0x8C8E; + public const int GL_TRANSFORM_FEEDBACK_BUFFER_START_EXT = 0x8C84; + public const int GL_TRANSFORM_FEEDBACK_BUFFER_SIZE_EXT = 0x8C85; + public const int GL_TRANSFORM_FEEDBACK_BUFFER_BINDING_EXT = 0x8C8F; + public const int GL_INTERLEAVED_ATTRIBS_EXT = 0x8C8C; + public const int GL_SEPARATE_ATTRIBS_EXT = 0x8C8D; + public const int GL_PRIMITIVES_GENERATED_EXT = 0x8C87; + public const int GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN_EXT = 0x8C88; + public const int GL_RASTERIZER_DISCARD_EXT = 0x8C89; + public const int GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS_EXT = 0x8C8A; + public const int GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS_EXT = 0x8C8B; + public const int GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS_EXT = 0x8C80; + public const int GL_TRANSFORM_FEEDBACK_VARYINGS_EXT = 0x8C83; + public const int GL_TRANSFORM_FEEDBACK_BUFFER_MODE_EXT = 0x8C7F; + public const int GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH_EXT = 0x8C76; + public const int GL_EXT_vertex_array = 1; + public const int GL_VERTEX_ARRAY_EXT = 0x8074; + public const int GL_NORMAL_ARRAY_EXT = 0x8075; + public const int GL_COLOR_ARRAY_EXT = 0x8076; + public const int GL_INDEX_ARRAY_EXT = 0x8077; + public const int GL_TEXTURE_COORD_ARRAY_EXT = 0x8078; + public const int GL_EDGE_FLAG_ARRAY_EXT = 0x8079; + public const int GL_VERTEX_ARRAY_SIZE_EXT = 0x807A; + public const int GL_VERTEX_ARRAY_TYPE_EXT = 0x807B; + public const int GL_VERTEX_ARRAY_STRIDE_EXT = 0x807C; + public const int GL_VERTEX_ARRAY_COUNT_EXT = 0x807D; + public const int GL_NORMAL_ARRAY_TYPE_EXT = 0x807E; + public const int GL_NORMAL_ARRAY_STRIDE_EXT = 0x807F; + public const int GL_NORMAL_ARRAY_COUNT_EXT = 0x8080; + public const int GL_COLOR_ARRAY_SIZE_EXT = 0x8081; + public const int GL_COLOR_ARRAY_TYPE_EXT = 0x8082; + public const int GL_COLOR_ARRAY_STRIDE_EXT = 0x8083; + public const int GL_COLOR_ARRAY_COUNT_EXT = 0x8084; + public const int GL_INDEX_ARRAY_TYPE_EXT = 0x8085; + public const int GL_INDEX_ARRAY_STRIDE_EXT = 0x8086; + public const int GL_INDEX_ARRAY_COUNT_EXT = 0x8087; + public const int GL_TEXTURE_COORD_ARRAY_SIZE_EXT = 0x8088; + public const int GL_TEXTURE_COORD_ARRAY_TYPE_EXT = 0x8089; + public const int GL_TEXTURE_COORD_ARRAY_STRIDE_EXT = 0x808A; + public const int GL_TEXTURE_COORD_ARRAY_COUNT_EXT = 0x808B; + public const int GL_EDGE_FLAG_ARRAY_STRIDE_EXT = 0x808C; + public const int GL_EDGE_FLAG_ARRAY_COUNT_EXT = 0x808D; + public const int GL_VERTEX_ARRAY_POINTER_EXT = 0x808E; + public const int GL_NORMAL_ARRAY_POINTER_EXT = 0x808F; + public const int GL_COLOR_ARRAY_POINTER_EXT = 0x8090; + public const int GL_INDEX_ARRAY_POINTER_EXT = 0x8091; + public const int GL_TEXTURE_COORD_ARRAY_POINTER_EXT = 0x8092; + public const int GL_EDGE_FLAG_ARRAY_POINTER_EXT = 0x8093; + public const int GL_EXT_vertex_array_bgra = 1; + public const int GL_EXT_vertex_attrib_64bit = 1; + public const int GL_DOUBLE_VEC2_EXT = 0x8FFC; + public const int GL_DOUBLE_VEC3_EXT = 0x8FFD; + public const int GL_DOUBLE_VEC4_EXT = 0x8FFE; + public const int GL_DOUBLE_MAT2_EXT = 0x8F46; + public const int GL_DOUBLE_MAT3_EXT = 0x8F47; + public const int GL_DOUBLE_MAT4_EXT = 0x8F48; + public const int GL_DOUBLE_MAT2x3_EXT = 0x8F49; + public const int GL_DOUBLE_MAT2x4_EXT = 0x8F4A; + public const int GL_DOUBLE_MAT3x2_EXT = 0x8F4B; + public const int GL_DOUBLE_MAT3x4_EXT = 0x8F4C; + public const int GL_DOUBLE_MAT4x2_EXT = 0x8F4D; + public const int GL_DOUBLE_MAT4x3_EXT = 0x8F4E; + public const int GL_EXT_vertex_shader = 1; + public const int GL_VERTEX_SHADER_EXT = 0x8780; + public const int GL_VERTEX_SHADER_BINDING_EXT = 0x8781; + public const int GL_OP_INDEX_EXT = 0x8782; + public const int GL_OP_NEGATE_EXT = 0x8783; + public const int GL_OP_DOT3_EXT = 0x8784; + public const int GL_OP_DOT4_EXT = 0x8785; + public const int GL_OP_MUL_EXT = 0x8786; + public const int GL_OP_ADD_EXT = 0x8787; + public const int GL_OP_MADD_EXT = 0x8788; + public const int GL_OP_FRAC_EXT = 0x8789; + public const int GL_OP_MAX_EXT = 0x878A; + public const int GL_OP_MIN_EXT = 0x878B; + public const int GL_OP_SET_GE_EXT = 0x878C; + public const int GL_OP_SET_LT_EXT = 0x878D; + public const int GL_OP_CLAMP_EXT = 0x878E; + public const int GL_OP_FLOOR_EXT = 0x878F; + public const int GL_OP_ROUND_EXT = 0x8790; + public const int GL_OP_EXP_BASE_2_EXT = 0x8791; + public const int GL_OP_LOG_BASE_2_EXT = 0x8792; + public const int GL_OP_POWER_EXT = 0x8793; + public const int GL_OP_RECIP_EXT = 0x8794; + public const int GL_OP_RECIP_SQRT_EXT = 0x8795; + public const int GL_OP_SUB_EXT = 0x8796; + public const int GL_OP_CROSS_PRODUCT_EXT = 0x8797; + public const int GL_OP_MULTIPLY_MATRIX_EXT = 0x8798; + public const int GL_OP_MOV_EXT = 0x8799; + public const int GL_OUTPUT_VERTEX_EXT = 0x879A; + public const int GL_OUTPUT_COLOR0_EXT = 0x879B; + public const int GL_OUTPUT_COLOR1_EXT = 0x879C; + public const int GL_OUTPUT_TEXTURE_COORD0_EXT = 0x879D; + public const int GL_OUTPUT_TEXTURE_COORD1_EXT = 0x879E; + public const int GL_OUTPUT_TEXTURE_COORD2_EXT = 0x879F; + public const int GL_OUTPUT_TEXTURE_COORD3_EXT = 0x87A0; + public const int GL_OUTPUT_TEXTURE_COORD4_EXT = 0x87A1; + public const int GL_OUTPUT_TEXTURE_COORD5_EXT = 0x87A2; + public const int GL_OUTPUT_TEXTURE_COORD6_EXT = 0x87A3; + public const int GL_OUTPUT_TEXTURE_COORD7_EXT = 0x87A4; + public const int GL_OUTPUT_TEXTURE_COORD8_EXT = 0x87A5; + public const int GL_OUTPUT_TEXTURE_COORD9_EXT = 0x87A6; + public const int GL_OUTPUT_TEXTURE_COORD10_EXT = 0x87A7; + public const int GL_OUTPUT_TEXTURE_COORD11_EXT = 0x87A8; + public const int GL_OUTPUT_TEXTURE_COORD12_EXT = 0x87A9; + public const int GL_OUTPUT_TEXTURE_COORD13_EXT = 0x87AA; + public const int GL_OUTPUT_TEXTURE_COORD14_EXT = 0x87AB; + public const int GL_OUTPUT_TEXTURE_COORD15_EXT = 0x87AC; + public const int GL_OUTPUT_TEXTURE_COORD16_EXT = 0x87AD; + public const int GL_OUTPUT_TEXTURE_COORD17_EXT = 0x87AE; + public const int GL_OUTPUT_TEXTURE_COORD18_EXT = 0x87AF; + public const int GL_OUTPUT_TEXTURE_COORD19_EXT = 0x87B0; + public const int GL_OUTPUT_TEXTURE_COORD20_EXT = 0x87B1; + public const int GL_OUTPUT_TEXTURE_COORD21_EXT = 0x87B2; + public const int GL_OUTPUT_TEXTURE_COORD22_EXT = 0x87B3; + public const int GL_OUTPUT_TEXTURE_COORD23_EXT = 0x87B4; + public const int GL_OUTPUT_TEXTURE_COORD24_EXT = 0x87B5; + public const int GL_OUTPUT_TEXTURE_COORD25_EXT = 0x87B6; + public const int GL_OUTPUT_TEXTURE_COORD26_EXT = 0x87B7; + public const int GL_OUTPUT_TEXTURE_COORD27_EXT = 0x87B8; + public const int GL_OUTPUT_TEXTURE_COORD28_EXT = 0x87B9; + public const int GL_OUTPUT_TEXTURE_COORD29_EXT = 0x87BA; + public const int GL_OUTPUT_TEXTURE_COORD30_EXT = 0x87BB; + public const int GL_OUTPUT_TEXTURE_COORD31_EXT = 0x87BC; + public const int GL_OUTPUT_FOG_EXT = 0x87BD; + public const int GL_SCALAR_EXT = 0x87BE; + public const int GL_VECTOR_EXT = 0x87BF; + public const int GL_MATRIX_EXT = 0x87C0; + public const int GL_VARIANT_EXT = 0x87C1; + public const int GL_INVARIANT_EXT = 0x87C2; + public const int GL_LOCAL_CONSTANT_EXT = 0x87C3; + public const int GL_LOCAL_EXT = 0x87C4; + public const int GL_MAX_VERTEX_SHADER_INSTRUCTIONS_EXT = 0x87C5; + public const int GL_MAX_VERTEX_SHADER_VARIANTS_EXT = 0x87C6; + public const int GL_MAX_VERTEX_SHADER_INVARIANTS_EXT = 0x87C7; + public const int GL_MAX_VERTEX_SHADER_LOCAL_CONSTANTS_EXT = 0x87C8; + public const int GL_MAX_VERTEX_SHADER_LOCALS_EXT = 0x87C9; + public const int GL_MAX_OPTIMIZED_VERTEX_SHADER_INSTRUCTIONS_EXT = 0x87CA; + public const int GL_MAX_OPTIMIZED_VERTEX_SHADER_VARIANTS_EXT = 0x87CB; + public const int GL_MAX_OPTIMIZED_VERTEX_SHADER_LOCAL_CONSTANTS_EXT = 0x87CC; + public const int GL_MAX_OPTIMIZED_VERTEX_SHADER_INVARIANTS_EXT = 0x87CD; + public const int GL_MAX_OPTIMIZED_VERTEX_SHADER_LOCALS_EXT = 0x87CE; + public const int GL_VERTEX_SHADER_INSTRUCTIONS_EXT = 0x87CF; + public const int GL_VERTEX_SHADER_VARIANTS_EXT = 0x87D0; + public const int GL_VERTEX_SHADER_INVARIANTS_EXT = 0x87D1; + public const int GL_VERTEX_SHADER_LOCAL_CONSTANTS_EXT = 0x87D2; + public const int GL_VERTEX_SHADER_LOCALS_EXT = 0x87D3; + public const int GL_VERTEX_SHADER_OPTIMIZED_EXT = 0x87D4; + public const int GL_X_EXT = 0x87D5; + public const int GL_Y_EXT = 0x87D6; + public const int GL_Z_EXT = 0x87D7; + public const int GL_W_EXT = 0x87D8; + public const int GL_NEGATIVE_X_EXT = 0x87D9; + public const int GL_NEGATIVE_Y_EXT = 0x87DA; + public const int GL_NEGATIVE_Z_EXT = 0x87DB; + public const int GL_NEGATIVE_W_EXT = 0x87DC; + public const int GL_ZERO_EXT = 0x87DD; + public const int GL_ONE_EXT = 0x87DE; + public const int GL_NEGATIVE_ONE_EXT = 0x87DF; + public const int GL_NORMALIZED_RANGE_EXT = 0x87E0; + public const int GL_FULL_RANGE_EXT = 0x87E1; + public const int GL_CURRENT_VERTEX_EXT = 0x87E2; + public const int GL_MVP_MATRIX_EXT = 0x87E3; + public const int GL_VARIANT_VALUE_EXT = 0x87E4; + public const int GL_VARIANT_DATATYPE_EXT = 0x87E5; + public const int GL_VARIANT_ARRAY_STRIDE_EXT = 0x87E6; + public const int GL_VARIANT_ARRAY_TYPE_EXT = 0x87E7; + public const int GL_VARIANT_ARRAY_EXT = 0x87E8; + public const int GL_VARIANT_ARRAY_POINTER_EXT = 0x87E9; + public const int GL_INVARIANT_VALUE_EXT = 0x87EA; + public const int GL_INVARIANT_DATATYPE_EXT = 0x87EB; + public const int GL_LOCAL_CONSTANT_VALUE_EXT = 0x87EC; + public const int GL_LOCAL_CONSTANT_DATATYPE_EXT = 0x87ED; + public const int GL_EXT_vertex_weighting = 1; + public const int GL_MODELVIEW0_STACK_DEPTH_EXT = 0x0BA3; + public const int GL_MODELVIEW1_STACK_DEPTH_EXT = 0x8502; + public const int GL_MODELVIEW0_MATRIX_EXT = 0x0BA6; + public const int GL_MODELVIEW1_MATRIX_EXT = 0x8506; + public const int GL_VERTEX_WEIGHTING_EXT = 0x8509; + public const int GL_MODELVIEW0_EXT = 0x1700; + public const int GL_MODELVIEW1_EXT = 0x850A; + public const int GL_CURRENT_VERTEX_WEIGHT_EXT = 0x850B; + public const int GL_VERTEX_WEIGHT_ARRAY_EXT = 0x850C; + public const int GL_VERTEX_WEIGHT_ARRAY_SIZE_EXT = 0x850D; + public const int GL_VERTEX_WEIGHT_ARRAY_TYPE_EXT = 0x850E; + public const int GL_VERTEX_WEIGHT_ARRAY_STRIDE_EXT = 0x850F; + public const int GL_VERTEX_WEIGHT_ARRAY_POINTER_EXT = 0x8510; + public const int GL_EXT_win32_keyed_mutex = 1; + public const int GL_EXT_window_rectangles = 1; + public const int GL_INCLUSIVE_EXT = 0x8F10; + public const int GL_EXCLUSIVE_EXT = 0x8F11; + public const int GL_WINDOW_RECTANGLE_EXT = 0x8F12; + public const int GL_WINDOW_RECTANGLE_MODE_EXT = 0x8F13; + public const int GL_MAX_WINDOW_RECTANGLES_EXT = 0x8F14; + public const int GL_NUM_WINDOW_RECTANGLES_EXT = 0x8F15; + public const int GL_EXT_x11_sync_object = 1; + public const int GL_SYNC_X11_FENCE_EXT = 0x90E1; + public const int GL_GREMEDY_frame_terminator = 1; + public const int GL_GREMEDY_string_marker = 1; + public const int GL_HP_convolution_border_modes = 1; + public const int GL_IGNORE_BORDER_HP = 0x8150; + public const int GL_CONSTANT_BORDER_HP = 0x8151; + public const int GL_REPLICATE_BORDER_HP = 0x8153; + public const int GL_CONVOLUTION_BORDER_COLOR_HP = 0x8154; + public const int GL_HP_image_transform = 1; + public const int GL_IMAGE_SCALE_X_HP = 0x8155; + public const int GL_IMAGE_SCALE_Y_HP = 0x8156; + public const int GL_IMAGE_TRANSLATE_X_HP = 0x8157; + public const int GL_IMAGE_TRANSLATE_Y_HP = 0x8158; + public const int GL_IMAGE_ROTATE_ANGLE_HP = 0x8159; + public const int GL_IMAGE_ROTATE_ORIGIN_X_HP = 0x815A; + public const int GL_IMAGE_ROTATE_ORIGIN_Y_HP = 0x815B; + public const int GL_IMAGE_MAG_FILTER_HP = 0x815C; + public const int GL_IMAGE_MIN_FILTER_HP = 0x815D; + public const int GL_IMAGE_CUBIC_WEIGHT_HP = 0x815E; + public const int GL_CUBIC_HP = 0x815F; + public const int GL_AVERAGE_HP = 0x8160; + public const int GL_IMAGE_TRANSFORM_2D_HP = 0x8161; + public const int GL_POST_IMAGE_TRANSFORM_COLOR_TABLE_HP = 0x8162; + public const int GL_PROXY_POST_IMAGE_TRANSFORM_COLOR_TABLE_HP = 0x8163; + public const int GL_HP_occlusion_test = 1; + public const int GL_OCCLUSION_TEST_HP = 0x8165; + public const int GL_OCCLUSION_TEST_RESULT_HP = 0x8166; + public const int GL_HP_texture_lighting = 1; + public const int GL_TEXTURE_LIGHTING_MODE_HP = 0x8167; + public const int GL_TEXTURE_POST_SPECULAR_HP = 0x8168; + public const int GL_TEXTURE_PRE_SPECULAR_HP = 0x8169; + public const int GL_IBM_cull_vertex = 1; + public const int GL_CULL_VERTEX_IBM = 103050; + public const int GL_IBM_multimode_draw_arrays = 1; + public const int GL_IBM_rasterpos_clip = 1; + public const int GL_RASTER_POSITION_UNCLIPPED_IBM = 0x19262; + public const int GL_IBM_static_data = 1; + public const int GL_ALL_STATIC_DATA_IBM = 103060; + public const int GL_STATIC_VERTEX_ARRAY_IBM = 103061; + public const int GL_IBM_texture_mirrored_repeat = 1; + public const int GL_MIRRORED_REPEAT_IBM = 0x8370; + public const int GL_IBM_vertex_array_lists = 1; + public const int GL_VERTEX_ARRAY_LIST_IBM = 103070; + public const int GL_NORMAL_ARRAY_LIST_IBM = 103071; + public const int GL_COLOR_ARRAY_LIST_IBM = 103072; + public const int GL_INDEX_ARRAY_LIST_IBM = 103073; + public const int GL_TEXTURE_COORD_ARRAY_LIST_IBM = 103074; + public const int GL_EDGE_FLAG_ARRAY_LIST_IBM = 103075; + public const int GL_FOG_COORDINATE_ARRAY_LIST_IBM = 103076; + public const int GL_SECONDARY_COLOR_ARRAY_LIST_IBM = 103077; + public const int GL_VERTEX_ARRAY_LIST_STRIDE_IBM = 103080; + public const int GL_NORMAL_ARRAY_LIST_STRIDE_IBM = 103081; + public const int GL_COLOR_ARRAY_LIST_STRIDE_IBM = 103082; + public const int GL_INDEX_ARRAY_LIST_STRIDE_IBM = 103083; + public const int GL_TEXTURE_COORD_ARRAY_LIST_STRIDE_IBM = 103084; + public const int GL_EDGE_FLAG_ARRAY_LIST_STRIDE_IBM = 103085; + public const int GL_FOG_COORDINATE_ARRAY_LIST_STRIDE_IBM = 103086; + public const int GL_SECONDARY_COLOR_ARRAY_LIST_STRIDE_IBM = 103087; + public const int GL_INGR_blend_func_separate = 1; + public const int GL_INGR_color_clamp = 1; + public const int GL_RED_MIN_CLAMP_INGR = 0x8560; + public const int GL_GREEN_MIN_CLAMP_INGR = 0x8561; + public const int GL_BLUE_MIN_CLAMP_INGR = 0x8562; + public const int GL_ALPHA_MIN_CLAMP_INGR = 0x8563; + public const int GL_RED_MAX_CLAMP_INGR = 0x8564; + public const int GL_GREEN_MAX_CLAMP_INGR = 0x8565; + public const int GL_BLUE_MAX_CLAMP_INGR = 0x8566; + public const int GL_ALPHA_MAX_CLAMP_INGR = 0x8567; + public const int GL_INGR_interlace_read = 1; + public const int GL_INTERLACE_READ_INGR = 0x8568; + public const int GL_INTEL_blackhole_render = 1; + public const int GL_BLACKHOLE_RENDER_INTEL = 0x83FC; + public const int GL_INTEL_conservative_rasterization = 1; + public const int GL_CONSERVATIVE_RASTERIZATION_INTEL = 0x83FE; + public const int GL_INTEL_fragment_shader_ordering = 1; + public const int GL_INTEL_framebuffer_CMAA = 1; + public const int GL_INTEL_map_texture = 1; + public const int GL_TEXTURE_MEMORY_LAYOUT_INTEL = 0x83FF; + public const int GL_LAYOUT_DEFAULT_INTEL = 0; + public const int GL_LAYOUT_LINEAR_INTEL = 1; + public const int GL_LAYOUT_LINEAR_CPU_CACHED_INTEL = 2; + public const int GL_INTEL_parallel_arrays = 1; + public const int GL_PARALLEL_ARRAYS_INTEL = 0x83F4; + public const int GL_VERTEX_ARRAY_PARALLEL_POINTERS_INTEL = 0x83F5; + public const int GL_NORMAL_ARRAY_PARALLEL_POINTERS_INTEL = 0x83F6; + public const int GL_COLOR_ARRAY_PARALLEL_POINTERS_INTEL = 0x83F7; + public const int GL_TEXTURE_COORD_ARRAY_PARALLEL_POINTERS_INTEL = 0x83F8; + public const int GL_INTEL_performance_query = 1; + public const int GL_PERFQUERY_SINGLE_CONTEXT_INTEL = 0x00000000; + public const int GL_PERFQUERY_GLOBAL_CONTEXT_INTEL = 0x00000001; + public const int GL_PERFQUERY_WAIT_INTEL = 0x83FB; + public const int GL_PERFQUERY_FLUSH_INTEL = 0x83FA; + public const int GL_PERFQUERY_DONOT_FLUSH_INTEL = 0x83F9; + public const int GL_PERFQUERY_COUNTER_EVENT_INTEL = 0x94F0; + public const int GL_PERFQUERY_COUNTER_DURATION_NORM_INTEL = 0x94F1; + public const int GL_PERFQUERY_COUNTER_DURATION_RAW_INTEL = 0x94F2; + public const int GL_PERFQUERY_COUNTER_THROUGHPUT_INTEL = 0x94F3; + public const int GL_PERFQUERY_COUNTER_RAW_INTEL = 0x94F4; + public const int GL_PERFQUERY_COUNTER_TIMESTAMP_INTEL = 0x94F5; + public const int GL_PERFQUERY_COUNTER_DATA_UINT32_INTEL = 0x94F8; + public const int GL_PERFQUERY_COUNTER_DATA_UINT64_INTEL = 0x94F9; + public const int GL_PERFQUERY_COUNTER_DATA_FLOAT_INTEL = 0x94FA; + public const int GL_PERFQUERY_COUNTER_DATA_DOUBLE_INTEL = 0x94FB; + public const int GL_PERFQUERY_COUNTER_DATA_BOOL32_INTEL = 0x94FC; + public const int GL_PERFQUERY_QUERY_NAME_LENGTH_MAX_INTEL = 0x94FD; + public const int GL_PERFQUERY_COUNTER_NAME_LENGTH_MAX_INTEL = 0x94FE; + public const int GL_PERFQUERY_COUNTER_DESC_LENGTH_MAX_INTEL = 0x94FF; + public const int GL_PERFQUERY_GPA_EXTENDED_COUNTERS_INTEL = 0x9500; + public const int GL_MESAX_texture_stack = 1; + public const int GL_TEXTURE_1D_STACK_MESAX = 0x8759; + public const int GL_TEXTURE_2D_STACK_MESAX = 0x875A; + public const int GL_PROXY_TEXTURE_1D_STACK_MESAX = 0x875B; + public const int GL_PROXY_TEXTURE_2D_STACK_MESAX = 0x875C; + public const int GL_TEXTURE_1D_STACK_BINDING_MESAX = 0x875D; + public const int GL_TEXTURE_2D_STACK_BINDING_MESAX = 0x875E; + public const int GL_MESA_pack_invert = 1; + public const int GL_PACK_INVERT_MESA = 0x8758; + public const int GL_MESA_program_binary_formats = 1; + public const int GL_PROGRAM_BINARY_FORMAT_MESA = 0x875F; + public const int GL_MESA_resize_buffers = 1; + public const int GL_MESA_shader_integer_functions = 1; + public const int GL_MESA_tile_raster_order = 1; + public const int GL_TILE_RASTER_ORDER_FIXED_MESA = 0x8BB8; + public const int GL_TILE_RASTER_ORDER_INCREASING_X_MESA = 0x8BB9; + public const int GL_TILE_RASTER_ORDER_INCREASING_Y_MESA = 0x8BBA; + public const int GL_MESA_window_pos = 1; + public const int GL_MESA_ycbcr_texture = 1; + public const int GL_UNSIGNED_SHORT_8_8_MESA = 0x85BA; + public const int GL_UNSIGNED_SHORT_8_8_REV_MESA = 0x85BB; + public const int GL_YCBCR_MESA = 0x8757; + public const int GL_NVX_blend_equation_advanced_multi_draw_buffers = 1; + public const int GL_NVX_conditional_render = 1; + public const int GL_NVX_gpu_memory_info = 1; + public const int GL_GPU_MEMORY_INFO_DEDICATED_VIDMEM_NVX = 0x9047; + public const int GL_GPU_MEMORY_INFO_TOTAL_AVAILABLE_MEMORY_NVX = 0x9048; + public const int GL_GPU_MEMORY_INFO_CURRENT_AVAILABLE_VIDMEM_NVX = 0x9049; + public const int GL_GPU_MEMORY_INFO_EVICTION_COUNT_NVX = 0x904A; + public const int GL_GPU_MEMORY_INFO_EVICTED_MEMORY_NVX = 0x904B; + public const int GL_NVX_linked_gpu_multicast = 1; + public const int GL_LGPU_SEPARATE_STORAGE_BIT_NVX = 0x0800; + public const int GL_MAX_LGPU_GPUS_NVX = 0x92BA; + public const int GL_NV_alpha_to_coverage_dither_control = 1; + public const int GL_ALPHA_TO_COVERAGE_DITHER_DEFAULT_NV = 0x934D; + public const int GL_ALPHA_TO_COVERAGE_DITHER_ENABLE_NV = 0x934E; + public const int GL_ALPHA_TO_COVERAGE_DITHER_DISABLE_NV = 0x934F; + public const int GL_ALPHA_TO_COVERAGE_DITHER_MODE_NV = 0x92BF; + public const int GL_NV_bindless_multi_draw_indirect = 1; + public const int GL_NV_bindless_multi_draw_indirect_count = 1; + public const int GL_NV_bindless_texture = 1; + public const int GL_NV_blend_equation_advanced = 1; + public const int GL_BLEND_OVERLAP_NV = 0x9281; + public const int GL_BLEND_PREMULTIPLIED_SRC_NV = 0x9280; + public const int GL_BLUE_NV = 0x1905; + public const int GL_COLORBURN_NV = 0x929A; + public const int GL_COLORDODGE_NV = 0x9299; + public const int GL_CONJOINT_NV = 0x9284; + public const int GL_CONTRAST_NV = 0x92A1; + public const int GL_DARKEN_NV = 0x9297; + public const int GL_DIFFERENCE_NV = 0x929E; + public const int GL_DISJOINT_NV = 0x9283; + public const int GL_DST_ATOP_NV = 0x928F; + public const int GL_DST_IN_NV = 0x928B; + public const int GL_DST_NV = 0x9287; + public const int GL_DST_OUT_NV = 0x928D; + public const int GL_DST_OVER_NV = 0x9289; + public const int GL_EXCLUSION_NV = 0x92A0; + public const int GL_GREEN_NV = 0x1904; + public const int GL_HARDLIGHT_NV = 0x929B; + public const int GL_HARDMIX_NV = 0x92A9; + public const int GL_HSL_COLOR_NV = 0x92AF; + public const int GL_HSL_HUE_NV = 0x92AD; + public const int GL_HSL_LUMINOSITY_NV = 0x92B0; + public const int GL_HSL_SATURATION_NV = 0x92AE; + public const int GL_INVERT_OVG_NV = 0x92B4; + public const int GL_INVERT_RGB_NV = 0x92A3; + public const int GL_LIGHTEN_NV = 0x9298; + public const int GL_LINEARBURN_NV = 0x92A5; + public const int GL_LINEARDODGE_NV = 0x92A4; + public const int GL_LINEARLIGHT_NV = 0x92A7; + public const int GL_MINUS_CLAMPED_NV = 0x92B3; + public const int GL_MINUS_NV = 0x929F; + public const int GL_MULTIPLY_NV = 0x9294; + public const int GL_OVERLAY_NV = 0x9296; + public const int GL_PINLIGHT_NV = 0x92A8; + public const int GL_PLUS_CLAMPED_ALPHA_NV = 0x92B2; + public const int GL_PLUS_CLAMPED_NV = 0x92B1; + public const int GL_PLUS_DARKER_NV = 0x9292; + public const int GL_PLUS_NV = 0x9291; + public const int GL_RED_NV = 0x1903; + public const int GL_SCREEN_NV = 0x9295; + public const int GL_SOFTLIGHT_NV = 0x929C; + public const int GL_SRC_ATOP_NV = 0x928E; + public const int GL_SRC_IN_NV = 0x928A; + public const int GL_SRC_NV = 0x9286; + public const int GL_SRC_OUT_NV = 0x928C; + public const int GL_SRC_OVER_NV = 0x9288; + public const int GL_UNCORRELATED_NV = 0x9282; + public const int GL_VIVIDLIGHT_NV = 0x92A6; + public const int GL_XOR_NV = 0x1506; + public const int GL_NV_blend_equation_advanced_coherent = 1; + public const int GL_BLEND_ADVANCED_COHERENT_NV = 0x9285; + public const int GL_NV_blend_minmax_factor = 1; + public const int GL_NV_blend_square = 1; + public const int GL_NV_clip_space_w_scaling = 1; + public const int GL_VIEWPORT_POSITION_W_SCALE_NV = 0x937C; + public const int GL_VIEWPORT_POSITION_W_SCALE_X_COEFF_NV = 0x937D; + public const int GL_VIEWPORT_POSITION_W_SCALE_Y_COEFF_NV = 0x937E; + public const int GL_NV_command_list = 1; + public const int GL_TERMINATE_SEQUENCE_COMMAND_NV = 0x0000; + public const int GL_NOP_COMMAND_NV = 0x0001; + public const int GL_DRAW_ELEMENTS_COMMAND_NV = 0x0002; + public const int GL_DRAW_ARRAYS_COMMAND_NV = 0x0003; + public const int GL_DRAW_ELEMENTS_STRIP_COMMAND_NV = 0x0004; + public const int GL_DRAW_ARRAYS_STRIP_COMMAND_NV = 0x0005; + public const int GL_DRAW_ELEMENTS_INSTANCED_COMMAND_NV = 0x0006; + public const int GL_DRAW_ARRAYS_INSTANCED_COMMAND_NV = 0x0007; + public const int GL_ELEMENT_ADDRESS_COMMAND_NV = 0x0008; + public const int GL_ATTRIBUTE_ADDRESS_COMMAND_NV = 0x0009; + public const int GL_UNIFORM_ADDRESS_COMMAND_NV = 0x000A; + public const int GL_BLEND_COLOR_COMMAND_NV = 0x000B; + public const int GL_STENCIL_REF_COMMAND_NV = 0x000C; + public const int GL_LINE_WIDTH_COMMAND_NV = 0x000D; + public const int GL_POLYGON_OFFSET_COMMAND_NV = 0x000E; + public const int GL_ALPHA_REF_COMMAND_NV = 0x000F; + public const int GL_VIEWPORT_COMMAND_NV = 0x0010; + public const int GL_SCISSOR_COMMAND_NV = 0x0011; + public const int GL_FRONT_FACE_COMMAND_NV = 0x0012; + public const int GL_NV_compute_program5 = 1; + public const int GL_COMPUTE_PROGRAM_NV = 0x90FB; + public const int GL_COMPUTE_PROGRAM_PARAMETER_BUFFER_NV = 0x90FC; + public const int GL_NV_conditional_render = 1; + public const int GL_QUERY_WAIT_NV = 0x8E13; + public const int GL_QUERY_NO_WAIT_NV = 0x8E14; + public const int GL_QUERY_BY_REGION_WAIT_NV = 0x8E15; + public const int GL_QUERY_BY_REGION_NO_WAIT_NV = 0x8E16; + public const int GL_NV_conservative_raster = 1; + public const int GL_CONSERVATIVE_RASTERIZATION_NV = 0x9346; + public const int GL_SUBPIXEL_PRECISION_BIAS_X_BITS_NV = 0x9347; + public const int GL_SUBPIXEL_PRECISION_BIAS_Y_BITS_NV = 0x9348; + public const int GL_MAX_SUBPIXEL_PRECISION_BIAS_BITS_NV = 0x9349; + public const int GL_NV_conservative_raster_dilate = 1; + public const int GL_CONSERVATIVE_RASTER_DILATE_NV = 0x9379; + public const int GL_CONSERVATIVE_RASTER_DILATE_RANGE_NV = 0x937A; + public const int GL_CONSERVATIVE_RASTER_DILATE_GRANULARITY_NV = 0x937B; + public const int GL_NV_conservative_raster_pre_snap = 1; + public const int GL_CONSERVATIVE_RASTER_MODE_PRE_SNAP_NV = 0x9550; + public const int GL_NV_conservative_raster_pre_snap_triangles = 1; + public const int GL_CONSERVATIVE_RASTER_MODE_NV = 0x954D; + public const int GL_CONSERVATIVE_RASTER_MODE_POST_SNAP_NV = 0x954E; + public const int GL_CONSERVATIVE_RASTER_MODE_PRE_SNAP_TRIANGLES_NV = 0x954F; + public const int GL_NV_conservative_raster_underestimation = 1; + public const int GL_NV_copy_depth_to_color = 1; + public const int GL_DEPTH_STENCIL_TO_RGBA_NV = 0x886E; + public const int GL_DEPTH_STENCIL_TO_BGRA_NV = 0x886F; + public const int GL_NV_copy_image = 1; + public const int GL_NV_deep_texture3D = 1; + public const int GL_MAX_DEEP_3D_TEXTURE_WIDTH_HEIGHT_NV = 0x90D0; + public const int GL_MAX_DEEP_3D_TEXTURE_DEPTH_NV = 0x90D1; + public const int GL_NV_depth_buffer_float = 1; + public const int GL_DEPTH_COMPONENT32F_NV = 0x8DAB; + public const int GL_DEPTH32F_STENCIL8_NV = 0x8DAC; + public const int GL_FLOAT_32_UNSIGNED_INT_24_8_REV_NV = 0x8DAD; + public const int GL_DEPTH_BUFFER_FLOAT_MODE_NV = 0x8DAF; + public const int GL_NV_depth_clamp = 1; + public const int GL_DEPTH_CLAMP_NV = 0x864F; + public const int GL_NV_draw_texture = 1; + public const int GL_NV_draw_vulkan_image = 1; + public const int GL_NV_evaluators = 1; + public const int GL_EVAL_2D_NV = 0x86C0; + public const int GL_EVAL_TRIANGULAR_2D_NV = 0x86C1; + public const int GL_MAP_TESSELLATION_NV = 0x86C2; + public const int GL_MAP_ATTRIB_U_ORDER_NV = 0x86C3; + public const int GL_MAP_ATTRIB_V_ORDER_NV = 0x86C4; + public const int GL_EVAL_FRACTIONAL_TESSELLATION_NV = 0x86C5; + public const int GL_EVAL_VERTEX_ATTRIB0_NV = 0x86C6; + public const int GL_EVAL_VERTEX_ATTRIB1_NV = 0x86C7; + public const int GL_EVAL_VERTEX_ATTRIB2_NV = 0x86C8; + public const int GL_EVAL_VERTEX_ATTRIB3_NV = 0x86C9; + public const int GL_EVAL_VERTEX_ATTRIB4_NV = 0x86CA; + public const int GL_EVAL_VERTEX_ATTRIB5_NV = 0x86CB; + public const int GL_EVAL_VERTEX_ATTRIB6_NV = 0x86CC; + public const int GL_EVAL_VERTEX_ATTRIB7_NV = 0x86CD; + public const int GL_EVAL_VERTEX_ATTRIB8_NV = 0x86CE; + public const int GL_EVAL_VERTEX_ATTRIB9_NV = 0x86CF; + public const int GL_EVAL_VERTEX_ATTRIB10_NV = 0x86D0; + public const int GL_EVAL_VERTEX_ATTRIB11_NV = 0x86D1; + public const int GL_EVAL_VERTEX_ATTRIB12_NV = 0x86D2; + public const int GL_EVAL_VERTEX_ATTRIB13_NV = 0x86D3; + public const int GL_EVAL_VERTEX_ATTRIB14_NV = 0x86D4; + public const int GL_EVAL_VERTEX_ATTRIB15_NV = 0x86D5; + public const int GL_MAX_MAP_TESSELLATION_NV = 0x86D6; + public const int GL_MAX_RATIONAL_EVAL_ORDER_NV = 0x86D7; + public const int GL_NV_explicit_multisample = 1; + public const int GL_SAMPLE_POSITION_NV = 0x8E50; + public const int GL_SAMPLE_MASK_NV = 0x8E51; + public const int GL_SAMPLE_MASK_VALUE_NV = 0x8E52; + public const int GL_TEXTURE_BINDING_RENDERBUFFER_NV = 0x8E53; + public const int GL_TEXTURE_RENDERBUFFER_DATA_STORE_BINDING_NV = 0x8E54; + public const int GL_TEXTURE_RENDERBUFFER_NV = 0x8E55; + public const int GL_SAMPLER_RENDERBUFFER_NV = 0x8E56; + public const int GL_INT_SAMPLER_RENDERBUFFER_NV = 0x8E57; + public const int GL_UNSIGNED_INT_SAMPLER_RENDERBUFFER_NV = 0x8E58; + public const int GL_MAX_SAMPLE_MASK_WORDS_NV = 0x8E59; + public const int GL_NV_fence = 1; + public const int GL_ALL_COMPLETED_NV = 0x84F2; + public const int GL_FENCE_STATUS_NV = 0x84F3; + public const int GL_FENCE_CONDITION_NV = 0x84F4; + public const int GL_NV_fill_rectangle = 1; + public const int GL_FILL_RECTANGLE_NV = 0x933C; + public const int GL_NV_float_buffer = 1; + public const int GL_FLOAT_R_NV = 0x8880; + public const int GL_FLOAT_RG_NV = 0x8881; + public const int GL_FLOAT_RGB_NV = 0x8882; + public const int GL_FLOAT_RGBA_NV = 0x8883; + public const int GL_FLOAT_R16_NV = 0x8884; + public const int GL_FLOAT_R32_NV = 0x8885; + public const int GL_FLOAT_RG16_NV = 0x8886; + public const int GL_FLOAT_RG32_NV = 0x8887; + public const int GL_FLOAT_RGB16_NV = 0x8888; + public const int GL_FLOAT_RGB32_NV = 0x8889; + public const int GL_FLOAT_RGBA16_NV = 0x888A; + public const int GL_FLOAT_RGBA32_NV = 0x888B; + public const int GL_TEXTURE_FLOAT_COMPONENTS_NV = 0x888C; + public const int GL_FLOAT_CLEAR_COLOR_VALUE_NV = 0x888D; + public const int GL_FLOAT_RGBA_MODE_NV = 0x888E; + public const int GL_NV_fog_distance = 1; + public const int GL_FOG_DISTANCE_MODE_NV = 0x855A; + public const int GL_EYE_RADIAL_NV = 0x855B; + public const int GL_EYE_PLANE_ABSOLUTE_NV = 0x855C; + public const int GL_NV_fragment_coverage_to_color = 1; + public const int GL_FRAGMENT_COVERAGE_TO_COLOR_NV = 0x92DD; + public const int GL_FRAGMENT_COVERAGE_COLOR_NV = 0x92DE; + public const int GL_NV_fragment_program = 1; + public const int GL_MAX_FRAGMENT_PROGRAM_LOCAL_PARAMETERS_NV = 0x8868; + public const int GL_FRAGMENT_PROGRAM_NV = 0x8870; + public const int GL_MAX_TEXTURE_COORDS_NV = 0x8871; + public const int GL_MAX_TEXTURE_IMAGE_UNITS_NV = 0x8872; + public const int GL_FRAGMENT_PROGRAM_BINDING_NV = 0x8873; + public const int GL_PROGRAM_ERROR_STRING_NV = 0x8874; + public const int GL_NV_fragment_program2 = 1; + public const int GL_MAX_PROGRAM_EXEC_INSTRUCTIONS_NV = 0x88F4; + public const int GL_MAX_PROGRAM_CALL_DEPTH_NV = 0x88F5; + public const int GL_MAX_PROGRAM_IF_DEPTH_NV = 0x88F6; + public const int GL_MAX_PROGRAM_LOOP_DEPTH_NV = 0x88F7; + public const int GL_MAX_PROGRAM_LOOP_COUNT_NV = 0x88F8; + public const int GL_NV_fragment_program4 = 1; + public const int GL_NV_fragment_program_option = 1; + public const int GL_NV_fragment_shader_interlock = 1; + public const int GL_NV_framebuffer_mixed_samples = 1; + public const int GL_COVERAGE_MODULATION_TABLE_NV = 0x9331; + public const int GL_COLOR_SAMPLES_NV = 0x8E20; + public const int GL_DEPTH_SAMPLES_NV = 0x932D; + public const int GL_STENCIL_SAMPLES_NV = 0x932E; + public const int GL_MIXED_DEPTH_SAMPLES_SUPPORTED_NV = 0x932F; + public const int GL_MIXED_STENCIL_SAMPLES_SUPPORTED_NV = 0x9330; + public const int GL_COVERAGE_MODULATION_NV = 0x9332; + public const int GL_COVERAGE_MODULATION_TABLE_SIZE_NV = 0x9333; + public const int GL_NV_framebuffer_multisample_coverage = 1; + public const int GL_RENDERBUFFER_COVERAGE_SAMPLES_NV = 0x8CAB; + public const int GL_RENDERBUFFER_COLOR_SAMPLES_NV = 0x8E10; + public const int GL_MAX_MULTISAMPLE_COVERAGE_MODES_NV = 0x8E11; + public const int GL_MULTISAMPLE_COVERAGE_MODES_NV = 0x8E12; + public const int GL_NV_geometry_program4 = 1; + public const int GL_GEOMETRY_PROGRAM_NV = 0x8C26; + public const int GL_MAX_PROGRAM_OUTPUT_VERTICES_NV = 0x8C27; + public const int GL_MAX_PROGRAM_TOTAL_OUTPUT_COMPONENTS_NV = 0x8C28; + public const int GL_NV_geometry_shader4 = 1; + public const int GL_NV_geometry_shader_passthrough = 1; + public const int GL_NV_gpu_multicast = 1; + public const int GL_PER_GPU_STORAGE_BIT_NV = 0x0800; + public const int GL_MULTICAST_GPUS_NV = 0x92BA; + public const int GL_RENDER_GPU_MASK_NV = 0x9558; + public const int GL_PER_GPU_STORAGE_NV = 0x9548; + public const int GL_MULTICAST_PROGRAMMABLE_SAMPLE_LOCATION_NV = 0x9549; + public const int GL_NV_gpu_program4 = 1; + public const int GL_MIN_PROGRAM_TEXEL_OFFSET_NV = 0x8904; + public const int GL_MAX_PROGRAM_TEXEL_OFFSET_NV = 0x8905; + public const int GL_PROGRAM_ATTRIB_COMPONENTS_NV = 0x8906; + public const int GL_PROGRAM_RESULT_COMPONENTS_NV = 0x8907; + public const int GL_MAX_PROGRAM_ATTRIB_COMPONENTS_NV = 0x8908; + public const int GL_MAX_PROGRAM_RESULT_COMPONENTS_NV = 0x8909; + public const int GL_MAX_PROGRAM_GENERIC_ATTRIBS_NV = 0x8DA5; + public const int GL_MAX_PROGRAM_GENERIC_RESULTS_NV = 0x8DA6; + public const int GL_NV_gpu_program5 = 1; + public const int GL_MAX_GEOMETRY_PROGRAM_INVOCATIONS_NV = 0x8E5A; + public const int GL_MIN_FRAGMENT_INTERPOLATION_OFFSET_NV = 0x8E5B; + public const int GL_MAX_FRAGMENT_INTERPOLATION_OFFSET_NV = 0x8E5C; + public const int GL_FRAGMENT_PROGRAM_INTERPOLATION_OFFSET_BITS_NV = 0x8E5D; + public const int GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET_NV = 0x8E5E; + public const int GL_MAX_PROGRAM_TEXTURE_GATHER_OFFSET_NV = 0x8E5F; + public const int GL_MAX_PROGRAM_SUBROUTINE_PARAMETERS_NV = 0x8F44; + public const int GL_MAX_PROGRAM_SUBROUTINE_NUM_NV = 0x8F45; + public const int GL_NV_gpu_program5_mem_extended = 1; + public const int GL_NV_gpu_shader5 = 1; + public const int GL_NV_half_float = 1; + public const int GL_HALF_FLOAT_NV = 0x140B; + public const int GL_NV_internalformat_sample_query = 1; + public const int GL_MULTISAMPLES_NV = 0x9371; + public const int GL_SUPERSAMPLE_SCALE_X_NV = 0x9372; + public const int GL_SUPERSAMPLE_SCALE_Y_NV = 0x9373; + public const int GL_CONFORMANT_NV = 0x9374; + public const int GL_NV_light_max_exponent = 1; + public const int GL_MAX_SHININESS_NV = 0x8504; + public const int GL_MAX_SPOT_EXPONENT_NV = 0x8505; + public const int GL_NV_multisample_coverage = 1; + public const int GL_NV_multisample_filter_hint = 1; + public const int GL_MULTISAMPLE_FILTER_HINT_NV = 0x8534; + public const int GL_NV_occlusion_query = 1; + public const int GL_PIXEL_COUNTER_BITS_NV = 0x8864; + public const int GL_CURRENT_OCCLUSION_QUERY_ID_NV = 0x8865; + public const int GL_PIXEL_COUNT_NV = 0x8866; + public const int GL_PIXEL_COUNT_AVAILABLE_NV = 0x8867; + public const int GL_NV_packed_depth_stencil = 1; + public const int GL_DEPTH_STENCIL_NV = 0x84F9; + public const int GL_UNSIGNED_INT_24_8_NV = 0x84FA; + public const int GL_NV_parameter_buffer_object = 1; + public const int GL_MAX_PROGRAM_PARAMETER_BUFFER_BINDINGS_NV = 0x8DA0; + public const int GL_MAX_PROGRAM_PARAMETER_BUFFER_SIZE_NV = 0x8DA1; + public const int GL_VERTEX_PROGRAM_PARAMETER_BUFFER_NV = 0x8DA2; + public const int GL_GEOMETRY_PROGRAM_PARAMETER_BUFFER_NV = 0x8DA3; + public const int GL_FRAGMENT_PROGRAM_PARAMETER_BUFFER_NV = 0x8DA4; + public const int GL_NV_parameter_buffer_object2 = 1; + public const int GL_NV_path_rendering = 1; + public const int GL_PATH_FORMAT_SVG_NV = 0x9070; + public const int GL_PATH_FORMAT_PS_NV = 0x9071; + public const int GL_STANDARD_FONT_NAME_NV = 0x9072; + public const int GL_SYSTEM_FONT_NAME_NV = 0x9073; + public const int GL_FILE_NAME_NV = 0x9074; + public const int GL_PATH_STROKE_WIDTH_NV = 0x9075; + public const int GL_PATH_END_CAPS_NV = 0x9076; + public const int GL_PATH_INITIAL_END_CAP_NV = 0x9077; + public const int GL_PATH_TERMINAL_END_CAP_NV = 0x9078; + public const int GL_PATH_JOIN_STYLE_NV = 0x9079; + public const int GL_PATH_MITER_LIMIT_NV = 0x907A; + public const int GL_PATH_DASH_CAPS_NV = 0x907B; + public const int GL_PATH_INITIAL_DASH_CAP_NV = 0x907C; + public const int GL_PATH_TERMINAL_DASH_CAP_NV = 0x907D; + public const int GL_PATH_DASH_OFFSET_NV = 0x907E; + public const int GL_PATH_CLIENT_LENGTH_NV = 0x907F; + public const int GL_PATH_FILL_MODE_NV = 0x9080; + public const int GL_PATH_FILL_MASK_NV = 0x9081; + public const int GL_PATH_FILL_COVER_MODE_NV = 0x9082; + public const int GL_PATH_STROKE_COVER_MODE_NV = 0x9083; + public const int GL_PATH_STROKE_MASK_NV = 0x9084; + public const int GL_COUNT_UP_NV = 0x9088; + public const int GL_COUNT_DOWN_NV = 0x9089; + public const int GL_PATH_OBJECT_BOUNDING_BOX_NV = 0x908A; + public const int GL_CONVEX_HULL_NV = 0x908B; + public const int GL_BOUNDING_BOX_NV = 0x908D; + public const int GL_TRANSLATE_X_NV = 0x908E; + public const int GL_TRANSLATE_Y_NV = 0x908F; + public const int GL_TRANSLATE_2D_NV = 0x9090; + public const int GL_TRANSLATE_3D_NV = 0x9091; + public const int GL_AFFINE_2D_NV = 0x9092; + public const int GL_AFFINE_3D_NV = 0x9094; + public const int GL_TRANSPOSE_AFFINE_2D_NV = 0x9096; + public const int GL_TRANSPOSE_AFFINE_3D_NV = 0x9098; + public const int GL_UTF8_NV = 0x909A; + public const int GL_UTF16_NV = 0x909B; + public const int GL_BOUNDING_BOX_OF_BOUNDING_BOXES_NV = 0x909C; + public const int GL_PATH_COMMAND_COUNT_NV = 0x909D; + public const int GL_PATH_COORD_COUNT_NV = 0x909E; + public const int GL_PATH_DASH_ARRAY_COUNT_NV = 0x909F; + public const int GL_PATH_COMPUTED_LENGTH_NV = 0x90A0; + public const int GL_PATH_FILL_BOUNDING_BOX_NV = 0x90A1; + public const int GL_PATH_STROKE_BOUNDING_BOX_NV = 0x90A2; + public const int GL_SQUARE_NV = 0x90A3; + public const int GL_ROUND_NV = 0x90A4; + public const int GL_TRIANGULAR_NV = 0x90A5; + public const int GL_BEVEL_NV = 0x90A6; + public const int GL_MITER_REVERT_NV = 0x90A7; + public const int GL_MITER_TRUNCATE_NV = 0x90A8; + public const int GL_SKIP_MISSING_GLYPH_NV = 0x90A9; + public const int GL_USE_MISSING_GLYPH_NV = 0x90AA; + public const int GL_PATH_ERROR_POSITION_NV = 0x90AB; + public const int GL_ACCUM_ADJACENT_PAIRS_NV = 0x90AD; + public const int GL_ADJACENT_PAIRS_NV = 0x90AE; + public const int GL_FIRST_TO_REST_NV = 0x90AF; + public const int GL_PATH_GEN_MODE_NV = 0x90B0; + public const int GL_PATH_GEN_COEFF_NV = 0x90B1; + public const int GL_PATH_GEN_COMPONENTS_NV = 0x90B3; + public const int GL_PATH_STENCIL_FUNC_NV = 0x90B7; + public const int GL_PATH_STENCIL_REF_NV = 0x90B8; + public const int GL_PATH_STENCIL_VALUE_MASK_NV = 0x90B9; + public const int GL_PATH_STENCIL_DEPTH_OFFSET_FACTOR_NV = 0x90BD; + public const int GL_PATH_STENCIL_DEPTH_OFFSET_UNITS_NV = 0x90BE; + public const int GL_PATH_COVER_DEPTH_FUNC_NV = 0x90BF; + public const int GL_PATH_DASH_OFFSET_RESET_NV = 0x90B4; + public const int GL_MOVE_TO_RESETS_NV = 0x90B5; + public const int GL_MOVE_TO_CONTINUES_NV = 0x90B6; + public const int GL_CLOSE_PATH_NV = 0x00; + public const int GL_MOVE_TO_NV = 0x02; + public const int GL_RELATIVE_MOVE_TO_NV = 0x03; + public const int GL_LINE_TO_NV = 0x04; + public const int GL_RELATIVE_LINE_TO_NV = 0x05; + public const int GL_HORIZONTAL_LINE_TO_NV = 0x06; + public const int GL_RELATIVE_HORIZONTAL_LINE_TO_NV = 0x07; + public const int GL_VERTICAL_LINE_TO_NV = 0x08; + public const int GL_RELATIVE_VERTICAL_LINE_TO_NV = 0x09; + public const int GL_QUADRATIC_CURVE_TO_NV = 0x0A; + public const int GL_RELATIVE_QUADRATIC_CURVE_TO_NV = 0x0B; + public const int GL_CUBIC_CURVE_TO_NV = 0x0C; + public const int GL_RELATIVE_CUBIC_CURVE_TO_NV = 0x0D; + public const int GL_SMOOTH_QUADRATIC_CURVE_TO_NV = 0x0E; + public const int GL_RELATIVE_SMOOTH_QUADRATIC_CURVE_TO_NV = 0x0F; + public const int GL_SMOOTH_CUBIC_CURVE_TO_NV = 0x10; + public const int GL_RELATIVE_SMOOTH_CUBIC_CURVE_TO_NV = 0x11; + public const int GL_SMALL_CCW_ARC_TO_NV = 0x12; + public const int GL_RELATIVE_SMALL_CCW_ARC_TO_NV = 0x13; + public const int GL_SMALL_CW_ARC_TO_NV = 0x14; + public const int GL_RELATIVE_SMALL_CW_ARC_TO_NV = 0x15; + public const int GL_LARGE_CCW_ARC_TO_NV = 0x16; + public const int GL_RELATIVE_LARGE_CCW_ARC_TO_NV = 0x17; + public const int GL_LARGE_CW_ARC_TO_NV = 0x18; + public const int GL_RELATIVE_LARGE_CW_ARC_TO_NV = 0x19; + public const int GL_RESTART_PATH_NV = 0xF0; + public const int GL_DUP_FIRST_CUBIC_CURVE_TO_NV = 0xF2; + public const int GL_DUP_LAST_CUBIC_CURVE_TO_NV = 0xF4; + public const int GL_RECT_NV = 0xF6; + public const int GL_CIRCULAR_CCW_ARC_TO_NV = 0xF8; + public const int GL_CIRCULAR_CW_ARC_TO_NV = 0xFA; + public const int GL_CIRCULAR_TANGENT_ARC_TO_NV = 0xFC; + public const int GL_ARC_TO_NV = 0xFE; + public const int GL_RELATIVE_ARC_TO_NV = 0xFF; + public const int GL_BOLD_BIT_NV = 0x01; + public const int GL_ITALIC_BIT_NV = 0x02; + public const int GL_GLYPH_WIDTH_BIT_NV = 0x01; + public const int GL_GLYPH_HEIGHT_BIT_NV = 0x02; + public const int GL_GLYPH_HORIZONTAL_BEARING_X_BIT_NV = 0x04; + public const int GL_GLYPH_HORIZONTAL_BEARING_Y_BIT_NV = 0x08; + public const int GL_GLYPH_HORIZONTAL_BEARING_ADVANCE_BIT_NV = 0x10; + public const int GL_GLYPH_VERTICAL_BEARING_X_BIT_NV = 0x20; + public const int GL_GLYPH_VERTICAL_BEARING_Y_BIT_NV = 0x40; + public const int GL_GLYPH_VERTICAL_BEARING_ADVANCE_BIT_NV = 0x80; + public const int GL_GLYPH_HAS_KERNING_BIT_NV = 0x100; + public const int GL_FONT_X_MIN_BOUNDS_BIT_NV = 0x00010000; + public const int GL_FONT_Y_MIN_BOUNDS_BIT_NV = 0x00020000; + public const int GL_FONT_X_MAX_BOUNDS_BIT_NV = 0x00040000; + public const int GL_FONT_Y_MAX_BOUNDS_BIT_NV = 0x00080000; + public const int GL_FONT_UNITS_PER_EM_BIT_NV = 0x00100000; + public const int GL_FONT_ASCENDER_BIT_NV = 0x00200000; + public const int GL_FONT_DESCENDER_BIT_NV = 0x00400000; + public const int GL_FONT_HEIGHT_BIT_NV = 0x00800000; + public const int GL_FONT_MAX_ADVANCE_WIDTH_BIT_NV = 0x01000000; + public const int GL_FONT_MAX_ADVANCE_HEIGHT_BIT_NV = 0x02000000; + public const int GL_FONT_UNDERLINE_POSITION_BIT_NV = 0x04000000; + public const int GL_FONT_UNDERLINE_THICKNESS_BIT_NV = 0x08000000; + public const int GL_FONT_HAS_KERNING_BIT_NV = 0x10000000; + public const int GL_ROUNDED_RECT_NV = 0xE8; + public const int GL_RELATIVE_ROUNDED_RECT_NV = 0xE9; + public const int GL_ROUNDED_RECT2_NV = 0xEA; + public const int GL_RELATIVE_ROUNDED_RECT2_NV = 0xEB; + public const int GL_ROUNDED_RECT4_NV = 0xEC; + public const int GL_RELATIVE_ROUNDED_RECT4_NV = 0xED; + public const int GL_ROUNDED_RECT8_NV = 0xEE; + public const int GL_RELATIVE_ROUNDED_RECT8_NV = 0xEF; + public const int GL_RELATIVE_RECT_NV = 0xF7; + public const int GL_FONT_GLYPHS_AVAILABLE_NV = 0x9368; + public const int GL_FONT_TARGET_UNAVAILABLE_NV = 0x9369; + public const int GL_FONT_UNAVAILABLE_NV = 0x936A; + public const int GL_FONT_UNINTELLIGIBLE_NV = 0x936B; + public const int GL_CONIC_CURVE_TO_NV = 0x1A; + public const int GL_RELATIVE_CONIC_CURVE_TO_NV = 0x1B; + public const int GL_FONT_NUM_GLYPH_INDICES_BIT_NV = 0x20000000; + public const int GL_STANDARD_FONT_FORMAT_NV = 0x936C; + public const int GL_2_BYTES_NV = 0x1407; + public const int GL_3_BYTES_NV = 0x1408; + public const int GL_4_BYTES_NV = 0x1409; + public const int GL_EYE_LINEAR_NV = 0x2400; + public const int GL_OBJECT_LINEAR_NV = 0x2401; + public const int GL_CONSTANT_NV = 0x8576; + public const int GL_PATH_FOG_GEN_MODE_NV = 0x90AC; + public const int GL_PRIMARY_COLOR_NV = 0x852C; + public const int GL_SECONDARY_COLOR_NV = 0x852D; + public const int GL_PATH_GEN_COLOR_FORMAT_NV = 0x90B2; + public const int GL_PATH_PROJECTION_NV = 0x1701; + public const int GL_PATH_MODELVIEW_NV = 0x1700; + public const int GL_PATH_MODELVIEW_STACK_DEPTH_NV = 0x0BA3; + public const int GL_PATH_MODELVIEW_MATRIX_NV = 0x0BA6; + public const int GL_PATH_MAX_MODELVIEW_STACK_DEPTH_NV = 0x0D36; + public const int GL_PATH_TRANSPOSE_MODELVIEW_MATRIX_NV = 0x84E3; + public const int GL_PATH_PROJECTION_STACK_DEPTH_NV = 0x0BA4; + public const int GL_PATH_PROJECTION_MATRIX_NV = 0x0BA7; + public const int GL_PATH_MAX_PROJECTION_STACK_DEPTH_NV = 0x0D38; + public const int GL_PATH_TRANSPOSE_PROJECTION_MATRIX_NV = 0x84E4; + public const int GL_FRAGMENT_INPUT_NV = 0x936D; + public const int GL_NV_path_rendering_shared_edge = 1; + public const int GL_SHARED_EDGE_NV = 0xC0; + public const int GL_NV_pixel_data_range = 1; + public const int GL_WRITE_PIXEL_DATA_RANGE_NV = 0x8878; + public const int GL_READ_PIXEL_DATA_RANGE_NV = 0x8879; + public const int GL_WRITE_PIXEL_DATA_RANGE_LENGTH_NV = 0x887A; + public const int GL_READ_PIXEL_DATA_RANGE_LENGTH_NV = 0x887B; + public const int GL_WRITE_PIXEL_DATA_RANGE_POINTER_NV = 0x887C; + public const int GL_READ_PIXEL_DATA_RANGE_POINTER_NV = 0x887D; + public const int GL_NV_point_sprite = 1; + public const int GL_POINT_SPRITE_NV = 0x8861; + public const int GL_COORD_REPLACE_NV = 0x8862; + public const int GL_POINT_SPRITE_R_MODE_NV = 0x8863; + public const int GL_NV_present_video = 1; + public const int GL_FRAME_NV = 0x8E26; + public const int GL_FIELDS_NV = 0x8E27; + public const int GL_CURRENT_TIME_NV = 0x8E28; + public const int GL_NUM_FILL_STREAMS_NV = 0x8E29; + public const int GL_PRESENT_TIME_NV = 0x8E2A; + public const int GL_PRESENT_DURATION_NV = 0x8E2B; + public const int GL_NV_primitive_restart = 1; + public const int GL_PRIMITIVE_RESTART_NV = 0x8558; + public const int GL_PRIMITIVE_RESTART_INDEX_NV = 0x8559; + public const int GL_NV_query_resource = 1; + public const int GL_QUERY_RESOURCE_TYPE_VIDMEM_ALLOC_NV = 0x9540; + public const int GL_QUERY_RESOURCE_MEMTYPE_VIDMEM_NV = 0x9542; + public const int GL_QUERY_RESOURCE_SYS_RESERVED_NV = 0x9544; + public const int GL_QUERY_RESOURCE_TEXTURE_NV = 0x9545; + public const int GL_QUERY_RESOURCE_RENDERBUFFER_NV = 0x9546; + public const int GL_QUERY_RESOURCE_BUFFEROBJECT_NV = 0x9547; + public const int GL_NV_query_resource_tag = 1; + public const int GL_NV_register_combiners = 1; + public const int GL_REGISTER_COMBINERS_NV = 0x8522; + public const int GL_VARIABLE_A_NV = 0x8523; + public const int GL_VARIABLE_B_NV = 0x8524; + public const int GL_VARIABLE_C_NV = 0x8525; + public const int GL_VARIABLE_D_NV = 0x8526; + public const int GL_VARIABLE_E_NV = 0x8527; + public const int GL_VARIABLE_F_NV = 0x8528; + public const int GL_VARIABLE_G_NV = 0x8529; + public const int GL_CONSTANT_COLOR0_NV = 0x852A; + public const int GL_CONSTANT_COLOR1_NV = 0x852B; + public const int GL_SPARE0_NV = 0x852E; + public const int GL_SPARE1_NV = 0x852F; + public const int GL_DISCARD_NV = 0x8530; + public const int GL_E_TIMES_F_NV = 0x8531; + public const int GL_SPARE0_PLUS_SECONDARY_COLOR_NV = 0x8532; + public const int GL_UNSIGNED_IDENTITY_NV = 0x8536; + public const int GL_UNSIGNED_INVERT_NV = 0x8537; + public const int GL_EXPAND_NORMAL_NV = 0x8538; + public const int GL_EXPAND_NEGATE_NV = 0x8539; + public const int GL_HALF_BIAS_NORMAL_NV = 0x853A; + public const int GL_HALF_BIAS_NEGATE_NV = 0x853B; + public const int GL_SIGNED_IDENTITY_NV = 0x853C; + public const int GL_SIGNED_NEGATE_NV = 0x853D; + public const int GL_SCALE_BY_TWO_NV = 0x853E; + public const int GL_SCALE_BY_FOUR_NV = 0x853F; + public const int GL_SCALE_BY_ONE_HALF_NV = 0x8540; + public const int GL_BIAS_BY_NEGATIVE_ONE_HALF_NV = 0x8541; + public const int GL_COMBINER_INPUT_NV = 0x8542; + public const int GL_COMBINER_MAPPING_NV = 0x8543; + public const int GL_COMBINER_COMPONENT_USAGE_NV = 0x8544; + public const int GL_COMBINER_AB_DOT_PRODUCT_NV = 0x8545; + public const int GL_COMBINER_CD_DOT_PRODUCT_NV = 0x8546; + public const int GL_COMBINER_MUX_SUM_NV = 0x8547; + public const int GL_COMBINER_SCALE_NV = 0x8548; + public const int GL_COMBINER_BIAS_NV = 0x8549; + public const int GL_COMBINER_AB_OUTPUT_NV = 0x854A; + public const int GL_COMBINER_CD_OUTPUT_NV = 0x854B; + public const int GL_COMBINER_SUM_OUTPUT_NV = 0x854C; + public const int GL_MAX_GENERAL_COMBINERS_NV = 0x854D; + public const int GL_NUM_GENERAL_COMBINERS_NV = 0x854E; + public const int GL_COLOR_SUM_CLAMP_NV = 0x854F; + public const int GL_COMBINER0_NV = 0x8550; + public const int GL_COMBINER1_NV = 0x8551; + public const int GL_COMBINER2_NV = 0x8552; + public const int GL_COMBINER3_NV = 0x8553; + public const int GL_COMBINER4_NV = 0x8554; + public const int GL_COMBINER5_NV = 0x8555; + public const int GL_COMBINER6_NV = 0x8556; + public const int GL_COMBINER7_NV = 0x8557; + public const int GL_NV_register_combiners2 = 1; + public const int GL_PER_STAGE_CONSTANTS_NV = 0x8535; + public const int GL_NV_robustness_video_memory_purge = 1; + public const int GL_PURGED_CONTEXT_RESET_NV = 0x92BB; + public const int GL_NV_sample_locations = 1; + public const int GL_SAMPLE_LOCATION_SUBPIXEL_BITS_NV = 0x933D; + public const int GL_SAMPLE_LOCATION_PIXEL_GRID_WIDTH_NV = 0x933E; + public const int GL_SAMPLE_LOCATION_PIXEL_GRID_HEIGHT_NV = 0x933F; + public const int GL_PROGRAMMABLE_SAMPLE_LOCATION_TABLE_SIZE_NV = 0x9340; + public const int GL_SAMPLE_LOCATION_NV = 0x8E50; + public const int GL_PROGRAMMABLE_SAMPLE_LOCATION_NV = 0x9341; + public const int GL_FRAMEBUFFER_PROGRAMMABLE_SAMPLE_LOCATIONS_NV = 0x9342; + public const int GL_FRAMEBUFFER_SAMPLE_LOCATION_PIXEL_GRID_NV = 0x9343; + public const int GL_NV_sample_mask_override_coverage = 1; + public const int GL_NV_shader_atomic_counters = 1; + public const int GL_NV_shader_atomic_float = 1; + public const int GL_NV_shader_atomic_float64 = 1; + public const int GL_NV_shader_atomic_fp16_vector = 1; + public const int GL_NV_shader_atomic_int64 = 1; + public const int GL_NV_shader_buffer_load = 1; + public const int GL_BUFFER_GPU_ADDRESS_NV = 0x8F1D; + public const int GL_GPU_ADDRESS_NV = 0x8F34; + public const int GL_MAX_SHADER_BUFFER_ADDRESS_NV = 0x8F35; + public const int GL_NV_shader_buffer_store = 1; + public const int GL_SHADER_GLOBAL_ACCESS_BARRIER_BIT_NV = 0x00000010; + public const int GL_NV_shader_storage_buffer_object = 1; + public const int GL_NV_shader_thread_group = 1; + public const int GL_WARP_SIZE_NV = 0x9339; + public const int GL_WARPS_PER_SM_NV = 0x933A; + public const int GL_SM_COUNT_NV = 0x933B; + public const int GL_NV_shader_thread_shuffle = 1; + public const int GL_NV_stereo_view_rendering = 1; + public const int GL_NV_tessellation_program5 = 1; + public const int GL_MAX_PROGRAM_PATCH_ATTRIBS_NV = 0x86D8; + public const int GL_TESS_CONTROL_PROGRAM_NV = 0x891E; + public const int GL_TESS_EVALUATION_PROGRAM_NV = 0x891F; + public const int GL_TESS_CONTROL_PROGRAM_PARAMETER_BUFFER_NV = 0x8C74; + public const int GL_TESS_EVALUATION_PROGRAM_PARAMETER_BUFFER_NV = 0x8C75; + public const int GL_NV_texgen_emboss = 1; + public const int GL_EMBOSS_LIGHT_NV = 0x855D; + public const int GL_EMBOSS_CONSTANT_NV = 0x855E; + public const int GL_EMBOSS_MAP_NV = 0x855F; + public const int GL_NV_texgen_reflection = 1; + public const int GL_NORMAL_MAP_NV = 0x8511; + public const int GL_REFLECTION_MAP_NV = 0x8512; + public const int GL_NV_texture_barrier = 1; + public const int GL_NV_texture_compression_vtc = 1; + public const int GL_NV_texture_env_combine4 = 1; + public const int GL_COMBINE4_NV = 0x8503; + public const int GL_SOURCE3_RGB_NV = 0x8583; + public const int GL_SOURCE3_ALPHA_NV = 0x858B; + public const int GL_OPERAND3_RGB_NV = 0x8593; + public const int GL_OPERAND3_ALPHA_NV = 0x859B; + public const int GL_NV_texture_expand_normal = 1; + public const int GL_TEXTURE_UNSIGNED_REMAP_MODE_NV = 0x888F; + public const int GL_NV_texture_multisample = 1; + public const int GL_TEXTURE_COVERAGE_SAMPLES_NV = 0x9045; + public const int GL_TEXTURE_COLOR_SAMPLES_NV = 0x9046; + public const int GL_NV_texture_rectangle = 1; + public const int GL_TEXTURE_RECTANGLE_NV = 0x84F5; + public const int GL_TEXTURE_BINDING_RECTANGLE_NV = 0x84F6; + public const int GL_PROXY_TEXTURE_RECTANGLE_NV = 0x84F7; + public const int GL_MAX_RECTANGLE_TEXTURE_SIZE_NV = 0x84F8; + public const int GL_NV_texture_rectangle_compressed = 1; + public const int GL_NV_texture_shader = 1; + public const int GL_OFFSET_TEXTURE_RECTANGLE_NV = 0x864C; + public const int GL_OFFSET_TEXTURE_RECTANGLE_SCALE_NV = 0x864D; + public const int GL_DOT_PRODUCT_TEXTURE_RECTANGLE_NV = 0x864E; + public const int GL_RGBA_UNSIGNED_DOT_PRODUCT_MAPPING_NV = 0x86D9; + public const int GL_UNSIGNED_INT_S8_S8_8_8_NV = 0x86DA; + public const int GL_UNSIGNED_INT_8_8_S8_S8_REV_NV = 0x86DB; + public const int GL_DSDT_MAG_INTENSITY_NV = 0x86DC; + public const int GL_SHADER_CONSISTENT_NV = 0x86DD; + public const int GL_TEXTURE_SHADER_NV = 0x86DE; + public const int GL_SHADER_OPERATION_NV = 0x86DF; + public const int GL_CULL_MODES_NV = 0x86E0; + public const int GL_OFFSET_TEXTURE_MATRIX_NV = 0x86E1; + public const int GL_OFFSET_TEXTURE_SCALE_NV = 0x86E2; + public const int GL_OFFSET_TEXTURE_BIAS_NV = 0x86E3; + public const int GL_OFFSET_TEXTURE_2D_MATRIX_NV = 0x86E1; + public const int GL_OFFSET_TEXTURE_2D_SCALE_NV = 0x86E2; + public const int GL_OFFSET_TEXTURE_2D_BIAS_NV = 0x86E3; + public const int GL_PREVIOUS_TEXTURE_INPUT_NV = 0x86E4; + public const int GL_CONST_EYE_NV = 0x86E5; + public const int GL_PASS_THROUGH_NV = 0x86E6; + public const int GL_CULL_FRAGMENT_NV = 0x86E7; + public const int GL_OFFSET_TEXTURE_2D_NV = 0x86E8; + public const int GL_DEPENDENT_AR_TEXTURE_2D_NV = 0x86E9; + public const int GL_DEPENDENT_GB_TEXTURE_2D_NV = 0x86EA; + public const int GL_DOT_PRODUCT_NV = 0x86EC; + public const int GL_DOT_PRODUCT_DEPTH_REPLACE_NV = 0x86ED; + public const int GL_DOT_PRODUCT_TEXTURE_2D_NV = 0x86EE; + public const int GL_DOT_PRODUCT_TEXTURE_CUBE_MAP_NV = 0x86F0; + public const int GL_DOT_PRODUCT_DIFFUSE_CUBE_MAP_NV = 0x86F1; + public const int GL_DOT_PRODUCT_REFLECT_CUBE_MAP_NV = 0x86F2; + public const int GL_DOT_PRODUCT_CONST_EYE_REFLECT_CUBE_MAP_NV = 0x86F3; + public const int GL_HILO_NV = 0x86F4; + public const int GL_DSDT_NV = 0x86F5; + public const int GL_DSDT_MAG_NV = 0x86F6; + public const int GL_DSDT_MAG_VIB_NV = 0x86F7; + public const int GL_HILO16_NV = 0x86F8; + public const int GL_SIGNED_HILO_NV = 0x86F9; + public const int GL_SIGNED_HILO16_NV = 0x86FA; + public const int GL_SIGNED_RGBA_NV = 0x86FB; + public const int GL_SIGNED_RGBA8_NV = 0x86FC; + public const int GL_SIGNED_RGB_NV = 0x86FE; + public const int GL_SIGNED_RGB8_NV = 0x86FF; + public const int GL_SIGNED_LUMINANCE_NV = 0x8701; + public const int GL_SIGNED_LUMINANCE8_NV = 0x8702; + public const int GL_SIGNED_LUMINANCE_ALPHA_NV = 0x8703; + public const int GL_SIGNED_LUMINANCE8_ALPHA8_NV = 0x8704; + public const int GL_SIGNED_ALPHA_NV = 0x8705; + public const int GL_SIGNED_ALPHA8_NV = 0x8706; + public const int GL_SIGNED_INTENSITY_NV = 0x8707; + public const int GL_SIGNED_INTENSITY8_NV = 0x8708; + public const int GL_DSDT8_NV = 0x8709; + public const int GL_DSDT8_MAG8_NV = 0x870A; + public const int GL_DSDT8_MAG8_INTENSITY8_NV = 0x870B; + public const int GL_SIGNED_RGB_UNSIGNED_ALPHA_NV = 0x870C; + public const int GL_SIGNED_RGB8_UNSIGNED_ALPHA8_NV = 0x870D; + public const int GL_HI_SCALE_NV = 0x870E; + public const int GL_LO_SCALE_NV = 0x870F; + public const int GL_DS_SCALE_NV = 0x8710; + public const int GL_DT_SCALE_NV = 0x8711; + public const int GL_MAGNITUDE_SCALE_NV = 0x8712; + public const int GL_VIBRANCE_SCALE_NV = 0x8713; + public const int GL_HI_BIAS_NV = 0x8714; + public const int GL_LO_BIAS_NV = 0x8715; + public const int GL_DS_BIAS_NV = 0x8716; + public const int GL_DT_BIAS_NV = 0x8717; + public const int GL_MAGNITUDE_BIAS_NV = 0x8718; + public const int GL_VIBRANCE_BIAS_NV = 0x8719; + public const int GL_TEXTURE_BORDER_VALUES_NV = 0x871A; + public const int GL_TEXTURE_HI_SIZE_NV = 0x871B; + public const int GL_TEXTURE_LO_SIZE_NV = 0x871C; + public const int GL_TEXTURE_DS_SIZE_NV = 0x871D; + public const int GL_TEXTURE_DT_SIZE_NV = 0x871E; + public const int GL_TEXTURE_MAG_SIZE_NV = 0x871F; + public const int GL_NV_texture_shader2 = 1; + public const int GL_DOT_PRODUCT_TEXTURE_3D_NV = 0x86EF; + public const int GL_NV_texture_shader3 = 1; + public const int GL_OFFSET_PROJECTIVE_TEXTURE_2D_NV = 0x8850; + public const int GL_OFFSET_PROJECTIVE_TEXTURE_2D_SCALE_NV = 0x8851; + public const int GL_OFFSET_PROJECTIVE_TEXTURE_RECTANGLE_NV = 0x8852; + public const int GL_OFFSET_PROJECTIVE_TEXTURE_RECTANGLE_SCALE_NV = 0x8853; + public const int GL_OFFSET_HILO_TEXTURE_2D_NV = 0x8854; + public const int GL_OFFSET_HILO_TEXTURE_RECTANGLE_NV = 0x8855; + public const int GL_OFFSET_HILO_PROJECTIVE_TEXTURE_2D_NV = 0x8856; + public const int GL_OFFSET_HILO_PROJECTIVE_TEXTURE_RECTANGLE_NV = 0x8857; + public const int GL_DEPENDENT_HILO_TEXTURE_2D_NV = 0x8858; + public const int GL_DEPENDENT_RGB_TEXTURE_3D_NV = 0x8859; + public const int GL_DEPENDENT_RGB_TEXTURE_CUBE_MAP_NV = 0x885A; + public const int GL_DOT_PRODUCT_PASS_THROUGH_NV = 0x885B; + public const int GL_DOT_PRODUCT_TEXTURE_1D_NV = 0x885C; + public const int GL_DOT_PRODUCT_AFFINE_DEPTH_REPLACE_NV = 0x885D; + public const int GL_HILO8_NV = 0x885E; + public const int GL_SIGNED_HILO8_NV = 0x885F; + public const int GL_FORCE_BLUE_TO_ONE_NV = 0x8860; + public const int GL_NV_transform_feedback = 1; + public const int GL_BACK_PRIMARY_COLOR_NV = 0x8C77; + public const int GL_BACK_SECONDARY_COLOR_NV = 0x8C78; + public const int GL_TEXTURE_COORD_NV = 0x8C79; + public const int GL_CLIP_DISTANCE_NV = 0x8C7A; + public const int GL_VERTEX_ID_NV = 0x8C7B; + public const int GL_PRIMITIVE_ID_NV = 0x8C7C; + public const int GL_GENERIC_ATTRIB_NV = 0x8C7D; + public const int GL_TRANSFORM_FEEDBACK_ATTRIBS_NV = 0x8C7E; + public const int GL_TRANSFORM_FEEDBACK_BUFFER_MODE_NV = 0x8C7F; + public const int GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS_NV = 0x8C80; + public const int GL_ACTIVE_VARYINGS_NV = 0x8C81; + public const int GL_ACTIVE_VARYING_MAX_LENGTH_NV = 0x8C82; + public const int GL_TRANSFORM_FEEDBACK_VARYINGS_NV = 0x8C83; + public const int GL_TRANSFORM_FEEDBACK_BUFFER_START_NV = 0x8C84; + public const int GL_TRANSFORM_FEEDBACK_BUFFER_SIZE_NV = 0x8C85; + public const int GL_TRANSFORM_FEEDBACK_RECORD_NV = 0x8C86; + public const int GL_PRIMITIVES_GENERATED_NV = 0x8C87; + public const int GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN_NV = 0x8C88; + public const int GL_RASTERIZER_DISCARD_NV = 0x8C89; + public const int GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS_NV = 0x8C8A; + public const int GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS_NV = 0x8C8B; + public const int GL_INTERLEAVED_ATTRIBS_NV = 0x8C8C; + public const int GL_SEPARATE_ATTRIBS_NV = 0x8C8D; + public const int GL_TRANSFORM_FEEDBACK_BUFFER_NV = 0x8C8E; + public const int GL_TRANSFORM_FEEDBACK_BUFFER_BINDING_NV = 0x8C8F; + public const int GL_LAYER_NV = 0x8DAA; + public const int GL_NEXT_BUFFER_NV = -2; + public const int GL_SKIP_COMPONENTS4_NV = -3; + public const int GL_SKIP_COMPONENTS3_NV = -4; + public const int GL_SKIP_COMPONENTS2_NV = -5; + public const int GL_SKIP_COMPONENTS1_NV = -6; + public const int GL_NV_transform_feedback2 = 1; + public const int GL_TRANSFORM_FEEDBACK_NV = 0x8E22; + public const int GL_TRANSFORM_FEEDBACK_BUFFER_PAUSED_NV = 0x8E23; + public const int GL_TRANSFORM_FEEDBACK_BUFFER_ACTIVE_NV = 0x8E24; + public const int GL_TRANSFORM_FEEDBACK_BINDING_NV = 0x8E25; + public const int GL_NV_uniform_buffer_unified_memory = 1; + public const int GL_UNIFORM_BUFFER_UNIFIED_NV = 0x936E; + public const int GL_UNIFORM_BUFFER_ADDRESS_NV = 0x936F; + public const int GL_UNIFORM_BUFFER_LENGTH_NV = 0x9370; + public const int GL_NV_vdpau_interop = 1; + public const int GL_SURFACE_STATE_NV = 0x86EB; + public const int GL_SURFACE_REGISTERED_NV = 0x86FD; + public const int GL_SURFACE_MAPPED_NV = 0x8700; + public const int GL_WRITE_DISCARD_NV = 0x88BE; + public const int GL_NV_vertex_array_range = 1; + public const int GL_VERTEX_ARRAY_RANGE_NV = 0x851D; + public const int GL_VERTEX_ARRAY_RANGE_LENGTH_NV = 0x851E; + public const int GL_VERTEX_ARRAY_RANGE_VALID_NV = 0x851F; + public const int GL_MAX_VERTEX_ARRAY_RANGE_ELEMENT_NV = 0x8520; + public const int GL_VERTEX_ARRAY_RANGE_POINTER_NV = 0x8521; + public const int GL_NV_vertex_array_range2 = 1; + public const int GL_VERTEX_ARRAY_RANGE_WITHOUT_FLUSH_NV = 0x8533; + public const int GL_NV_vertex_attrib_integer_64bit = 1; + public const int GL_NV_vertex_buffer_unified_memory = 1; + public const int GL_VERTEX_ATTRIB_ARRAY_UNIFIED_NV = 0x8F1E; + public const int GL_ELEMENT_ARRAY_UNIFIED_NV = 0x8F1F; + public const int GL_VERTEX_ATTRIB_ARRAY_ADDRESS_NV = 0x8F20; + public const int GL_VERTEX_ARRAY_ADDRESS_NV = 0x8F21; + public const int GL_NORMAL_ARRAY_ADDRESS_NV = 0x8F22; + public const int GL_COLOR_ARRAY_ADDRESS_NV = 0x8F23; + public const int GL_INDEX_ARRAY_ADDRESS_NV = 0x8F24; + public const int GL_TEXTURE_COORD_ARRAY_ADDRESS_NV = 0x8F25; + public const int GL_EDGE_FLAG_ARRAY_ADDRESS_NV = 0x8F26; + public const int GL_SECONDARY_COLOR_ARRAY_ADDRESS_NV = 0x8F27; + public const int GL_FOG_COORD_ARRAY_ADDRESS_NV = 0x8F28; + public const int GL_ELEMENT_ARRAY_ADDRESS_NV = 0x8F29; + public const int GL_VERTEX_ATTRIB_ARRAY_LENGTH_NV = 0x8F2A; + public const int GL_VERTEX_ARRAY_LENGTH_NV = 0x8F2B; + public const int GL_NORMAL_ARRAY_LENGTH_NV = 0x8F2C; + public const int GL_COLOR_ARRAY_LENGTH_NV = 0x8F2D; + public const int GL_INDEX_ARRAY_LENGTH_NV = 0x8F2E; + public const int GL_TEXTURE_COORD_ARRAY_LENGTH_NV = 0x8F2F; + public const int GL_EDGE_FLAG_ARRAY_LENGTH_NV = 0x8F30; + public const int GL_SECONDARY_COLOR_ARRAY_LENGTH_NV = 0x8F31; + public const int GL_FOG_COORD_ARRAY_LENGTH_NV = 0x8F32; + public const int GL_ELEMENT_ARRAY_LENGTH_NV = 0x8F33; + public const int GL_DRAW_INDIRECT_UNIFIED_NV = 0x8F40; + public const int GL_DRAW_INDIRECT_ADDRESS_NV = 0x8F41; + public const int GL_DRAW_INDIRECT_LENGTH_NV = 0x8F42; + public const int GL_NV_vertex_program = 1; + public const int GL_VERTEX_PROGRAM_NV = 0x8620; + public const int GL_VERTEX_STATE_PROGRAM_NV = 0x8621; + public const int GL_ATTRIB_ARRAY_SIZE_NV = 0x8623; + public const int GL_ATTRIB_ARRAY_STRIDE_NV = 0x8624; + public const int GL_ATTRIB_ARRAY_TYPE_NV = 0x8625; + public const int GL_CURRENT_ATTRIB_NV = 0x8626; + public const int GL_PROGRAM_LENGTH_NV = 0x8627; + public const int GL_PROGRAM_STRING_NV = 0x8628; + public const int GL_MODELVIEW_PROJECTION_NV = 0x8629; + public const int GL_IDENTITY_NV = 0x862A; + public const int GL_INVERSE_NV = 0x862B; + public const int GL_TRANSPOSE_NV = 0x862C; + public const int GL_INVERSE_TRANSPOSE_NV = 0x862D; + public const int GL_MAX_TRACK_MATRIX_STACK_DEPTH_NV = 0x862E; + public const int GL_MAX_TRACK_MATRICES_NV = 0x862F; + public const int GL_MATRIX0_NV = 0x8630; + public const int GL_MATRIX1_NV = 0x8631; + public const int GL_MATRIX2_NV = 0x8632; + public const int GL_MATRIX3_NV = 0x8633; + public const int GL_MATRIX4_NV = 0x8634; + public const int GL_MATRIX5_NV = 0x8635; + public const int GL_MATRIX6_NV = 0x8636; + public const int GL_MATRIX7_NV = 0x8637; + public const int GL_CURRENT_MATRIX_STACK_DEPTH_NV = 0x8640; + public const int GL_CURRENT_MATRIX_NV = 0x8641; + public const int GL_VERTEX_PROGRAM_POINT_SIZE_NV = 0x8642; + public const int GL_VERTEX_PROGRAM_TWO_SIDE_NV = 0x8643; + public const int GL_PROGRAM_PARAMETER_NV = 0x8644; + public const int GL_ATTRIB_ARRAY_POINTER_NV = 0x8645; + public const int GL_PROGRAM_TARGET_NV = 0x8646; + public const int GL_PROGRAM_RESIDENT_NV = 0x8647; + public const int GL_TRACK_MATRIX_NV = 0x8648; + public const int GL_TRACK_MATRIX_TRANSFORM_NV = 0x8649; + public const int GL_VERTEX_PROGRAM_BINDING_NV = 0x864A; + public const int GL_PROGRAM_ERROR_POSITION_NV = 0x864B; + public const int GL_VERTEX_ATTRIB_ARRAY0_NV = 0x8650; + public const int GL_VERTEX_ATTRIB_ARRAY1_NV = 0x8651; + public const int GL_VERTEX_ATTRIB_ARRAY2_NV = 0x8652; + public const int GL_VERTEX_ATTRIB_ARRAY3_NV = 0x8653; + public const int GL_VERTEX_ATTRIB_ARRAY4_NV = 0x8654; + public const int GL_VERTEX_ATTRIB_ARRAY5_NV = 0x8655; + public const int GL_VERTEX_ATTRIB_ARRAY6_NV = 0x8656; + public const int GL_VERTEX_ATTRIB_ARRAY7_NV = 0x8657; + public const int GL_VERTEX_ATTRIB_ARRAY8_NV = 0x8658; + public const int GL_VERTEX_ATTRIB_ARRAY9_NV = 0x8659; + public const int GL_VERTEX_ATTRIB_ARRAY10_NV = 0x865A; + public const int GL_VERTEX_ATTRIB_ARRAY11_NV = 0x865B; + public const int GL_VERTEX_ATTRIB_ARRAY12_NV = 0x865C; + public const int GL_VERTEX_ATTRIB_ARRAY13_NV = 0x865D; + public const int GL_VERTEX_ATTRIB_ARRAY14_NV = 0x865E; + public const int GL_VERTEX_ATTRIB_ARRAY15_NV = 0x865F; + public const int GL_MAP1_VERTEX_ATTRIB0_4_NV = 0x8660; + public const int GL_MAP1_VERTEX_ATTRIB1_4_NV = 0x8661; + public const int GL_MAP1_VERTEX_ATTRIB2_4_NV = 0x8662; + public const int GL_MAP1_VERTEX_ATTRIB3_4_NV = 0x8663; + public const int GL_MAP1_VERTEX_ATTRIB4_4_NV = 0x8664; + public const int GL_MAP1_VERTEX_ATTRIB5_4_NV = 0x8665; + public const int GL_MAP1_VERTEX_ATTRIB6_4_NV = 0x8666; + public const int GL_MAP1_VERTEX_ATTRIB7_4_NV = 0x8667; + public const int GL_MAP1_VERTEX_ATTRIB8_4_NV = 0x8668; + public const int GL_MAP1_VERTEX_ATTRIB9_4_NV = 0x8669; + public const int GL_MAP1_VERTEX_ATTRIB10_4_NV = 0x866A; + public const int GL_MAP1_VERTEX_ATTRIB11_4_NV = 0x866B; + public const int GL_MAP1_VERTEX_ATTRIB12_4_NV = 0x866C; + public const int GL_MAP1_VERTEX_ATTRIB13_4_NV = 0x866D; + public const int GL_MAP1_VERTEX_ATTRIB14_4_NV = 0x866E; + public const int GL_MAP1_VERTEX_ATTRIB15_4_NV = 0x866F; + public const int GL_MAP2_VERTEX_ATTRIB0_4_NV = 0x8670; + public const int GL_MAP2_VERTEX_ATTRIB1_4_NV = 0x8671; + public const int GL_MAP2_VERTEX_ATTRIB2_4_NV = 0x8672; + public const int GL_MAP2_VERTEX_ATTRIB3_4_NV = 0x8673; + public const int GL_MAP2_VERTEX_ATTRIB4_4_NV = 0x8674; + public const int GL_MAP2_VERTEX_ATTRIB5_4_NV = 0x8675; + public const int GL_MAP2_VERTEX_ATTRIB6_4_NV = 0x8676; + public const int GL_MAP2_VERTEX_ATTRIB7_4_NV = 0x8677; + public const int GL_MAP2_VERTEX_ATTRIB8_4_NV = 0x8678; + public const int GL_MAP2_VERTEX_ATTRIB9_4_NV = 0x8679; + public const int GL_MAP2_VERTEX_ATTRIB10_4_NV = 0x867A; + public const int GL_MAP2_VERTEX_ATTRIB11_4_NV = 0x867B; + public const int GL_MAP2_VERTEX_ATTRIB12_4_NV = 0x867C; + public const int GL_MAP2_VERTEX_ATTRIB13_4_NV = 0x867D; + public const int GL_MAP2_VERTEX_ATTRIB14_4_NV = 0x867E; + public const int GL_MAP2_VERTEX_ATTRIB15_4_NV = 0x867F; + public const int GL_NV_vertex_program1_1 = 1; + public const int GL_NV_vertex_program2 = 1; + public const int GL_NV_vertex_program2_option = 1; + public const int GL_NV_vertex_program3 = 1; + public const int GL_NV_vertex_program4 = 1; + public const int GL_VERTEX_ATTRIB_ARRAY_INTEGER_NV = 0x88FD; + public const int GL_NV_video_capture = 1; + public const int GL_VIDEO_BUFFER_NV = 0x9020; + public const int GL_VIDEO_BUFFER_BINDING_NV = 0x9021; + public const int GL_FIELD_UPPER_NV = 0x9022; + public const int GL_FIELD_LOWER_NV = 0x9023; + public const int GL_NUM_VIDEO_CAPTURE_STREAMS_NV = 0x9024; + public const int GL_NEXT_VIDEO_CAPTURE_BUFFER_STATUS_NV = 0x9025; + public const int GL_VIDEO_CAPTURE_TO_422_SUPPORTED_NV = 0x9026; + public const int GL_LAST_VIDEO_CAPTURE_STATUS_NV = 0x9027; + public const int GL_VIDEO_BUFFER_PITCH_NV = 0x9028; + public const int GL_VIDEO_COLOR_CONVERSION_MATRIX_NV = 0x9029; + public const int GL_VIDEO_COLOR_CONVERSION_MAX_NV = 0x902A; + public const int GL_VIDEO_COLOR_CONVERSION_MIN_NV = 0x902B; + public const int GL_VIDEO_COLOR_CONVERSION_OFFSET_NV = 0x902C; + public const int GL_VIDEO_BUFFER_INTERNAL_FORMAT_NV = 0x902D; + public const int GL_PARTIAL_SUCCESS_NV = 0x902E; + public const int GL_SUCCESS_NV = 0x902F; + public const int GL_FAILURE_NV = 0x9030; + public const int GL_YCBYCR8_422_NV = 0x9031; + public const int GL_YCBAYCR8A_4224_NV = 0x9032; + public const int GL_Z6Y10Z6CB10Z6Y10Z6CR10_422_NV = 0x9033; + public const int GL_Z6Y10Z6CB10Z6A10Z6Y10Z6CR10Z6A10_4224_NV = 0x9034; + public const int GL_Z4Y12Z4CB12Z4Y12Z4CR12_422_NV = 0x9035; + public const int GL_Z4Y12Z4CB12Z4A12Z4Y12Z4CR12Z4A12_4224_NV = 0x9036; + public const int GL_Z4Y12Z4CB12Z4CR12_444_NV = 0x9037; + public const int GL_VIDEO_CAPTURE_FRAME_WIDTH_NV = 0x9038; + public const int GL_VIDEO_CAPTURE_FRAME_HEIGHT_NV = 0x9039; + public const int GL_VIDEO_CAPTURE_FIELD_UPPER_HEIGHT_NV = 0x903A; + public const int GL_VIDEO_CAPTURE_FIELD_LOWER_HEIGHT_NV = 0x903B; + public const int GL_VIDEO_CAPTURE_SURFACE_ORIGIN_NV = 0x903C; + public const int GL_NV_viewport_array2 = 1; + public const int GL_NV_viewport_swizzle = 1; + public const int GL_VIEWPORT_SWIZZLE_POSITIVE_X_NV = 0x9350; + public const int GL_VIEWPORT_SWIZZLE_NEGATIVE_X_NV = 0x9351; + public const int GL_VIEWPORT_SWIZZLE_POSITIVE_Y_NV = 0x9352; + public const int GL_VIEWPORT_SWIZZLE_NEGATIVE_Y_NV = 0x9353; + public const int GL_VIEWPORT_SWIZZLE_POSITIVE_Z_NV = 0x9354; + public const int GL_VIEWPORT_SWIZZLE_NEGATIVE_Z_NV = 0x9355; + public const int GL_VIEWPORT_SWIZZLE_POSITIVE_W_NV = 0x9356; + public const int GL_VIEWPORT_SWIZZLE_NEGATIVE_W_NV = 0x9357; + public const int GL_VIEWPORT_SWIZZLE_X_NV = 0x9358; + public const int GL_VIEWPORT_SWIZZLE_Y_NV = 0x9359; + public const int GL_VIEWPORT_SWIZZLE_Z_NV = 0x935A; + public const int GL_VIEWPORT_SWIZZLE_W_NV = 0x935B; + public const int GL_OML_interlace = 1; + public const int GL_INTERLACE_OML = 0x8980; + public const int GL_INTERLACE_READ_OML = 0x8981; + public const int GL_OML_resample = 1; + public const int GL_PACK_RESAMPLE_OML = 0x8984; + public const int GL_UNPACK_RESAMPLE_OML = 0x8985; + public const int GL_RESAMPLE_REPLICATE_OML = 0x8986; + public const int GL_RESAMPLE_ZERO_FILL_OML = 0x8987; + public const int GL_RESAMPLE_AVERAGE_OML = 0x8988; + public const int GL_RESAMPLE_DECIMATE_OML = 0x8989; + public const int GL_OML_subsample = 1; + public const int GL_FORMAT_SUBSAMPLE_24_24_OML = 0x8982; + public const int GL_FORMAT_SUBSAMPLE_244_244_OML = 0x8983; + public const int GL_OVR_multiview = 1; + public const int GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_NUM_VIEWS_OVR = 0x9630; + public const int GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_BASE_VIEW_INDEX_OVR = 0x9632; + public const int GL_MAX_VIEWS_OVR = 0x9631; + public const int GL_FRAMEBUFFER_INCOMPLETE_VIEW_TARGETS_OVR = 0x9633; + public const int GL_OVR_multiview2 = 1; + public const int GL_PGI_misc_hints = 1; + public const int GL_PREFER_DOUBLEBUFFER_HINT_PGI = 0x1A1F8; + public const int GL_CONSERVE_MEMORY_HINT_PGI = 0x1A1FD; + public const int GL_RECLAIM_MEMORY_HINT_PGI = 0x1A1FE; + public const int GL_NATIVE_GRAPHICS_HANDLE_PGI = 0x1A202; + public const int GL_NATIVE_GRAPHICS_BEGIN_HINT_PGI = 0x1A203; + public const int GL_NATIVE_GRAPHICS_END_HINT_PGI = 0x1A204; + public const int GL_ALWAYS_FAST_HINT_PGI = 0x1A20C; + public const int GL_ALWAYS_SOFT_HINT_PGI = 0x1A20D; + public const int GL_ALLOW_DRAW_OBJ_HINT_PGI = 0x1A20E; + public const int GL_ALLOW_DRAW_WIN_HINT_PGI = 0x1A20F; + public const int GL_ALLOW_DRAW_FRG_HINT_PGI = 0x1A210; + public const int GL_ALLOW_DRAW_MEM_HINT_PGI = 0x1A211; + public const int GL_STRICT_DEPTHFUNC_HINT_PGI = 0x1A216; + public const int GL_STRICT_LIGHTING_HINT_PGI = 0x1A217; + public const int GL_STRICT_SCISSOR_HINT_PGI = 0x1A218; + public const int GL_FULL_STIPPLE_HINT_PGI = 0x1A219; + public const int GL_CLIP_NEAR_HINT_PGI = 0x1A220; + public const int GL_CLIP_FAR_HINT_PGI = 0x1A221; + public const int GL_WIDE_LINE_HINT_PGI = 0x1A222; + public const int GL_BACK_NORMALS_HINT_PGI = 0x1A223; + public const int GL_PGI_vertex_hints = 1; + public const int GL_VERTEX_DATA_HINT_PGI = 0x1A22A; + public const int GL_VERTEX_CONSISTENT_HINT_PGI = 0x1A22B; + public const int GL_MATERIAL_SIDE_HINT_PGI = 0x1A22C; + public const int GL_MAX_VERTEX_HINT_PGI = 0x1A22D; + public const int GL_COLOR3_BIT_PGI = 0x00010000; + public const int GL_COLOR4_BIT_PGI = 0x00020000; + public const int GL_EDGEFLAG_BIT_PGI = 0x00040000; + public const int GL_INDEX_BIT_PGI = 0x00080000; + public const int GL_MAT_AMBIENT_BIT_PGI = 0x00100000; + public const int GL_MAT_AMBIENT_AND_DIFFUSE_BIT_PGI = 0x00200000; + public const int GL_MAT_DIFFUSE_BIT_PGI = 0x00400000; + public const int GL_MAT_EMISSION_BIT_PGI = 0x00800000; + public const int GL_MAT_COLOR_INDEXES_BIT_PGI = 0x01000000; + public const int GL_MAT_SHININESS_BIT_PGI = 0x02000000; + public const int GL_MAT_SPECULAR_BIT_PGI = 0x04000000; + public const int GL_NORMAL_BIT_PGI = 0x08000000; + public const int GL_TEXCOORD1_BIT_PGI = 0x10000000; + public const int GL_TEXCOORD2_BIT_PGI = 0x20000000; + public const int GL_TEXCOORD3_BIT_PGI = 0x40000000; + public const int GL_TEXCOORD4_BIT_PGI = unchecked((int)0x80000000); + public const int GL_VERTEX23_BIT_PGI = 0x00000004; + public const int GL_VERTEX4_BIT_PGI = 0x00000008; + public const int GL_REND_screen_coordinates = 1; + public const int GL_SCREEN_COORDINATES_REND = 0x8490; + public const int GL_INVERTED_SCREEN_W_REND = 0x8491; + public const int GL_S3_s3tc = 1; + public const int GL_RGB_S3TC = 0x83A0; + public const int GL_RGB4_S3TC = 0x83A1; + public const int GL_RGBA_S3TC = 0x83A2; + public const int GL_RGBA4_S3TC = 0x83A3; + public const int GL_RGBA_DXT5_S3TC = 0x83A4; + public const int GL_RGBA4_DXT5_S3TC = 0x83A5; + public const int GL_SGIS_detail_texture = 1; + public const int GL_DETAIL_TEXTURE_2D_SGIS = 0x8095; + public const int GL_DETAIL_TEXTURE_2D_BINDING_SGIS = 0x8096; + public const int GL_LINEAR_DETAIL_SGIS = 0x8097; + public const int GL_LINEAR_DETAIL_ALPHA_SGIS = 0x8098; + public const int GL_LINEAR_DETAIL_COLOR_SGIS = 0x8099; + public const int GL_DETAIL_TEXTURE_LEVEL_SGIS = 0x809A; + public const int GL_DETAIL_TEXTURE_MODE_SGIS = 0x809B; + public const int GL_DETAIL_TEXTURE_FUNC_POINTS_SGIS = 0x809C; + public const int GL_SGIS_fog_function = 1; + public const int GL_FOG_FUNC_SGIS = 0x812A; + public const int GL_FOG_FUNC_POINTS_SGIS = 0x812B; + public const int GL_MAX_FOG_FUNC_POINTS_SGIS = 0x812C; + public const int GL_SGIS_generate_mipmap = 1; + public const int GL_GENERATE_MIPMAP_SGIS = 0x8191; + public const int GL_GENERATE_MIPMAP_HINT_SGIS = 0x8192; + public const int GL_SGIS_multisample = 1; + public const int GL_MULTISAMPLE_SGIS = 0x809D; + public const int GL_SAMPLE_ALPHA_TO_MASK_SGIS = 0x809E; + public const int GL_SAMPLE_ALPHA_TO_ONE_SGIS = 0x809F; + public const int GL_SAMPLE_MASK_SGIS = 0x80A0; + public const int GL_1PASS_SGIS = 0x80A1; + public const int GL_2PASS_0_SGIS = 0x80A2; + public const int GL_2PASS_1_SGIS = 0x80A3; + public const int GL_4PASS_0_SGIS = 0x80A4; + public const int GL_4PASS_1_SGIS = 0x80A5; + public const int GL_4PASS_2_SGIS = 0x80A6; + public const int GL_4PASS_3_SGIS = 0x80A7; + public const int GL_SAMPLE_BUFFERS_SGIS = 0x80A8; + public const int GL_SAMPLES_SGIS = 0x80A9; + public const int GL_SAMPLE_MASK_VALUE_SGIS = 0x80AA; + public const int GL_SAMPLE_MASK_INVERT_SGIS = 0x80AB; + public const int GL_SAMPLE_PATTERN_SGIS = 0x80AC; + public const int GL_SGIS_pixel_texture = 1; + public const int GL_PIXEL_TEXTURE_SGIS = 0x8353; + public const int GL_PIXEL_FRAGMENT_RGB_SOURCE_SGIS = 0x8354; + public const int GL_PIXEL_FRAGMENT_ALPHA_SOURCE_SGIS = 0x8355; + public const int GL_PIXEL_GROUP_COLOR_SGIS = 0x8356; + public const int GL_SGIS_point_line_texgen = 1; + public const int GL_EYE_DISTANCE_TO_POINT_SGIS = 0x81F0; + public const int GL_OBJECT_DISTANCE_TO_POINT_SGIS = 0x81F1; + public const int GL_EYE_DISTANCE_TO_LINE_SGIS = 0x81F2; + public const int GL_OBJECT_DISTANCE_TO_LINE_SGIS = 0x81F3; + public const int GL_EYE_POINT_SGIS = 0x81F4; + public const int GL_OBJECT_POINT_SGIS = 0x81F5; + public const int GL_EYE_LINE_SGIS = 0x81F6; + public const int GL_OBJECT_LINE_SGIS = 0x81F7; + public const int GL_SGIS_point_parameters = 1; + public const int GL_POINT_SIZE_MIN_SGIS = 0x8126; + public const int GL_POINT_SIZE_MAX_SGIS = 0x8127; + public const int GL_POINT_FADE_THRESHOLD_SIZE_SGIS = 0x8128; + public const int GL_DISTANCE_ATTENUATION_SGIS = 0x8129; + public const int GL_SGIS_sharpen_texture = 1; + public const int GL_LINEAR_SHARPEN_SGIS = 0x80AD; + public const int GL_LINEAR_SHARPEN_ALPHA_SGIS = 0x80AE; + public const int GL_LINEAR_SHARPEN_COLOR_SGIS = 0x80AF; + public const int GL_SHARPEN_TEXTURE_FUNC_POINTS_SGIS = 0x80B0; + public const int GL_SGIS_texture4D = 1; + public const int GL_PACK_SKIP_VOLUMES_SGIS = 0x8130; + public const int GL_PACK_IMAGE_DEPTH_SGIS = 0x8131; + public const int GL_UNPACK_SKIP_VOLUMES_SGIS = 0x8132; + public const int GL_UNPACK_IMAGE_DEPTH_SGIS = 0x8133; + public const int GL_TEXTURE_4D_SGIS = 0x8134; + public const int GL_PROXY_TEXTURE_4D_SGIS = 0x8135; + public const int GL_TEXTURE_4DSIZE_SGIS = 0x8136; + public const int GL_TEXTURE_WRAP_Q_SGIS = 0x8137; + public const int GL_MAX_4D_TEXTURE_SIZE_SGIS = 0x8138; + public const int GL_TEXTURE_4D_BINDING_SGIS = 0x814F; + public const int GL_SGIS_texture_border_clamp = 1; + public const int GL_CLAMP_TO_BORDER_SGIS = 0x812D; + public const int GL_SGIS_texture_color_mask = 1; + public const int GL_TEXTURE_COLOR_WRITEMASK_SGIS = 0x81EF; + public const int GL_SGIS_texture_edge_clamp = 1; + public const int GL_CLAMP_TO_EDGE_SGIS = 0x812F; + public const int GL_SGIS_texture_filter4 = 1; + public const int GL_FILTER4_SGIS = 0x8146; + public const int GL_TEXTURE_FILTER4_SIZE_SGIS = 0x8147; + public const int GL_SGIS_texture_lod = 1; + public const int GL_TEXTURE_MIN_LOD_SGIS = 0x813A; + public const int GL_TEXTURE_MAX_LOD_SGIS = 0x813B; + public const int GL_TEXTURE_BASE_LEVEL_SGIS = 0x813C; + public const int GL_TEXTURE_MAX_LEVEL_SGIS = 0x813D; + public const int GL_SGIS_texture_select = 1; + public const int GL_DUAL_ALPHA4_SGIS = 0x8110; + public const int GL_DUAL_ALPHA8_SGIS = 0x8111; + public const int GL_DUAL_ALPHA12_SGIS = 0x8112; + public const int GL_DUAL_ALPHA16_SGIS = 0x8113; + public const int GL_DUAL_LUMINANCE4_SGIS = 0x8114; + public const int GL_DUAL_LUMINANCE8_SGIS = 0x8115; + public const int GL_DUAL_LUMINANCE12_SGIS = 0x8116; + public const int GL_DUAL_LUMINANCE16_SGIS = 0x8117; + public const int GL_DUAL_INTENSITY4_SGIS = 0x8118; + public const int GL_DUAL_INTENSITY8_SGIS = 0x8119; + public const int GL_DUAL_INTENSITY12_SGIS = 0x811A; + public const int GL_DUAL_INTENSITY16_SGIS = 0x811B; + public const int GL_DUAL_LUMINANCE_ALPHA4_SGIS = 0x811C; + public const int GL_DUAL_LUMINANCE_ALPHA8_SGIS = 0x811D; + public const int GL_QUAD_ALPHA4_SGIS = 0x811E; + public const int GL_QUAD_ALPHA8_SGIS = 0x811F; + public const int GL_QUAD_LUMINANCE4_SGIS = 0x8120; + public const int GL_QUAD_LUMINANCE8_SGIS = 0x8121; + public const int GL_QUAD_INTENSITY4_SGIS = 0x8122; + public const int GL_QUAD_INTENSITY8_SGIS = 0x8123; + public const int GL_DUAL_TEXTURE_SELECT_SGIS = 0x8124; + public const int GL_QUAD_TEXTURE_SELECT_SGIS = 0x8125; + public const int GL_SGIX_async = 1; + public const int GL_ASYNC_MARKER_SGIX = 0x8329; + public const int GL_SGIX_async_histogram = 1; + public const int GL_ASYNC_HISTOGRAM_SGIX = 0x832C; + public const int GL_MAX_ASYNC_HISTOGRAM_SGIX = 0x832D; + public const int GL_SGIX_async_pixel = 1; + public const int GL_ASYNC_TEX_IMAGE_SGIX = 0x835C; + public const int GL_ASYNC_DRAW_PIXELS_SGIX = 0x835D; + public const int GL_ASYNC_READ_PIXELS_SGIX = 0x835E; + public const int GL_MAX_ASYNC_TEX_IMAGE_SGIX = 0x835F; + public const int GL_MAX_ASYNC_DRAW_PIXELS_SGIX = 0x8360; + public const int GL_MAX_ASYNC_READ_PIXELS_SGIX = 0x8361; + public const int GL_SGIX_blend_alpha_minmax = 1; + public const int GL_ALPHA_MIN_SGIX = 0x8320; + public const int GL_ALPHA_MAX_SGIX = 0x8321; + public const int GL_SGIX_calligraphic_fragment = 1; + public const int GL_CALLIGRAPHIC_FRAGMENT_SGIX = 0x8183; + public const int GL_SGIX_clipmap = 1; + public const int GL_LINEAR_CLIPMAP_LINEAR_SGIX = 0x8170; + public const int GL_TEXTURE_CLIPMAP_CENTER_SGIX = 0x8171; + public const int GL_TEXTURE_CLIPMAP_FRAME_SGIX = 0x8172; + public const int GL_TEXTURE_CLIPMAP_OFFSET_SGIX = 0x8173; + public const int GL_TEXTURE_CLIPMAP_VIRTUAL_DEPTH_SGIX = 0x8174; + public const int GL_TEXTURE_CLIPMAP_LOD_OFFSET_SGIX = 0x8175; + public const int GL_TEXTURE_CLIPMAP_DEPTH_SGIX = 0x8176; + public const int GL_MAX_CLIPMAP_DEPTH_SGIX = 0x8177; + public const int GL_MAX_CLIPMAP_VIRTUAL_DEPTH_SGIX = 0x8178; + public const int GL_NEAREST_CLIPMAP_NEAREST_SGIX = 0x844D; + public const int GL_NEAREST_CLIPMAP_LINEAR_SGIX = 0x844E; + public const int GL_LINEAR_CLIPMAP_NEAREST_SGIX = 0x844F; + public const int GL_SGIX_convolution_accuracy = 1; + public const int GL_CONVOLUTION_HINT_SGIX = 0x8316; + public const int GL_SGIX_depth_pass_instrument = 1; + public const int GL_SGIX_depth_texture = 1; + public const int GL_DEPTH_COMPONENT16_SGIX = 0x81A5; + public const int GL_DEPTH_COMPONENT24_SGIX = 0x81A6; + public const int GL_DEPTH_COMPONENT32_SGIX = 0x81A7; + public const int GL_SGIX_flush_raster = 1; + public const int GL_SGIX_fog_offset = 1; + public const int GL_FOG_OFFSET_SGIX = 0x8198; + public const int GL_FOG_OFFSET_VALUE_SGIX = 0x8199; + public const int GL_SGIX_fragment_lighting = 1; + public const int GL_FRAGMENT_LIGHTING_SGIX = 0x8400; + public const int GL_FRAGMENT_COLOR_MATERIAL_SGIX = 0x8401; + public const int GL_FRAGMENT_COLOR_MATERIAL_FACE_SGIX = 0x8402; + public const int GL_FRAGMENT_COLOR_MATERIAL_PARAMETER_SGIX = 0x8403; + public const int GL_MAX_FRAGMENT_LIGHTS_SGIX = 0x8404; + public const int GL_MAX_ACTIVE_LIGHTS_SGIX = 0x8405; + public const int GL_CURRENT_RASTER_NORMAL_SGIX = 0x8406; + public const int GL_LIGHT_ENV_MODE_SGIX = 0x8407; + public const int GL_FRAGMENT_LIGHT_MODEL_LOCAL_VIEWER_SGIX = 0x8408; + public const int GL_FRAGMENT_LIGHT_MODEL_TWO_SIDE_SGIX = 0x8409; + public const int GL_FRAGMENT_LIGHT_MODEL_AMBIENT_SGIX = 0x840A; + public const int GL_FRAGMENT_LIGHT_MODEL_NORMAL_INTERPOLATION_SGIX = 0x840B; + public const int GL_FRAGMENT_LIGHT0_SGIX = 0x840C; + public const int GL_FRAGMENT_LIGHT1_SGIX = 0x840D; + public const int GL_FRAGMENT_LIGHT2_SGIX = 0x840E; + public const int GL_FRAGMENT_LIGHT3_SGIX = 0x840F; + public const int GL_FRAGMENT_LIGHT4_SGIX = 0x8410; + public const int GL_FRAGMENT_LIGHT5_SGIX = 0x8411; + public const int GL_FRAGMENT_LIGHT6_SGIX = 0x8412; + public const int GL_FRAGMENT_LIGHT7_SGIX = 0x8413; + public const int GL_SGIX_framezoom = 1; + public const int GL_FRAMEZOOM_SGIX = 0x818B; + public const int GL_FRAMEZOOM_FACTOR_SGIX = 0x818C; + public const int GL_MAX_FRAMEZOOM_FACTOR_SGIX = 0x818D; + public const int GL_SGIX_igloo_interface = 1; + public const int GL_SGIX_instruments = 1; + public const int GL_INSTRUMENT_BUFFER_POINTER_SGIX = 0x8180; + public const int GL_INSTRUMENT_MEASUREMENTS_SGIX = 0x8181; + public const int GL_SGIX_interlace = 1; + public const int GL_INTERLACE_SGIX = 0x8094; + public const int GL_SGIX_ir_instrument1 = 1; + public const int GL_IR_INSTRUMENT1_SGIX = 0x817F; + public const int GL_SGIX_list_priority = 1; + public const int GL_LIST_PRIORITY_SGIX = 0x8182; + public const int GL_SGIX_pixel_texture = 1; + public const int GL_PIXEL_TEX_GEN_SGIX = 0x8139; + public const int GL_PIXEL_TEX_GEN_MODE_SGIX = 0x832B; + public const int GL_SGIX_pixel_tiles = 1; + public const int GL_PIXEL_TILE_BEST_ALIGNMENT_SGIX = 0x813E; + public const int GL_PIXEL_TILE_CACHE_INCREMENT_SGIX = 0x813F; + public const int GL_PIXEL_TILE_WIDTH_SGIX = 0x8140; + public const int GL_PIXEL_TILE_HEIGHT_SGIX = 0x8141; + public const int GL_PIXEL_TILE_GRID_WIDTH_SGIX = 0x8142; + public const int GL_PIXEL_TILE_GRID_HEIGHT_SGIX = 0x8143; + public const int GL_PIXEL_TILE_GRID_DEPTH_SGIX = 0x8144; + public const int GL_PIXEL_TILE_CACHE_SIZE_SGIX = 0x8145; + public const int GL_SGIX_polynomial_ffd = 1; + public const int GL_TEXTURE_DEFORMATION_BIT_SGIX = 0x00000001; + public const int GL_GEOMETRY_DEFORMATION_BIT_SGIX = 0x00000002; + public const int GL_GEOMETRY_DEFORMATION_SGIX = 0x8194; + public const int GL_TEXTURE_DEFORMATION_SGIX = 0x8195; + public const int GL_DEFORMATIONS_MASK_SGIX = 0x8196; + public const int GL_MAX_DEFORMATION_ORDER_SGIX = 0x8197; + public const int GL_SGIX_reference_plane = 1; + public const int GL_REFERENCE_PLANE_SGIX = 0x817D; + public const int GL_REFERENCE_PLANE_EQUATION_SGIX = 0x817E; + public const int GL_SGIX_resample = 1; + public const int GL_PACK_RESAMPLE_SGIX = 0x842E; + public const int GL_UNPACK_RESAMPLE_SGIX = 0x842F; + public const int GL_RESAMPLE_REPLICATE_SGIX = 0x8433; + public const int GL_RESAMPLE_ZERO_FILL_SGIX = 0x8434; + public const int GL_RESAMPLE_DECIMATE_SGIX = 0x8430; + public const int GL_SGIX_scalebias_hint = 1; + public const int GL_SCALEBIAS_HINT_SGIX = 0x8322; + public const int GL_SGIX_shadow = 1; + public const int GL_TEXTURE_COMPARE_SGIX = 0x819A; + public const int GL_TEXTURE_COMPARE_OPERATOR_SGIX = 0x819B; + public const int GL_TEXTURE_LEQUAL_R_SGIX = 0x819C; + public const int GL_TEXTURE_GEQUAL_R_SGIX = 0x819D; + public const int GL_SGIX_shadow_ambient = 1; + public const int GL_SHADOW_AMBIENT_SGIX = 0x80BF; + public const int GL_SGIX_sprite = 1; + public const int GL_SPRITE_SGIX = 0x8148; + public const int GL_SPRITE_MODE_SGIX = 0x8149; + public const int GL_SPRITE_AXIS_SGIX = 0x814A; + public const int GL_SPRITE_TRANSLATION_SGIX = 0x814B; + public const int GL_SPRITE_AXIAL_SGIX = 0x814C; + public const int GL_SPRITE_OBJECT_ALIGNED_SGIX = 0x814D; + public const int GL_SPRITE_EYE_ALIGNED_SGIX = 0x814E; + public const int GL_SGIX_subsample = 1; + public const int GL_PACK_SUBSAMPLE_RATE_SGIX = 0x85A0; + public const int GL_UNPACK_SUBSAMPLE_RATE_SGIX = 0x85A1; + public const int GL_PIXEL_SUBSAMPLE_4444_SGIX = 0x85A2; + public const int GL_PIXEL_SUBSAMPLE_2424_SGIX = 0x85A3; + public const int GL_PIXEL_SUBSAMPLE_4242_SGIX = 0x85A4; + public const int GL_SGIX_tag_sample_buffer = 1; + public const int GL_SGIX_texture_add_env = 1; + public const int GL_TEXTURE_ENV_BIAS_SGIX = 0x80BE; + public const int GL_SGIX_texture_coordinate_clamp = 1; + public const int GL_TEXTURE_MAX_CLAMP_S_SGIX = 0x8369; + public const int GL_TEXTURE_MAX_CLAMP_T_SGIX = 0x836A; + public const int GL_TEXTURE_MAX_CLAMP_R_SGIX = 0x836B; + public const int GL_SGIX_texture_lod_bias = 1; + public const int GL_TEXTURE_LOD_BIAS_S_SGIX = 0x818E; + public const int GL_TEXTURE_LOD_BIAS_T_SGIX = 0x818F; + public const int GL_TEXTURE_LOD_BIAS_R_SGIX = 0x8190; + public const int GL_SGIX_texture_multi_buffer = 1; + public const int GL_TEXTURE_MULTI_BUFFER_HINT_SGIX = 0x812E; + public const int GL_SGIX_texture_scale_bias = 1; + public const int GL_POST_TEXTURE_FILTER_BIAS_SGIX = 0x8179; + public const int GL_POST_TEXTURE_FILTER_SCALE_SGIX = 0x817A; + public const int GL_POST_TEXTURE_FILTER_BIAS_RANGE_SGIX = 0x817B; + public const int GL_POST_TEXTURE_FILTER_SCALE_RANGE_SGIX = 0x817C; + public const int GL_SGIX_vertex_preclip = 1; + public const int GL_VERTEX_PRECLIP_SGIX = 0x83EE; + public const int GL_VERTEX_PRECLIP_HINT_SGIX = 0x83EF; + public const int GL_SGIX_ycrcb = 1; + public const int GL_YCRCB_422_SGIX = 0x81BB; + public const int GL_YCRCB_444_SGIX = 0x81BC; + public const int GL_SGIX_ycrcb_subsample = 1; + public const int GL_SGIX_ycrcba = 1; + public const int GL_YCRCB_SGIX = 0x8318; + public const int GL_YCRCBA_SGIX = 0x8319; + public const int GL_SGI_color_matrix = 1; + public const int GL_COLOR_MATRIX_SGI = 0x80B1; + public const int GL_COLOR_MATRIX_STACK_DEPTH_SGI = 0x80B2; + public const int GL_MAX_COLOR_MATRIX_STACK_DEPTH_SGI = 0x80B3; + public const int GL_POST_COLOR_MATRIX_RED_SCALE_SGI = 0x80B4; + public const int GL_POST_COLOR_MATRIX_GREEN_SCALE_SGI = 0x80B5; + public const int GL_POST_COLOR_MATRIX_BLUE_SCALE_SGI = 0x80B6; + public const int GL_POST_COLOR_MATRIX_ALPHA_SCALE_SGI = 0x80B7; + public const int GL_POST_COLOR_MATRIX_RED_BIAS_SGI = 0x80B8; + public const int GL_POST_COLOR_MATRIX_GREEN_BIAS_SGI = 0x80B9; + public const int GL_POST_COLOR_MATRIX_BLUE_BIAS_SGI = 0x80BA; + public const int GL_POST_COLOR_MATRIX_ALPHA_BIAS_SGI = 0x80BB; + public const int GL_SGI_color_table = 1; + public const int GL_COLOR_TABLE_SGI = 0x80D0; + public const int GL_POST_CONVOLUTION_COLOR_TABLE_SGI = 0x80D1; + public const int GL_POST_COLOR_MATRIX_COLOR_TABLE_SGI = 0x80D2; + public const int GL_PROXY_COLOR_TABLE_SGI = 0x80D3; + public const int GL_PROXY_POST_CONVOLUTION_COLOR_TABLE_SGI = 0x80D4; + public const int GL_PROXY_POST_COLOR_MATRIX_COLOR_TABLE_SGI = 0x80D5; + public const int GL_COLOR_TABLE_SCALE_SGI = 0x80D6; + public const int GL_COLOR_TABLE_BIAS_SGI = 0x80D7; + public const int GL_COLOR_TABLE_FORMAT_SGI = 0x80D8; + public const int GL_COLOR_TABLE_WIDTH_SGI = 0x80D9; + public const int GL_COLOR_TABLE_RED_SIZE_SGI = 0x80DA; + public const int GL_COLOR_TABLE_GREEN_SIZE_SGI = 0x80DB; + public const int GL_COLOR_TABLE_BLUE_SIZE_SGI = 0x80DC; + public const int GL_COLOR_TABLE_ALPHA_SIZE_SGI = 0x80DD; + public const int GL_COLOR_TABLE_LUMINANCE_SIZE_SGI = 0x80DE; + public const int GL_COLOR_TABLE_INTENSITY_SIZE_SGI = 0x80DF; + public const int GL_SGI_texture_color_table = 1; + public const int GL_TEXTURE_COLOR_TABLE_SGI = 0x80BC; + public const int GL_PROXY_TEXTURE_COLOR_TABLE_SGI = 0x80BD; + public const int GL_SUNX_constant_data = 1; + public const int GL_UNPACK_CONSTANT_DATA_SUNX = 0x81D5; + public const int GL_TEXTURE_CONSTANT_DATA_SUNX = 0x81D6; + public const int GL_SUN_convolution_border_modes = 1; + public const int GL_WRAP_BORDER_SUN = 0x81D4; + public const int GL_SUN_global_alpha = 1; + public const int GL_GLOBAL_ALPHA_SUN = 0x81D9; + public const int GL_GLOBAL_ALPHA_FACTOR_SUN = 0x81DA; + public const int GL_SUN_mesh_array = 1; + public const int GL_QUAD_MESH_SUN = 0x8614; + public const int GL_TRIANGLE_MESH_SUN = 0x8615; + public const int GL_SUN_slice_accum = 1; + public const int GL_SLICE_ACCUM_SUN = 0x85CC; + public const int GL_SUN_triangle_list = 1; + public const int GL_RESTART_SUN = 0x0001; + public const int GL_REPLACE_MIDDLE_SUN = 0x0002; + public const int GL_REPLACE_OLDEST_SUN = 0x0003; + public const int GL_TRIANGLE_LIST_SUN = 0x81D7; + public const int GL_REPLACEMENT_CODE_SUN = 0x81D8; + public const int GL_REPLACEMENT_CODE_ARRAY_SUN = 0x85C0; + public const int GL_REPLACEMENT_CODE_ARRAY_TYPE_SUN = 0x85C1; + public const int GL_REPLACEMENT_CODE_ARRAY_STRIDE_SUN = 0x85C2; + public const int GL_REPLACEMENT_CODE_ARRAY_POINTER_SUN = 0x85C3; + public const int GL_R1UI_V3F_SUN = 0x85C4; + public const int GL_R1UI_C4UB_V3F_SUN = 0x85C5; + public const int GL_R1UI_C3F_V3F_SUN = 0x85C6; + public const int GL_R1UI_N3F_V3F_SUN = 0x85C7; + public const int GL_R1UI_C4F_N3F_V3F_SUN = 0x85C8; + public const int GL_R1UI_T2F_V3F_SUN = 0x85C9; + public const int GL_R1UI_T2F_N3F_V3F_SUN = 0x85CA; + public const int GL_R1UI_T2F_C4F_N3F_V3F_SUN = 0x85CB; + public const int GL_SUN_vertex = 1; + public const int GL_WIN_phong_shading = 1; + public const int GL_PHONG_WIN = 0x80EA; + public const int GL_PHONG_HINT_WIN = 0x80EB; + public const int GL_WIN_specular_fog = 1; + public const int GL_FOG_SPECULAR_TEXTURE_WIN = 0x80EC; } } diff --git a/src/Avalonia.OpenGL/GlDisplayType.cs b/src/Avalonia.OpenGL/GlDisplayType.cs deleted file mode 100644 index 2e5178bc37..0000000000 --- a/src/Avalonia.OpenGL/GlDisplayType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Avalonia.OpenGL -{ - public enum GlDisplayType - { - OpenGL2, - OpenGLES2 - } -} \ No newline at end of file diff --git a/src/Avalonia.OpenGL/GlEntryPointAttribute.cs b/src/Avalonia.OpenGL/GlEntryPointAttribute.cs index 016c3d0af9..2ffdaca8fe 100644 --- a/src/Avalonia.OpenGL/GlEntryPointAttribute.cs +++ b/src/Avalonia.OpenGL/GlEntryPointAttribute.cs @@ -2,16 +2,117 @@ using System; namespace Avalonia.OpenGL { + public interface IGlEntryPointAttribute + { + IntPtr GetProcAddress(Func getProcAddress); + } + + public interface IGlEntryPointAttribute + { + IntPtr GetProcAddress(TContext context, Func getProcAddress); + } + [AttributeUsage(AttributeTargets.Property)] - public class GlEntryPointAttribute : Attribute + public class GlOptionalEntryPoint : Attribute + { + + } + + [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] + public class GlEntryPointAttribute : Attribute, IGlEntryPointAttribute + { + public string[] EntryPoints { get; } + + public GlEntryPointAttribute(string entryPoint) + { + EntryPoints = new []{entryPoint}; + } +/* + public GlEntryPointAttribute(params string[] entryPoints) + { + EntryPoints = entryPoints; + } +*/ + public IntPtr GetProcAddress(Func getProcAddress) + { + foreach (var name in EntryPoints) + { + var rv = getProcAddress(name); + if (rv != IntPtr.Zero) + return rv; + } + return IntPtr.Zero; + } + } + + [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] + public class GlMinVersionEntryPoint : Attribute, IGlEntryPointAttribute { - public string EntryPoint { get; } - public bool Optional { get; } + private readonly string _entry; + private readonly GlProfileType? _profile; + private readonly int _minVersionMajor; + private readonly int _minVersionMinor; - public GlEntryPointAttribute(string entryPoint, bool optional = false) + public GlMinVersionEntryPoint(string entry, GlProfileType profile, int minVersionMajor, + int minVersionMinor) { - EntryPoint = entryPoint; - Optional = optional; + _entry = entry; + _profile = profile; + _minVersionMajor = minVersionMajor; + _minVersionMinor = minVersionMinor; + } + + public GlMinVersionEntryPoint(string entry, int minVersionMajor, + int minVersionMinor) + { + _entry = entry; + _minVersionMajor = minVersionMajor; + _minVersionMinor = minVersionMinor; + } + + public IntPtr GetProcAddress(GlInterface.GlContextInfo context, Func getProcAddress) + { + if(_profile.HasValue && context.Version.Type != _profile) + return IntPtr.Zero; + if(context.Version.Major<_minVersionMajor) + return IntPtr.Zero; + if (context.Version.Major == _minVersionMajor && context.Version.Minor < _minVersionMinor) + return IntPtr.Zero; + return getProcAddress(_entry); + } + } + + [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] + public class GlExtensionEntryPoint : Attribute, IGlEntryPointAttribute + { + private readonly string _entry; + private readonly GlProfileType? _profile; + private readonly string _extension; + + public GlExtensionEntryPoint(string entry, GlProfileType profile, string extension) + { + _entry = entry; + _profile = profile; + _extension = extension; + } + + public GlExtensionEntryPoint(string entry, string extension) + { + _entry = entry; + _extension = extension; + } + + public IntPtr GetProcAddress(GlInterface.GlContextInfo context, Func getProcAddress) + { + // Ignore different profile type + if (_profile.HasValue && _profile != context.Version.Type) + return IntPtr.Zero; + + // Check if extension is supported by the current context + if (!context.Extensions.Contains(_extension)) + return IntPtr.Zero; + + return getProcAddress(_entry); } } } diff --git a/src/Avalonia.OpenGL/GlInterface.cs b/src/Avalonia.OpenGL/GlInterface.cs index 30f7d67152..23188e7dbf 100644 --- a/src/Avalonia.OpenGL/GlInterface.cs +++ b/src/Avalonia.OpenGL/GlInterface.cs @@ -1,31 +1,60 @@ using System; +using System.Collections.Generic; using System.Runtime.InteropServices; +using System.Text; using Avalonia.Platform.Interop; +using static Avalonia.OpenGL.GlConsts; namespace Avalonia.OpenGL { public delegate IntPtr GlGetProcAddressDelegate(string procName); - public class GlInterface : GlInterfaceBase + public unsafe class GlInterface : GlBasicInfoInterface { public string Version { get; } public string Vendor { get; } public string Renderer { get; } + public GlContextInfo ContextInfo { get; } - public GlInterface(Func getProcAddress) : base(getProcAddress) + public class GlContextInfo { + public GlVersion Version { get; } + public HashSet Extensions { get; } + + public GlContextInfo(GlVersion version, HashSet extensions) + { + Version = version; + Extensions = extensions; + } + + public static GlContextInfo Create(GlVersion version, Func getProcAddress) + { + var basicInfoInterface = new GlBasicInfoInterface(getProcAddress); + var exts = basicInfoInterface.GetExtensions(); + return new GlContextInfo(version, new HashSet(exts)); + } + } + + private GlInterface(GlContextInfo info, Func getProcAddress) : base(getProcAddress, info) + { + ContextInfo = info; Version = GetString(GlConsts.GL_VERSION); Renderer = GetString(GlConsts.GL_RENDERER); - Vendor = GetString(GlConsts.GL_VENDOR); + Vendor = GetString(GlConsts.GL_VENDOR); } - public GlInterface(Func n) : this(ConvertNative(n)) + public GlInterface(GlVersion version, Func getProcAddress) : this( + GlContextInfo.Create(version, getProcAddress), getProcAddress) + { + } + + public GlInterface(GlVersion version, Func n) : this(version, ConvertNative(n)) { } - public static GlInterface FromNativeUtf8GetProcAddress(Func getProcAddress) => - new GlInterface(getProcAddress); + public static GlInterface FromNativeUtf8GetProcAddress(GlVersion version, Func getProcAddress) => + new GlInterface(version, getProcAddress); public T GetProcAddress(string proc) => Marshal.GetDelegateForFunctionPointer(GetProcAddress(proc)); @@ -70,6 +99,249 @@ namespace Avalonia.OpenGL [GlEntryPoint("glGetIntegerv")] public GlGetIntegerv GetIntegerv { get; } + public delegate void GlGenFramebuffers(int count, int[] res); + [GlEntryPoint("glGenFramebuffers")] + public GlGenFramebuffers GenFramebuffers { get; } + + public delegate void GlDeleteFramebuffers(int count, int[] framebuffers); + [GlEntryPoint("glDeleteFramebuffers")] + public GlDeleteFramebuffers DeleteFramebuffers { get; } + + public delegate void GlBindFramebuffer(int target, int fb); + [GlEntryPoint("glBindFramebuffer")] + public GlBindFramebuffer BindFramebuffer { get; } + + public delegate int GlCheckFramebufferStatus(int target); + [GlEntryPoint("glCheckFramebufferStatus")] + public GlCheckFramebufferStatus CheckFramebufferStatus { get; } + + public delegate void GlGenRenderbuffers(int count, int[] res); + [GlEntryPoint("glGenRenderbuffers")] + public GlGenRenderbuffers GenRenderbuffers { get; } + + public delegate void GlDeleteRenderbuffers(int count, int[] renderbuffers); + [GlEntryPoint("glDeleteRenderbuffers")] + public GlDeleteTextures DeleteRenderbuffers { get; } + + public delegate void GlBindRenderbuffer(int target, int fb); + [GlEntryPoint("glBindRenderbuffer")] + public GlBindRenderbuffer BindRenderbuffer { get; } + + public delegate void GlRenderbufferStorage(int target, int internalFormat, int width, int height); + [GlEntryPoint("glRenderbufferStorage")] + public GlRenderbufferStorage RenderbufferStorage { get; } + + public delegate void GlFramebufferRenderbuffer(int target, int attachment, + int renderbufferTarget, int renderbuffer); + [GlEntryPoint("glFramebufferRenderbuffer")] + public GlFramebufferRenderbuffer FramebufferRenderbuffer { get; } + + public delegate void GlGenTextures(int count, int[] res); + [GlEntryPoint("glGenTextures")] + public GlGenTextures GenTextures { get; } + + public delegate void GlBindTexture(int target, int fb); + [GlEntryPoint("glBindTexture")] + public GlBindTexture BindTexture { get; } + + public delegate void GlDeleteTextures(int count, int[] textures); + [GlEntryPoint("glDeleteTextures")] + public GlDeleteTextures DeleteTextures { get; } + + + public delegate void GlTexImage2D(int target, int level, int internalFormat, int width, int height, int border, + int format, int type, IntPtr data); + [GlEntryPoint("glTexImage2D")] + public GlTexImage2D TexImage2D { get; } + + public delegate void GlTexParameteri(int target, int name, int value); + [GlEntryPoint("glTexParameteri")] + public GlTexParameteri TexParameteri { get; } + + public delegate void GlFramebufferTexture2D(int target, int attachment, + int texTarget, int texture, int level); + [GlEntryPoint("glFramebufferTexture2D")] + public GlFramebufferTexture2D FramebufferTexture2D { get; } + + public delegate int GlCreateShader(int shaderType); + [GlEntryPoint("glCreateShader")] + public GlCreateShader CreateShader { get; } + + public delegate void GlShaderSource(int shader, int count, IntPtr strings, IntPtr lengths); + [GlEntryPoint("glShaderSource")] + public GlShaderSource ShaderSource { get; } + + public void ShaderSourceString(int shader, string source) + { + using (var b = new Utf8Buffer(source)) + { + var ptr = b.DangerousGetHandle(); + var len = new IntPtr(b.ByteLen); + ShaderSource(shader, 1, new IntPtr(&ptr), new IntPtr(&len)); + } + } + + public delegate void GlCompileShader(int shader); + [GlEntryPoint("glCompileShader")] + public GlCompileShader CompileShader { get; } + + public delegate void GlGetShaderiv(int shader, int name, int* parameters); + [GlEntryPoint("glGetShaderiv")] + public GlGetShaderiv GetShaderiv { get; } + + public delegate void GlGetShaderInfoLog(int shader, int maxLength, out int length, void*infoLog); + [GlEntryPoint("glGetShaderInfoLog")] + public GlGetShaderInfoLog GetShaderInfoLog { get; } + + public unsafe string CompileShaderAndGetError(int shader, string source) + { + ShaderSourceString(shader, source); + CompileShader(shader); + int compiled; + GetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + if (compiled != 0) + return null; + int logLength; + GetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength); + if (logLength == 0) + logLength = 4096; + var logData = new byte[logLength]; + int len; + fixed (void* ptr = logData) + GetShaderInfoLog(shader, logLength, out len, ptr); + return Encoding.UTF8.GetString(logData,0, len); + } + + public delegate int GlCreateProgram(); + [GlEntryPoint("glCreateProgram")] + public GlCreateProgram CreateProgram { get; } + + public delegate void GlAttachShader(int program, int shader); + [GlEntryPoint("glAttachShader")] + public GlAttachShader AttachShader { get; } + + public delegate void GlLinkProgram(int program); + [GlEntryPoint("glLinkProgram")] + public GlLinkProgram LinkProgram { get; } + + public delegate void GlGetProgramiv(int program, int name, int* parameters); + [GlEntryPoint("glGetProgramiv")] + public GlGetProgramiv GetProgramiv { get; } + + public delegate void GlGetProgramInfoLog(int program, int maxLength, out int len, void* infoLog); + [GlEntryPoint("glGetProgramInfoLog")] + public GlGetProgramInfoLog GetProgramInfoLog { get; } + + public unsafe string LinkProgramAndGetError(int program) + { + LinkProgram(program); + int compiled; + GetProgramiv(program, GL_LINK_STATUS, &compiled); + if (compiled != 0) + return null; + int logLength; + GetProgramiv(program, GL_INFO_LOG_LENGTH, &logLength); + var logData = new byte[logLength]; + int len; + fixed (void* ptr = logData) + GetProgramInfoLog(program, logLength, out len, ptr); + return Encoding.UTF8.GetString(logData,0, len); + } + + public delegate void GlBindAttribLocation(int program, int index, IntPtr name); + [GlEntryPoint("glBindAttribLocation")] + public GlBindAttribLocation BindAttribLocation { get; } + + public void BindAttribLocationString(int program, int index, string name) + { + using (var b = new Utf8Buffer(name)) + BindAttribLocation(program, index, b.DangerousGetHandle()); + } + + public delegate void GlGenBuffers(int len, int[] rv); + [GlEntryPoint("glGenBuffers")] + public GlGenBuffers GenBuffers { get; } + + public int GenBuffer() + { + var rv = new int[1]; + GenBuffers(1, rv); + return rv[0]; + } + + public delegate void GlBindBuffer(int target, int buffer); + [GlEntryPoint("glBindBuffer")] + public GlBindBuffer BindBuffer { get; } + + public delegate void GlBufferData(int target, IntPtr size, IntPtr data, int usage); + [GlEntryPoint("glBufferData")] + public GlBufferData BufferData { get; } + + public delegate int GlGetAttribLocation(int program, IntPtr name); + [GlEntryPoint("glGetAttribLocation")] + public GlGetAttribLocation GetAttribLocation { get; } + + public int GetAttribLocationString(int program, string name) + { + using (var b = new Utf8Buffer(name)) + return GetAttribLocation(program, b.DangerousGetHandle()); + } + + public delegate void GlVertexAttribPointer(int index, int size, int type, + int normalized, int stride, IntPtr pointer); + [GlEntryPoint("glVertexAttribPointer")] + public GlVertexAttribPointer VertexAttribPointer { get; } + + public delegate void GlEnableVertexAttribArray(int index); + [GlEntryPoint("glEnableVertexAttribArray")] + public GlEnableVertexAttribArray EnableVertexAttribArray { get; } + + public delegate void GlUseProgram(int program); + [GlEntryPoint("glUseProgram")] + public GlUseProgram UseProgram { get; } + + public delegate void GlDrawArrays(int mode, int first, IntPtr count); + [GlEntryPoint("glDrawArrays")] + public GlDrawArrays DrawArrays { get; } + + public delegate void GlDrawElements(int mode, int count, int type, IntPtr indices); + [GlEntryPoint("glDrawElements")] + public GlDrawElements DrawElements { get; } + + public delegate int GlGetUniformLocation(int program, IntPtr name); + [GlEntryPoint("glGetUniformLocation")] + public GlGetUniformLocation GetUniformLocation { get; } + + public int GetUniformLocationString(int program, string name) + { + using (var b = new Utf8Buffer(name)) + return GetUniformLocation(program, b.DangerousGetHandle()); + } + + public delegate void GlUniform1f(int location, float falue); + [GlEntryPoint("glUniform1f")] + public GlUniform1f Uniform1f { get; } + + + public delegate void GlUniformMatrix4fv(int location, int count, bool transpose, void* value); + [GlEntryPoint("glUniformMatrix4fv")] + public GlUniformMatrix4fv UniformMatrix4fv { get; } + + public delegate void GlEnable(int what); + [GlEntryPoint("glEnable")] + public GlEnable Enable { get; } + + public delegate void GlDeleteBuffers(int count, int[] buffers); + [GlEntryPoint("glDeleteBuffers")] + public GlDeleteBuffers DeleteBuffers { get; } + + public delegate void GlDeleteProgram(int program); + [GlEntryPoint("glDeleteProgram")] + public GlDeleteProgram DeleteProgram { get; } + + public delegate void GlDeleteShader(int shader); + [GlEntryPoint("glDeleteShader")] + public GlDeleteShader DeleteShader { get; } // ReSharper restore UnassignedGetOnlyAutoProperty } } diff --git a/src/Avalonia.OpenGL/GlInterfaceBase.cs b/src/Avalonia.OpenGL/GlInterfaceBase.cs index 89ec0e77d4..e7dd440e36 100644 --- a/src/Avalonia.OpenGL/GlInterfaceBase.cs +++ b/src/Avalonia.OpenGL/GlInterfaceBase.cs @@ -1,53 +1,79 @@ using System; +using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using Avalonia.Platform.Interop; namespace Avalonia.OpenGL { - public class GlInterfaceBase + public class GlInterfaceBase : GlInterfaceBase { - - private readonly Func _getProcAddress; - public GlInterfaceBase(Func getProcAddress) + public GlInterfaceBase(Func getProcAddress) : base(getProcAddress, null) + { + } + + public GlInterfaceBase(Func nativeGetProcAddress) : base(nativeGetProcAddress, null) + { + } + } + + public class GlInterfaceBase + { + private readonly Func _getProcAddress; + public GlInterfaceBase(Func getProcAddress, TContext context) { _getProcAddress = getProcAddress; foreach (var prop in this.GetType().GetProperties()) { - var a = prop.GetCustomAttribute(); - if (a != null) + var attrs = prop.GetCustomAttributes() + .Where(a => + a is IGlEntryPointAttribute || a is IGlEntryPointAttribute) + .ToList(); + if(attrs.Count == 0) + continue; + + var isOptional = prop.GetCustomAttribute() != null; + + var fieldName = $"<{prop.Name}>k__BackingField"; + var field = prop.DeclaringType.GetField(fieldName, + BindingFlags.Instance | BindingFlags.NonPublic); + if (field == null) + throw new InvalidProgramException($"Expected property {prop.Name} to have {fieldName}"); + + + IntPtr proc = IntPtr.Zero; + foreach (var attr in attrs) { - var fieldName = $"<{prop.Name}>k__BackingField"; - var field = prop.DeclaringType.GetField(fieldName, - BindingFlags.Instance | BindingFlags.NonPublic); - if (field == null) - throw new InvalidProgramException($"Expected property {prop.Name} to have {fieldName}"); - var proc = getProcAddress(a.EntryPoint, a.Optional); + if (attr is IGlEntryPointAttribute typed) + proc = typed.GetProcAddress(context, getProcAddress); + else if (attr is IGlEntryPointAttribute untyped) + proc = untyped.GetProcAddress(getProcAddress); if (proc != IntPtr.Zero) - field.SetValue(this, Marshal.GetDelegateForFunctionPointer(proc, prop.PropertyType)); + break; } + + if (proc != IntPtr.Zero) + field.SetValue(this, Marshal.GetDelegateForFunctionPointer(proc, prop.PropertyType)); + else if (!isOptional) + throw new OpenGlException("Unable to find a suitable GL function for " + prop.Name); } } - protected static Func ConvertNative(Func func) => - (proc, optional) => + protected static Func ConvertNative(Func func) => + (proc) => { using (var u = new Utf8Buffer(proc)) { var rv = func(u); - if (rv == IntPtr.Zero && !optional) - throw new OpenGlException("Missing function " + proc); return rv; } }; - public GlInterfaceBase(Func nativeGetProcAddress) : this(ConvertNative(nativeGetProcAddress)) + public GlInterfaceBase(Func nativeGetProcAddress, TContext context) : this(ConvertNative(nativeGetProcAddress), context) { } - public IntPtr GetProcAddress(string proc) => _getProcAddress(proc, true); - public IntPtr GetProcAddress(string proc, bool optional) => _getProcAddress(proc, optional); - + public IntPtr GetProcAddress(string proc) => _getProcAddress(proc); } } diff --git a/src/Avalonia.OpenGL/GlVersion.cs b/src/Avalonia.OpenGL/GlVersion.cs new file mode 100644 index 0000000000..042ff4c2f0 --- /dev/null +++ b/src/Avalonia.OpenGL/GlVersion.cs @@ -0,0 +1,22 @@ +namespace Avalonia.OpenGL +{ + public enum GlProfileType + { + OpenGL, + OpenGLES + } + + public struct GlVersion + { + public GlProfileType Type { get; } + public int Major { get; } + public int Minor { get; } + + public GlVersion(GlProfileType type, int major, int minor) + { + Type = type; + Major = major; + Minor = minor; + } + } +} diff --git a/src/Avalonia.OpenGL/IGlContext.cs b/src/Avalonia.OpenGL/IGlContext.cs index 04aa1b98e4..eb4313fba9 100644 --- a/src/Avalonia.OpenGL/IGlContext.cs +++ b/src/Avalonia.OpenGL/IGlContext.cs @@ -1,8 +1,13 @@ +using System; + namespace Avalonia.OpenGL { - public interface IGlContext + public interface IGlContext : IDisposable { - IGlDisplay Display { get; } - void MakeCurrent(); + GlVersion Version { get; } + GlInterface GlInterface { get; } + int SampleCount { get; } + int StencilSize { get; } + IDisposable MakeCurrent(); } } diff --git a/src/Avalonia.OpenGL/IGlDisplay.cs b/src/Avalonia.OpenGL/IGlDisplay.cs deleted file mode 100644 index e7568bd5e7..0000000000 --- a/src/Avalonia.OpenGL/IGlDisplay.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Avalonia.OpenGL -{ - public interface IGlDisplay - { - GlDisplayType Type { get; } - GlInterface GlInterface { get; } - void ClearContext(); - int SampleCount { get; } - int StencilSize { get; } - } -} diff --git a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderingSession.cs b/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderingSession.cs index 4b0de05b77..89911a20a8 100644 --- a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderingSession.cs +++ b/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderingSession.cs @@ -4,8 +4,9 @@ namespace Avalonia.OpenGL { public interface IGlPlatformSurfaceRenderingSession : IDisposable { - IGlDisplay Display { get; } + IGlContext Context { get; } PixelSize Size { get; } double Scaling { get; } + bool IsYFlipped { get; } } } diff --git a/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs b/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs new file mode 100644 index 0000000000..30f83745ad --- /dev/null +++ b/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs @@ -0,0 +1,9 @@ +using Avalonia.OpenGL.Imaging; + +namespace Avalonia.OpenGL +{ + public interface IOpenGlAwarePlatformRenderInterface + { + IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap(); + } +} diff --git a/src/Avalonia.OpenGL/IWindowingPlatformGlFeature.cs b/src/Avalonia.OpenGL/IWindowingPlatformGlFeature.cs index 2c4a8b64b3..b91496f42b 100644 --- a/src/Avalonia.OpenGL/IWindowingPlatformGlFeature.cs +++ b/src/Avalonia.OpenGL/IWindowingPlatformGlFeature.cs @@ -2,6 +2,7 @@ namespace Avalonia.OpenGL { public interface IWindowingPlatformGlFeature { - IGlContext ImmediateContext { get; } + IGlContext CreateContext(); + IGlContext MainContext { get; } } } diff --git a/src/Avalonia.OpenGL/Imaging/IOpenGlTextureBitmapImpl.cs b/src/Avalonia.OpenGL/Imaging/IOpenGlTextureBitmapImpl.cs new file mode 100644 index 0000000000..e5f3691569 --- /dev/null +++ b/src/Avalonia.OpenGL/Imaging/IOpenGlTextureBitmapImpl.cs @@ -0,0 +1,13 @@ +using System; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace Avalonia.OpenGL.Imaging +{ + public interface IOpenGlTextureBitmapImpl : IBitmapImpl + { + IDisposable Lock(); + void SetBackBuffer(int textureId, int internalFormat, PixelSize pixelSize, double dpiScaling); + void SetDirty(); + } +} diff --git a/src/Avalonia.OpenGL/Imaging/OpenGlTextureBitmap.cs b/src/Avalonia.OpenGL/Imaging/OpenGlTextureBitmap.cs new file mode 100644 index 0000000000..558eae8fdf --- /dev/null +++ b/src/Avalonia.OpenGL/Imaging/OpenGlTextureBitmap.cs @@ -0,0 +1,46 @@ +using System; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Threading; + +namespace Avalonia.OpenGL.Imaging +{ + public class OpenGlTextureBitmap : Bitmap, IAffectsRender + { + private IOpenGlTextureBitmapImpl _impl; + static IOpenGlTextureBitmapImpl CreateOrThrow() + { + if (!(AvaloniaLocator.Current.GetService() is IOpenGlAwarePlatformRenderInterface + glAware)) + throw new PlatformNotSupportedException("Rendering platform does not support OpenGL integration"); + return glAware.CreateOpenGlTextureBitmap(); + } + + public OpenGlTextureBitmap() + : base(CreateOrThrow()) + { + _impl = (IOpenGlTextureBitmapImpl)PlatformImpl.Item; + } + + public IDisposable Lock() => _impl.Lock(); + + public void SetTexture(int textureId, int internalFormat, PixelSize size, double dpiScaling) + { + _impl.SetBackBuffer(textureId, internalFormat, size, dpiScaling); + SetIsDirty(); + } + + public void SetIsDirty() + { + if (Dispatcher.UIThread.CheckAccess()) + CallInvalidated(); + else + Dispatcher.UIThread.Post(CallInvalidated); + } + + private void CallInvalidated() => Invalidated?.Invoke(this, EventArgs.Empty); + + public event EventHandler Invalidated; + } +} diff --git a/src/Avalonia.OpenGL/OpenGlControlBase.cs b/src/Avalonia.OpenGL/OpenGlControlBase.cs new file mode 100644 index 0000000000..8567dcae20 --- /dev/null +++ b/src/Avalonia.OpenGL/OpenGlControlBase.cs @@ -0,0 +1,215 @@ +using System; +using Avalonia.Controls; +using Avalonia.Logging; +using Avalonia.Media; +using Avalonia.OpenGL.Imaging; +using Avalonia.Rendering; +using Avalonia.VisualTree; +using static Avalonia.OpenGL.GlConsts; + +namespace Avalonia.OpenGL +{ + public abstract class OpenGlControlBase : Control + { + private IGlContext _context; + private int _fb, _texture, _renderBuffer; + private OpenGlTextureBitmap _bitmap; + private PixelSize _oldSize; + private bool _glFailed; + protected GlVersion GlVersion { get; private set; } + public sealed override void Render(DrawingContext context) + { + if(!EnsureInitialized()) + return; + + using (_context.MakeCurrent()) + { + using (_bitmap.Lock()) + { + var gl = _context.GlInterface; + gl.BindFramebuffer(GL_FRAMEBUFFER, _fb); + if (_oldSize != GetPixelSize()) + ResizeTexture(gl); + + OnOpenGlRender(gl, _fb); + gl.Flush(); + } + } + + context.DrawImage(_bitmap, new Rect(_bitmap.Size), Bounds); + base.Render(context); + } + + void DoCleanup(bool callUserDeinit) + { + if (_context != null) + { + using (_context.MakeCurrent()) + { + var gl = _context.GlInterface; + gl.BindTexture(GL_TEXTURE_2D, 0); + gl.BindFramebuffer(GL_FRAMEBUFFER, 0); + gl.DeleteFramebuffers(1, new[] { _fb }); + using (_bitmap.Lock()) + _bitmap.SetTexture(0, 0, new PixelSize(1, 1), 1); + gl.DeleteTextures(1, new[] { _texture }); + gl.DeleteRenderbuffers(1, new[] { _renderBuffer }); + _bitmap.Dispose(); + + try + { + if (callUserDeinit) + OnOpenGlDeinit(_context.GlInterface, _fb); + } + finally + { + _context.Dispose(); + _context = null; + } + } + } + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + DoCleanup(true); + base.OnDetachedFromVisualTree(e); + } + + bool EnsureInitialized() + { + if (_context != null) + return true; + + if (_glFailed) + return false; + + var feature = AvaloniaLocator.Current.GetService(); + if (feature == null) + return false; + try + { + _context = feature.CreateContext(); + + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL: unable to create additional OpenGL context: {exception}", e); + _glFailed = true; + return false; + } + + GlVersion = _context.Version; + try + { + _bitmap = new OpenGlTextureBitmap(); + } + catch (Exception e) + { + _context.Dispose(); + _context = null; + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL: unable to create OpenGlTextureBitmap: {exception}", e); + _glFailed = true; + return false; + } + + using (_context.MakeCurrent()) + { + try + { + _oldSize = GetPixelSize(); + var gl = _context.GlInterface; + var oneArr = new int[1]; + gl.GenFramebuffers(1, oneArr); + _fb = oneArr[0]; + gl.BindFramebuffer(GL_FRAMEBUFFER, _fb); + + gl.GenTextures(1, oneArr); + _texture = oneArr[0]; + + ResizeTexture(gl); + + gl.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0); + + var status = gl.CheckFramebufferStatus(GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) + { + int code; + while ((code = gl.GetError()) != 0) + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL FBO: {code}", code); + + _glFailed = true; + return false; + } + } + catch(Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL FBO: {exception}", e); + _glFailed = true; + } + + if (!_glFailed) + OnOpenGlInit(_context.GlInterface, _fb); + } + + if (_glFailed) + { + DoCleanup(false); + } + + return true; + } + + void ResizeTexture(GlInterface gl) + { + var size = GetPixelSize(); + + gl.GetIntegerv( GL_TEXTURE_BINDING_2D, out var oldTexture); + gl.BindTexture(GL_TEXTURE_2D, _texture); + gl.TexImage2D(GL_TEXTURE_2D, 0, + GlVersion.Type == GlProfileType.OpenGLES ? GL_RGBA : GL_RGBA8, + size.Width, size.Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, IntPtr.Zero); + gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + gl.BindTexture(GL_TEXTURE_2D, oldTexture); + + gl.GetIntegerv(GL_RENDERBUFFER_BINDING, out var oldRenderBuffer); + gl.DeleteRenderbuffers(1, new[] { _renderBuffer }); + var oneArr = new int[1]; + gl.GenRenderbuffers(1, oneArr); + _renderBuffer = oneArr[0]; + gl.BindRenderbuffer(GL_RENDERBUFFER, _renderBuffer); + gl.RenderbufferStorage(GL_RENDERBUFFER, + GlVersion.Type == GlProfileType.OpenGLES ? GL_DEPTH_COMPONENT16 : GL_DEPTH_COMPONENT, + size.Width, size.Height); + gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _renderBuffer); + gl.BindRenderbuffer(GL_RENDERBUFFER, oldRenderBuffer); + using (_bitmap.Lock()) + _bitmap.SetTexture(_texture, GL_RGBA8, size, 1); + } + + PixelSize GetPixelSize() + { + var scaling = VisualRoot.RenderScaling; + return new PixelSize(Math.Max(1, (int)(Bounds.Width * scaling)), + Math.Max(1, (int)(Bounds.Height * scaling))); + } + + + protected virtual void OnOpenGlInit(GlInterface gl, int fb) + { + + } + + protected virtual void OnOpenGlDeinit(GlInterface gl, int fb) + { + + } + + protected abstract void OnOpenGlRender(GlInterface gl, int fb); + } +} diff --git a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs index ced26a3004..6771d3e179 100644 --- a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs +++ b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs @@ -1,6 +1,3 @@ -// 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.Controls; using Avalonia.Threading; using ReactiveUI; @@ -18,7 +15,7 @@ namespace Avalonia.ReactiveUI public static TAppBuilder UseReactiveUI(this TAppBuilder builder) where TAppBuilder : AppBuilderBase, new() { - return builder.AfterSetup(_ => + return builder.AfterPlatformServicesSetup(_ => { RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); diff --git a/src/Avalonia.ReactiveUI/Attributes.cs b/src/Avalonia.ReactiveUI/Attributes.cs index e908d1de80..31c374392f 100644 --- a/src/Avalonia.ReactiveUI/Attributes.cs +++ b/src/Avalonia.ReactiveUI/Attributes.cs @@ -1,6 +1,3 @@ -// 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 System.Reflection; using System.Runtime.CompilerServices; using Avalonia.Metadata; diff --git a/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs b/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs index b404ea57cb..bd21503a58 100644 --- a/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs +++ b/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs @@ -1,6 +1,3 @@ -// 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; using Avalonia.VisualTree; using Avalonia.Controls; diff --git a/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs b/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs index 5a4d625c41..5c713804e9 100644 --- a/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs +++ b/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs @@ -1,10 +1,5 @@ -// 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 System; -using System.Reflection; using System.Reactive.Linq; -using Avalonia; using Avalonia.VisualTree; using Avalonia.Controls; using ReactiveUI; @@ -21,7 +16,7 @@ namespace Avalonia.ReactiveUI /// public int GetAffinityForView(Type view) { - return typeof(IVisual).GetTypeInfo().IsAssignableFrom(view.GetTypeInfo()) ? 10 : 0; + return typeof(IVisual).IsAssignableFrom(view) ? 10 : 0; } /// diff --git a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs index 010acc3ae0..fc48a8853d 100644 --- a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs +++ b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs @@ -1,6 +1,3 @@ -// 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; using Avalonia.VisualTree; using Avalonia.Controls; @@ -15,7 +12,7 @@ namespace Avalonia.ReactiveUI /// ViewModel type. public class ReactiveUserControl : UserControl, IViewFor where TViewModel : class { - public static readonly AvaloniaProperty ViewModelProperty = AvaloniaProperty + public static readonly StyledProperty ViewModelProperty = AvaloniaProperty .Register, TViewModel>(nameof(ViewModel)); /// diff --git a/src/Avalonia.ReactiveUI/ReactiveWindow.cs b/src/Avalonia.ReactiveUI/ReactiveWindow.cs index f0f115afbc..5695a727af 100644 --- a/src/Avalonia.ReactiveUI/ReactiveWindow.cs +++ b/src/Avalonia.ReactiveUI/ReactiveWindow.cs @@ -1,6 +1,3 @@ -// 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; using Avalonia.VisualTree; using Avalonia.Controls; @@ -15,7 +12,7 @@ namespace Avalonia.ReactiveUI /// ViewModel type. public class ReactiveWindow : Window, IViewFor where TViewModel : class { - public static readonly AvaloniaProperty ViewModelProperty = AvaloniaProperty + public static readonly StyledProperty ViewModelProperty = AvaloniaProperty .Register, TViewModel>(nameof(ViewModel)); /// @@ -41,4 +38,4 @@ namespace Avalonia.ReactiveUI set => ViewModel = (TViewModel)value; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index ac5db32c14..1af8012a20 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Reactive.Disposables; using System.Reactive.Linq; @@ -58,7 +55,7 @@ namespace Avalonia.ReactiveUI /// /// for the property. /// - public static readonly AvaloniaProperty RouterProperty = + public static readonly StyledProperty RouterProperty = AvaloniaProperty.Register(nameof(Router)); /// @@ -118,4 +115,4 @@ namespace Avalonia.ReactiveUI Content = viewInstance; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs index 1bec5fc365..cc1613ce19 100644 --- a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs +++ b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Animation; using Avalonia.Controls; @@ -16,14 +13,14 @@ namespace Avalonia.ReactiveUI /// /// for the property. /// - public static readonly AvaloniaProperty PageTransitionProperty = + public static readonly StyledProperty PageTransitionProperty = AvaloniaProperty.Register(nameof(PageTransition), new CrossFade(TimeSpan.FromSeconds(0.5))); /// /// for the property. /// - public static readonly AvaloniaProperty DefaultContentProperty = + public static readonly StyledProperty DefaultContentProperty = AvaloniaProperty.Register(nameof(DefaultContent)); /// @@ -72,4 +69,4 @@ namespace Avalonia.ReactiveUI await PageTransition.Start(null, this, true); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs index 5cfa464c37..0169b268ca 100644 --- a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs +++ b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Reactive.Disposables; using ReactiveUI; diff --git a/src/Avalonia.Remote.Protocol/BsonStreamTransport.cs b/src/Avalonia.Remote.Protocol/BsonStreamTransport.cs index 78712985d9..c58aafa4e0 100644 --- a/src/Avalonia.Remote.Protocol/BsonStreamTransport.cs +++ b/src/Avalonia.Remote.Protocol/BsonStreamTransport.cs @@ -144,5 +144,9 @@ namespace Avalonia.Remote.Protocol public event Action OnMessage; public event Action OnException; + public void Start() + { + + } } } diff --git a/src/Avalonia.Remote.Protocol/DesignMessages.cs b/src/Avalonia.Remote.Protocol/DesignMessages.cs index 5ff16c574d..5c769ad48c 100644 --- a/src/Avalonia.Remote.Protocol/DesignMessages.cs +++ b/src/Avalonia.Remote.Protocol/DesignMessages.cs @@ -1,4 +1,7 @@ using System; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Xml; namespace Avalonia.Remote.Protocol.Designer { @@ -26,6 +29,27 @@ namespace Avalonia.Remote.Protocol.Designer public class ExceptionDetails { + public ExceptionDetails() + { + } + + public ExceptionDetails(Exception e) + { + if (e is TargetInvocationException) + { + e = e.InnerException; + } + + ExceptionType = e.GetType().Name; + Message = e.Message; + + if (e is XmlException xml) + { + LineNumber = xml.LineNumber; + LinePosition = xml.LinePosition; + } + } + public string ExceptionType { get; set; } public string Message { get; set; } public int? LineNumber { get; set; } diff --git a/src/Avalonia.Remote.Protocol/ITransport.cs b/src/Avalonia.Remote.Protocol/ITransport.cs index 0f419b95ae..6772fa75b2 100644 --- a/src/Avalonia.Remote.Protocol/ITransport.cs +++ b/src/Avalonia.Remote.Protocol/ITransport.cs @@ -8,5 +8,6 @@ namespace Avalonia.Remote.Protocol Task Send(object data); event Action OnMessage; event Action OnException; + void Start(); } } diff --git a/src/Avalonia.Remote.Protocol/TransportConnectionWrapper.cs b/src/Avalonia.Remote.Protocol/TransportConnectionWrapper.cs index d7919af9d9..77c77a70ad 100644 --- a/src/Avalonia.Remote.Protocol/TransportConnectionWrapper.cs +++ b/src/Avalonia.Remote.Protocol/TransportConnectionWrapper.cs @@ -98,5 +98,7 @@ namespace Avalonia.Remote.Protocol add => _onException.Add(value); remove => _onException.Remove(value); } + + public void Start() => _conn.Start(); } } diff --git a/src/Avalonia.Remote.Protocol/TransportMessages.cs b/src/Avalonia.Remote.Protocol/TransportMessages.cs new file mode 100644 index 0000000000..08486806c7 --- /dev/null +++ b/src/Avalonia.Remote.Protocol/TransportMessages.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Remote.Protocol +{ + [AvaloniaRemoteMessageGuid("53778004-78fa-4381-8ec3-176a6f2328b6")] + public class HtmlTransportStartedMessage + { + public string Uri { get; set; } + + } +} diff --git a/src/Avalonia.Remote.Protocol/ViewportMessages.cs b/src/Avalonia.Remote.Protocol/ViewportMessages.cs index dc4550b846..06881f2c7f 100644 --- a/src/Avalonia.Remote.Protocol/ViewportMessages.cs +++ b/src/Avalonia.Remote.Protocol/ViewportMessages.cs @@ -60,6 +60,8 @@ public int Width { get; set; } public int Height { get; set; } public int Stride { get; set; } + public double DpiX { get; set; } + public double DpiY { get; set; } } } diff --git a/src/Avalonia.Styling/Avalonia.Styling.csproj b/src/Avalonia.Styling/Avalonia.Styling.csproj index a396cee35f..b4f6c2c942 100644 --- a/src/Avalonia.Styling/Avalonia.Styling.csproj +++ b/src/Avalonia.Styling/Avalonia.Styling.csproj @@ -8,5 +8,4 @@ - diff --git a/src/Avalonia.Styling/Controls/Classes.cs b/src/Avalonia.Styling/Controls/Classes.cs index 3981ec01ad..51dca57928 100644 --- a/src/Avalonia.Styling/Controls/Classes.cs +++ b/src/Avalonia.Styling/Controls/Classes.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using Avalonia.Collections; diff --git a/src/Avalonia.Styling/Controls/INameScope.cs b/src/Avalonia.Styling/Controls/INameScope.cs index b4f4c8e57b..2d5295fe45 100644 --- a/src/Avalonia.Styling/Controls/INameScope.cs +++ b/src/Avalonia.Styling/Controls/INameScope.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Threading.Tasks; using Avalonia.Utilities; diff --git a/src/Avalonia.Styling/Controls/IPseudoClasses.cs b/src/Avalonia.Styling/Controls/IPseudoClasses.cs index 6c59078ebc..27290716d0 100644 --- a/src/Avalonia.Styling/Controls/IPseudoClasses.cs +++ b/src/Avalonia.Styling/Controls/IPseudoClasses.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Controls { diff --git a/src/Avalonia.Styling/Controls/IResourceDictionary.cs b/src/Avalonia.Styling/Controls/IResourceDictionary.cs index 2a395f44b4..3a68dde31e 100644 --- a/src/Avalonia.Styling/Controls/IResourceDictionary.cs +++ b/src/Avalonia.Styling/Controls/IResourceDictionary.cs @@ -1,14 +1,13 @@ -// 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 System.Collections.Generic; -using System.Collections.Generic; +#nullable enable namespace Avalonia.Controls { /// /// An indexed dictionary of resources. /// - public interface IResourceDictionary : IResourceProvider, IDictionary + public interface IResourceDictionary : IResourceProvider, IDictionary { /// /// Gets a collection of child resource dictionaries. diff --git a/src/Avalonia.Styling/Controls/IResourceHost.cs b/src/Avalonia.Styling/Controls/IResourceHost.cs new file mode 100644 index 0000000000..fed9359d94 --- /dev/null +++ b/src/Avalonia.Styling/Controls/IResourceHost.cs @@ -0,0 +1,32 @@ +using System; + +#nullable enable + +namespace Avalonia.Controls +{ + /// + /// Represents an element which hosts resources. + /// + /// + /// This interface is implemented by and `Application`. + /// + public interface IResourceHost : IResourceNode + { + /// + /// Raised when the resources change on the element or an ancestor of the element. + /// + event EventHandler ResourcesChanged; + + /// + /// Notifies the resource host that one or more of its hosted resources has changed. + /// + /// The event args. + /// + /// This method will be called automatically by the framework, you should not need to call + /// this method yourself. It is called when the resources hosted by this element have + /// changed, and is usually called by a resource dictionary or style hosted by the element + /// in response to a resource being added or removed. + /// + void NotifyHostedResourcesChanged(ResourcesChangedEventArgs e); + } +} diff --git a/src/Avalonia.Styling/Controls/IResourceNode.cs b/src/Avalonia.Styling/Controls/IResourceNode.cs index b6a6cdc3e3..73bfeaf161 100644 --- a/src/Avalonia.Styling/Controls/IResourceNode.cs +++ b/src/Avalonia.Styling/Controls/IResourceNode.cs @@ -1,15 +1,35 @@ using System; +#nullable enable + namespace Avalonia.Controls { /// - /// Represents resource provider in a tree. + /// Represents an object that can be queried for resources. /// - public interface IResourceNode : IResourceProvider + /// + /// The interface represents a common interface for both controls that host resources + /// () and resource providers such as + /// (see ). + /// + public interface IResourceNode { /// - /// Gets the parent resource node, if any. + /// Gets a value indicating whether the object has resources. + /// + bool HasResources { get; } + + /// + /// Tries to find a resource within the object. /// - IResourceNode ResourceParent { get; } + /// The resource key. + /// + /// When this method returns, contains the value associated with the specified key, + /// if the key is found; otherwise, null. + /// + /// + /// True if the resource if found, otherwise false. + /// + bool TryGetResource(object key, out object? value); } } diff --git a/src/Avalonia.Styling/Controls/IResourceProvider.cs b/src/Avalonia.Styling/Controls/IResourceProvider.cs index cbaacee012..27a8c2da68 100644 --- a/src/Avalonia.Styling/Controls/IResourceProvider.cs +++ b/src/Avalonia.Styling/Controls/IResourceProvider.cs @@ -1,33 +1,42 @@ using System; +using Avalonia.Styling; + +#nullable enable namespace Avalonia.Controls { /// - /// Represents an object that can be queried for resources. + /// Represents an object that can be queried for resources but does not appear in the logical tree. /// - public interface IResourceProvider + /// + /// This interface is implemented by , and + /// + /// + public interface IResourceProvider : IResourceNode { /// - /// Raised when resources in the provider are changed. + /// Gets the owner of the resource provider. + /// + /// + /// If multiple owners are added, returns the first. + /// + IResourceHost? Owner { get; } + + /// + /// Raised when the of the resource provider changes. /// - event EventHandler ResourcesChanged; + event EventHandler OwnerChanged; /// - /// Gets a value indicating whether the element has resources. + /// Adds an owner to the resource provider. /// - bool HasResources { get; } + /// The owner. + void AddOwner(IResourceHost owner); /// - /// Tries to find a resource within the provider. + /// Removes a resource provider owner. /// - /// The resource key. - /// - /// When this method returns, contains the value associated with the specified key, - /// if the key is found; otherwise, null. - /// - /// - /// True if the resource if found, otherwise false. - /// - bool TryGetResource(object key, out object value); + /// The owner. + void RemoveOwner(IResourceHost owner); } } diff --git a/src/Avalonia.Styling/Controls/ISetInheritanceParent.cs b/src/Avalonia.Styling/Controls/ISetInheritanceParent.cs index 1a778fa0f9..ef3c1d5b89 100644 --- a/src/Avalonia.Styling/Controls/ISetInheritanceParent.cs +++ b/src/Avalonia.Styling/Controls/ISetInheritanceParent.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Controls { /// diff --git a/src/Avalonia.Styling/Controls/ISetLogicalParent.cs b/src/Avalonia.Styling/Controls/ISetLogicalParent.cs index 5fb450c896..0c0cd1c1bb 100644 --- a/src/Avalonia.Styling/Controls/ISetLogicalParent.cs +++ b/src/Avalonia.Styling/Controls/ISetLogicalParent.cs @@ -1,6 +1,3 @@ -// 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.LogicalTree; namespace Avalonia.Controls diff --git a/src/Avalonia.Styling/Controls/NameScope.cs b/src/Avalonia.Styling/Controls/NameScope.cs index ec01a53cfd..62a04eac8b 100644 --- a/src/Avalonia.Styling/Controls/NameScope.cs +++ b/src/Avalonia.Styling/Controls/NameScope.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/src/Avalonia.Styling/Controls/NameScopeEventArgs.cs b/src/Avalonia.Styling/Controls/NameScopeEventArgs.cs index 8fc4d12799..3e9eaa6057 100644 --- a/src/Avalonia.Styling/Controls/NameScopeEventArgs.cs +++ b/src/Avalonia.Styling/Controls/NameScopeEventArgs.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Controls diff --git a/src/Avalonia.Styling/Controls/NameScopeExtensions.cs b/src/Avalonia.Styling/Controls/NameScopeExtensions.cs index 5921acd3b6..75630711b8 100644 --- a/src/Avalonia.Styling/Controls/NameScopeExtensions.cs +++ b/src/Avalonia.Styling/Controls/NameScopeExtensions.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Linq; diff --git a/src/Avalonia.Styling/Controls/NameScopeLocator.cs b/src/Avalonia.Styling/Controls/NameScopeLocator.cs index 354ed33657..51f4c5c4eb 100644 --- a/src/Avalonia.Styling/Controls/NameScopeLocator.cs +++ b/src/Avalonia.Styling/Controls/NameScopeLocator.cs @@ -1,11 +1,5 @@ using System; -using System.Linq; using System.Reactive.Disposables; -using System.Reactive.Threading.Tasks; -using System.Reflection; -using System.Threading.Tasks; -using Avalonia.LogicalTree; -using Avalonia.Reactive; using Avalonia.Utilities; namespace Avalonia.Controls diff --git a/src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs b/src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs new file mode 100644 index 0000000000..7b28d1a911 --- /dev/null +++ b/src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs @@ -0,0 +1,28 @@ +using System; + +namespace Avalonia.Controls +{ + public static class PseudolassesExtensions + { + /// + /// Adds or removes a pseudoclass depending on a boolean value. + /// + /// The pseudoclasses collection. + /// The name of the pseudoclass to set. + /// True to add the pseudoclass or false to remove. + public static void Set(this IPseudoClasses classes, string name, bool value) + { + Contract.Requires(classes != null); + + if (value) + { + classes.Add(name); + } + else + { + classes.Remove(name); + } + } + + } +} diff --git a/src/Avalonia.Styling/Controls/ResourceDictionary.cs b/src/Avalonia.Styling/Controls/ResourceDictionary.cs index 901e27b7b7..c56dd74143 100644 --- a/src/Avalonia.Styling/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Styling/Controls/ResourceDictionary.cs @@ -1,20 +1,20 @@ -// 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 System; +using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using Avalonia.Collections; +using Avalonia.Metadata; + +#nullable enable namespace Avalonia.Controls { /// /// An indexed dictionary of resources. /// - public class ResourceDictionary : AvaloniaDictionary, IResourceDictionary + public class ResourceDictionary : AvaloniaDictionary, IResourceDictionary { - private AvaloniaList _mergedDictionaries; + private IResourceHost? _owner; + private AvaloniaList? _mergedDictionaries; /// /// Initializes a new instance of the class. @@ -24,10 +24,28 @@ namespace Avalonia.Controls CollectionChanged += OnCollectionChanged; } - /// - public event EventHandler ResourcesChanged; + /// + /// Initializes a new instance of the class. + /// + public ResourceDictionary(IResourceHost owner) + : this() + { + Owner = owner; + } + + public IResourceHost? Owner + { + get => _owner; + private set + { + if (_owner != value) + { + _owner = value; + OwnerChanged?.Invoke(this, EventArgs.Empty); + } + } + } - /// public IList MergedDictionaries { get @@ -39,37 +57,51 @@ namespace Avalonia.Controls _mergedDictionaries.ForEachItem( x => { - if (x.HasResources) + if (Owner is object) { - OnResourcesChanged(); + x.AddOwner(Owner); } - - x.ResourcesChanged += MergedDictionaryResourcesChanged; }, x => { - if (x.HasResources) + if (Owner is object) { - OnResourcesChanged(); + x.RemoveOwner(Owner); } - - x.ResourcesChanged -= MergedDictionaryResourcesChanged; - }, - () => { }); + }, null); } return _mergedDictionaries; } } - /// - bool IResourceProvider.HasResources + bool IResourceNode.HasResources { - get => Count > 0 || (_mergedDictionaries?.Any(x => x.HasResources) ?? false); + get + { + if (Count > 0) + { + return true; + } + + if (_mergedDictionaries?.Count > 0) + { + foreach (var i in _mergedDictionaries) + { + if (i.HasResources) + { + return true; + } + } + } + + return false; + } } - /// - public bool TryGetResource(object key, out object value) + public event EventHandler? OwnerChanged; + + public bool TryGetResource(object key, out object? value) { if (TryGetValue(key, out value)) { @@ -90,12 +122,63 @@ namespace Avalonia.Controls return false; } - private void OnResourcesChanged() + void IResourceProvider.AddOwner(IResourceHost owner) { - ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); + owner = owner ?? throw new ArgumentNullException(nameof(owner)); + + if (Owner != null) + { + throw new InvalidOperationException("The ResourceDictionary already has a parent."); + } + + Owner = owner; + + var hasResources = Count > 0; + + if (_mergedDictionaries is object) + { + foreach (var i in _mergedDictionaries) + { + i.AddOwner(owner); + hasResources |= i.HasResources; + } + } + + if (hasResources) + { + owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } } - private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => OnResourcesChanged(); - private void MergedDictionaryResourcesChanged(object sender, ResourcesChangedEventArgs e) => OnResourcesChanged(); + void IResourceProvider.RemoveOwner(IResourceHost owner) + { + owner = owner ?? throw new ArgumentNullException(nameof(owner)); + + if (Owner == owner) + { + Owner = null; + + var hasResources = Count > 0; + + if (_mergedDictionaries is object) + { + foreach (var i in _mergedDictionaries) + { + i.RemoveOwner(owner); + hasResources |= i.HasResources; + } + } + + if (hasResources) + { + owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } + } + } + + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } } } diff --git a/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs index 19a16f86c4..250274c39b 100644 --- a/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs @@ -1,6 +1,10 @@ using System; +using Avalonia.Data.Converters; +using Avalonia.LogicalTree; using Avalonia.Reactive; +#nullable enable + namespace Avalonia.Controls { public static class ResourceNodeExtensions @@ -11,8 +15,11 @@ namespace Avalonia.Controls /// The control. /// The resource key. /// The resource, or if not found. - public static object FindResource(this IResourceNode control, object key) + public static object? FindResource(this IResourceHost control, object key) { + control = control ?? throw new ArgumentNullException(nameof(control)); + key = key ?? throw new ArgumentNullException(nameof(key)); + if (control.TryFindResource(key, out var value)) { return value; @@ -28,16 +35,16 @@ namespace Avalonia.Controls /// The resource key. /// On return, contains the resource if found, otherwise null. /// True if the resource was found; otherwise false. - public static bool TryFindResource(this IResourceNode control, object key, out object value) + public static bool TryFindResource(this IResourceHost control, object key, out object? value) { - Contract.Requires(control != null); - Contract.Requires(key != null); + control = control ?? throw new ArgumentNullException(nameof(control)); + key = key ?? throw new ArgumentNullException(nameof(key)); - var current = control; + IResourceHost? current = control; while (current != null) { - if (current is IResourceNode host) + if (current is IResourceHost host) { if (host.TryGetResource(key, out value)) { @@ -45,27 +52,46 @@ namespace Avalonia.Controls } } - current = current.ResourceParent; + current = (current as IStyledElement)?.StylingParent as IResourceHost; } value = null; return false; } - public static IObservable GetResourceObservable(this IResourceNode target, object key) + public static IObservable GetResourceObservable( + this IStyledElement control, + object key, + Func? converter = null) + { + control = control ?? throw new ArgumentNullException(nameof(control)); + key = key ?? throw new ArgumentNullException(nameof(key)); + + return new ResourceObservable(control, key, converter); + } + + public static IObservable GetResourceObservable( + this IResourceProvider resourceProvider, + object key, + Func? converter = null) { - return new ResourceObservable(target, key); + resourceProvider = resourceProvider ?? throw new ArgumentNullException(nameof(resourceProvider)); + key = key ?? throw new ArgumentNullException(nameof(key)); + + return new FloatingResourceObservable(resourceProvider, key, converter); } - private class ResourceObservable : LightweightObservableBase + private class ResourceObservable : LightweightObservableBase { - private readonly IResourceNode _target; + private readonly IStyledElement _target; private readonly object _key; + private readonly Func? _converter; - public ResourceObservable(IResourceNode target, object key) + public ResourceObservable(IStyledElement target, object key, Func? converter) { _target = target; _key = key; + _converter = converter; } protected override void Initialize() @@ -78,15 +104,81 @@ namespace Avalonia.Controls _target.ResourcesChanged -= ResourcesChanged; } - protected override void Subscribed(IObserver observer, bool first) + protected override void Subscribed(IObserver observer, bool first) + { + observer.OnNext(Convert(_target.FindResource(_key))); + } + + private void ResourcesChanged(object sender, ResourcesChangedEventArgs e) + { + PublishNext(Convert(_target.FindResource(_key))); + } + + private object? Convert(object? value) => _converter?.Invoke(value) ?? value; + } + + private class FloatingResourceObservable : LightweightObservableBase + { + private readonly IResourceProvider _target; + private readonly object _key; + private readonly Func? _converter; + private IResourceHost? _owner; + + public FloatingResourceObservable(IResourceProvider target, object key, Func? converter) + { + _target = target; + _key = key; + _converter = converter; + } + + protected override void Initialize() + { + _target.OwnerChanged += OwnerChanged; + _owner = _target.Owner; + } + + protected override void Deinitialize() + { + _target.OwnerChanged -= OwnerChanged; + _owner = null; + } + + protected override void Subscribed(IObserver observer, bool first) + { + if (_target.Owner is object) + { + observer.OnNext(Convert(_target.Owner?.FindResource(_key))); + } + } + + private void PublishNext() + { + PublishNext(Convert(_target.Owner?.FindResource(_key))); + } + + private void OwnerChanged(object sender, EventArgs e) { - observer.OnNext(_target.FindResource(_key)); + if (_owner is object) + { + _owner.ResourcesChanged -= ResourcesChanged; + } + + _owner = _target.Owner; + + if (_owner is object) + { + _owner.ResourcesChanged += ResourcesChanged; + } + + PublishNext(); } private void ResourcesChanged(object sender, ResourcesChangedEventArgs e) { - PublishNext(_target.FindResource(_key)); + PublishNext(); } + + private object? Convert(object? value) => _converter?.Invoke(value) ?? value; } } } diff --git a/src/Avalonia.Styling/Controls/ResourcesChangedEventArgs.cs b/src/Avalonia.Styling/Controls/ResourcesChangedEventArgs.cs index 68c7a58ab9..ee2ece8a26 100644 --- a/src/Avalonia.Styling/Controls/ResourcesChangedEventArgs.cs +++ b/src/Avalonia.Styling/Controls/ResourcesChangedEventArgs.cs @@ -1,11 +1,9 @@ -// 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 System; +using System; namespace Avalonia.Controls { public class ResourcesChangedEventArgs : EventArgs { + public static new readonly ResourcesChangedEventArgs Empty = new ResourcesChangedEventArgs(); } } diff --git a/src/Avalonia.Styling/IDataContextProvider.cs b/src/Avalonia.Styling/IDataContextProvider.cs new file mode 100644 index 0000000000..31639c5784 --- /dev/null +++ b/src/Avalonia.Styling/IDataContextProvider.cs @@ -0,0 +1,13 @@ +namespace Avalonia +{ + /// + /// Defines an element with a data context that can be used for binding. + /// + public interface IDataContextProvider : IAvaloniaObject + { + /// + /// Gets or sets the element's data context. + /// + object DataContext { get; set; } + } +} diff --git a/src/Avalonia.Styling/INamed.cs b/src/Avalonia.Styling/INamed.cs index 21bd233e41..1f8d269b07 100644 --- a/src/Avalonia.Styling/INamed.cs +++ b/src/Avalonia.Styling/INamed.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia { /// diff --git a/src/Avalonia.Styling/IStyledElement.cs b/src/Avalonia.Styling/IStyledElement.cs index bcf1898c4c..610c743a3e 100644 --- a/src/Avalonia.Styling/IStyledElement.cs +++ b/src/Avalonia.Styling/IStyledElement.cs @@ -9,14 +9,19 @@ namespace Avalonia IStyleable, IStyleHost, ILogical, - IResourceProvider, - IResourceNode + IResourceHost, + IDataContextProvider { /// /// Occurs when the control has finished initialization. /// event EventHandler Initialized; + /// + /// Raised when resources on the element are changed. + /// + event EventHandler ResourcesChanged; + /// /// Gets a value that indicates whether the element has finished initialization. /// @@ -27,11 +32,6 @@ namespace Avalonia /// new Classes Classes { get; set; } - /// - /// Gets or sets the control's data context. - /// - object DataContext { get; set; } - /// /// Gets the control's logical parent. /// diff --git a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs index 491f38c153..6d0302ace4 100644 --- a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs +++ b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs @@ -1,11 +1,5 @@ -// 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 System; using System.Linq; -using System.Reactive.Linq; -using System.Reflection; -using Avalonia.Controls; using Avalonia.Reactive; namespace Avalonia.LogicalTree @@ -25,7 +19,7 @@ namespace Avalonia.LogicalTree private readonly ILogical _relativeTo; private readonly int _ancestorLevel; private readonly Type _ancestorType; - ILogical _value; + private ILogical _value; public ControlTracker(ILogical relativeTo, int ancestorLevel, Type ancestorType) { @@ -69,7 +63,7 @@ namespace Avalonia.LogicalTree private void Update() { _value = _relativeTo.GetLogicalAncestors() - .Where(x => _ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) + .Where(x => _ancestorType?.IsAssignableFrom(x.GetType()) ?? true) .ElementAtOrDefault(_ancestorLevel); } } diff --git a/src/Avalonia.Styling/LogicalTree/ILogical.cs b/src/Avalonia.Styling/LogicalTree/ILogical.cs index 8ee3c9ea4f..9c4223618f 100644 --- a/src/Avalonia.Styling/LogicalTree/ILogical.cs +++ b/src/Avalonia.Styling/LogicalTree/ILogical.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Collections; using Avalonia.Controls; diff --git a/src/Avalonia.Styling/LogicalTree/ILogicalRoot.cs b/src/Avalonia.Styling/LogicalTree/ILogicalRoot.cs new file mode 100644 index 0000000000..4a61544a6f --- /dev/null +++ b/src/Avalonia.Styling/LogicalTree/ILogicalRoot.cs @@ -0,0 +1,9 @@ +namespace Avalonia.LogicalTree +{ + /// + /// Represents a root of a logical tree. + /// + public interface ILogicalRoot : ILogical + { + } +} diff --git a/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs b/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs index 276ea6c060..458ab0fce2 100644 --- a/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs +++ b/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs @@ -1,14 +1,18 @@ -// 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 System; using System.Collections.Generic; -using System.Linq; namespace Avalonia.LogicalTree { + /// + /// Provides extension methods for working with the logical tree. + /// public static class LogicalExtensions { + /// + /// Enumerates the ancestors of an in the logical tree. + /// + /// The logical. + /// The logical's ancestors. public static IEnumerable GetLogicalAncestors(this ILogical logical) { Contract.Requires(logical != null); @@ -22,6 +26,11 @@ namespace Avalonia.LogicalTree } } + /// + /// Enumerates an and its ancestors in the logical tree. + /// + /// The logical. + /// The logical and its ancestors. public static IEnumerable GetSelfAndLogicalAncestors(this ILogical logical) { yield return logical; @@ -32,11 +41,50 @@ namespace Avalonia.LogicalTree } } + /// + /// Finds first ancestor of given type. + /// + /// Ancestor type. + /// The logical. + /// If given logical should be included in search. + /// First ancestor of given type. + public static T FindLogicalAncestorOfType(this ILogical logical, bool includeSelf = false) where T : class + { + if (logical is null) + { + return null; + } + + ILogical parent = includeSelf ? logical : logical.LogicalParent; + + while (parent != null) + { + if (parent is T result) + { + return result; + } + + parent = parent.LogicalParent; + } + + return null; + } + + /// + /// Enumerates the children of an in the logical tree. + /// + /// The logical. + /// The logical children. public static IEnumerable GetLogicalChildren(this ILogical logical) { return logical.LogicalChildren; } + /// + /// Enumerates the descendants of an in the logical tree. + /// + /// The logical. + /// The logical's ancestors. public static IEnumerable GetLogicalDescendants(this ILogical logical) { foreach (ILogical child in logical.LogicalChildren) @@ -50,6 +98,11 @@ namespace Avalonia.LogicalTree } } + /// + /// Enumerates an and its descendants in the logical tree. + /// + /// The logical. + /// The logical and its ancestors. public static IEnumerable GetSelfAndLogicalDescendants(this ILogical logical) { yield return logical; @@ -60,16 +113,56 @@ namespace Avalonia.LogicalTree } } + /// + /// Finds first descendant of given type. + /// + /// Descendant type. + /// The logical. + /// If given logical should be included in search. + /// First descendant of given type. + public static T FindLogicalDescendantOfType(this ILogical logical, bool includeSelf = false) where T : class + { + if (logical is null) + { + return null; + } + + if (includeSelf && logical is T result) + { + return result; + } + + return FindDescendantOfTypeCore(logical); + } + + /// + /// Gets the logical parent of an . + /// + /// The logical. + /// The parent, or null if the logical is unparented. public static ILogical GetLogicalParent(this ILogical logical) { return logical.LogicalParent; } + /// + /// Gets the logical parent of an . + /// + /// The type of the logical parent. + /// The logical. + /// + /// The parent, or null if the logical is unparented or its parent is not of type . + /// public static T GetLogicalParent(this ILogical logical) where T : class { return logical.LogicalParent as T; } + /// + /// Enumerates the siblings of an in the logical tree. + /// + /// The logical. + /// The logical siblings. public static IEnumerable GetLogicalSiblings(this ILogical logical) { ILogical parent = logical.LogicalParent; @@ -83,9 +176,55 @@ namespace Avalonia.LogicalTree } } - public static bool IsLogicalParentOf(this ILogical logical, ILogical target) + /// + /// Tests whether an is an ancestor of another logical. + /// + /// The logical. + /// The potential descendant. + /// + /// True if is an ancestor of ; + /// otherwise false. + /// + public static bool IsLogicalAncestorOf(this ILogical logical, ILogical target) + { + ILogical current = target?.LogicalParent; + + while (current != null) + { + if (current == logical) + { + return true; + } + + current = current.LogicalParent; + } + + return false; + } + + private static T FindDescendantOfTypeCore(ILogical logical) where T : class { - return target.GetLogicalAncestors().Any(x => x == logical); + var logicalChildren = logical.LogicalChildren; + var logicalChildrenCount = logicalChildren.Count; + + for (var i = 0; i < logicalChildrenCount; i++) + { + ILogical child = logicalChildren[i]; + + if (child is T result) + { + return result; + } + + var childResult = FindDescendantOfTypeCore(child); + + if (!(childResult is null)) + { + return childResult; + } + } + + return null; } } } diff --git a/src/Avalonia.Styling/LogicalTree/LogicalTreeAttachmentEventArgs.cs b/src/Avalonia.Styling/LogicalTree/LogicalTreeAttachmentEventArgs.cs index 1b0eb2b61b..891724599c 100644 --- a/src/Avalonia.Styling/LogicalTree/LogicalTreeAttachmentEventArgs.cs +++ b/src/Avalonia.Styling/LogicalTree/LogicalTreeAttachmentEventArgs.cs @@ -1,8 +1,4 @@ -// 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 System; -using Avalonia.Styling; namespace Avalonia.LogicalTree { @@ -16,16 +12,44 @@ namespace Avalonia.LogicalTree /// Initializes a new instance of the class. /// /// The root of the logical tree. - public LogicalTreeAttachmentEventArgs(IStyleHost root) + /// The control being attached/detached. + /// The . + public LogicalTreeAttachmentEventArgs( + ILogicalRoot root, + ILogical source, + ILogical parent) { Contract.Requires(root != null); + Contract.Requires(source != null); Root = root; + Source = source; + Parent = parent; } /// /// Gets the root of the logical tree that the control is being attached to or detached from. /// - public IStyleHost Root { get; } + public ILogicalRoot Root { get; } + + /// + /// Gets the control that was attached or detached from the logical tree. + /// + /// + /// Logical tree attachment events travel down the attached logical tree from the point of + /// attachment/detachment, so this control may be different from the control that the + /// event is being raised on. + /// + public ILogical Source { get; } + + /// + /// Gets the control that is being attached to or detached from. + /// + /// + /// For logical tree attachment, holds the new logical parent of . For + /// detachment, holds the old logical parent of . If the detachment event + /// was caused by a top-level control being closed, then this property will be null. + /// + public ILogical Parent { get; } } } diff --git a/src/Avalonia.Styling/Properties/AssemblyInfo.cs b/src/Avalonia.Styling/Properties/AssemblyInfo.cs index 9f1794c700..b231f3b406 100644 --- a/src/Avalonia.Styling/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Styling/Properties/AssemblyInfo.cs @@ -1,6 +1,3 @@ -// 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 System.Reflection; using System.Runtime.CompilerServices; using Avalonia.Metadata; diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index de8093c048..05e031c9ec 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; -using System.Reactive.Linq; -using System.Reactive.Subjects; using Avalonia.Animation; using Avalonia.Collections; using Avalonia.Controls; @@ -14,6 +12,8 @@ using Avalonia.Logging; using Avalonia.LogicalTree; using Avalonia.Styling; +#nullable enable + namespace Avalonia { /// @@ -24,13 +24,13 @@ namespace Avalonia /// - Implements to form part of a logical tree. /// - A collection of class strings for custom styling. /// - public class StyledElement : Animatable, IStyledElement, ISetLogicalParent, ISetInheritanceParent + public class StyledElement : Animatable, IDataContextProvider, IStyledElement, ISetLogicalParent, ISetInheritanceParent { /// /// Defines the property. /// - public static readonly StyledProperty DataContextProperty = - AvaloniaProperty.Register( + public static readonly StyledProperty DataContextProperty = + AvaloniaProperty.Register( nameof(DataContext), inherits: true, notifying: DataContextNotifying); @@ -38,35 +38,36 @@ namespace Avalonia /// /// Defines the property. /// - public static readonly DirectProperty NameProperty = - AvaloniaProperty.RegisterDirect(nameof(Name), o => o.Name, (o, v) => o.Name = v); + public static readonly DirectProperty NameProperty = + AvaloniaProperty.RegisterDirect(nameof(Name), o => o.Name, (o, v) => o.Name = v); /// /// Defines the property. /// - public static readonly DirectProperty ParentProperty = - AvaloniaProperty.RegisterDirect(nameof(Parent), o => o.Parent); + public static readonly DirectProperty ParentProperty = + AvaloniaProperty.RegisterDirect(nameof(Parent), o => o.Parent); /// /// Defines the property. /// - public static readonly DirectProperty TemplatedParentProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty TemplatedParentProperty = + AvaloniaProperty.RegisterDirect( nameof(TemplatedParent), o => o.TemplatedParent, (o ,v) => o.TemplatedParent = v); private int _initCount; - private string _name; + private string? _name; private readonly Classes _classes = new Classes(); - private bool _isAttachedToLogicalTree; - private IAvaloniaList _logicalChildren; - private IResourceDictionary _resources; - private Styles _styles; + private ILogicalRoot? _logicalRoot; + private IAvaloniaList? _logicalChildren; + private IResourceDictionary? _resources; + private Styles? _styles; private bool _styled; - private Subject _styleDetach = new Subject(); - private ITemplatedControl _templatedParent; + private List? _appliedStyles; + private ITemplatedControl? _templatedParent; private bool _dataContextUpdating; + private bool _notifyingResourcesChanged; /// /// Initializes static members of the class. @@ -81,18 +82,18 @@ namespace Avalonia /// public StyledElement() { - _isAttachedToLogicalTree = this is IStyleRoot; + _logicalRoot = this as ILogicalRoot; } /// /// Raised when the styled element is attached to a rooted logical tree. /// - public event EventHandler AttachedToLogicalTree; + public event EventHandler? AttachedToLogicalTree; /// /// Raised when the styled element is detached from a rooted logical tree. /// - public event EventHandler DetachedFromLogicalTree; + public event EventHandler? DetachedFromLogicalTree; /// /// Occurs when the property changes. @@ -101,7 +102,7 @@ namespace Avalonia /// This event will be raised when the property has changed and /// all subscribers to that change have been notified. /// - public event EventHandler DataContextChanged; + public event EventHandler? DataContextChanged; /// /// Occurs when the styled element has finished initialization. @@ -114,12 +115,12 @@ namespace Avalonia /// is not used, it is called when the styled element is attached /// to the visual tree. /// - public event EventHandler Initialized; + public event EventHandler? Initialized; /// /// Occurs when a resource in this styled element or a parent styled element has changed. /// - public event EventHandler ResourcesChanged; + public event EventHandler? ResourcesChanged; /// /// Gets or sets the name of the styled element. @@ -128,20 +129,12 @@ namespace Avalonia /// An element's name is used to uniquely identify an element within the element's name /// scope. Once the element is added to a logical tree, its name cannot be changed. /// - public string Name + public string? Name { - get - { - return _name; - } + get => _name; set { - if (String.IsNullOrWhiteSpace(value)) - { - throw new InvalidOperationException("Cannot set Name to null or empty string."); - } - if (_styled) { throw new InvalidOperationException("Cannot set Name : styled element already styled."); @@ -189,7 +182,7 @@ namespace Avalonia /// The data context is an inherited property that specifies the default object that will /// be used for data binding. /// - public object DataContext + public object? DataContext { get { return GetValue(DataContextProperty); } set { SetValue(DataContextProperty, value); } @@ -212,65 +205,27 @@ namespace Avalonia /// each styled element may in addition define its own styles which are applied to the styled element /// itself and its children. /// - public Styles Styles - { - get { return _styles ?? (Styles = new Styles()); } - set - { - Contract.Requires(value != null); - - if (_styles != value) - { - if (_styles != null) - { - (_styles as ISetStyleParent)?.SetParent(null); - _styles.ResourcesChanged -= ThisResourcesChanged; - } - - _styles = value; - - if (value is ISetStyleParent setParent && setParent.ResourceParent == null) - { - setParent.SetParent(this); - } - - _styles.ResourcesChanged += ThisResourcesChanged; - } - } - } + public Styles Styles => _styles ??= new Styles(this); /// /// Gets or sets the styled element's resource dictionary. /// public IResourceDictionary Resources { - get => _resources ?? (Resources = new ResourceDictionary()); + get => _resources ??= new ResourceDictionary(this); set { - Contract.Requires(value != null); - - var hadResources = false; - - if (_resources != null) - { - hadResources = _resources.Count > 0; - _resources.ResourcesChanged -= ThisResourcesChanged; - } - + value = value ?? throw new ArgumentNullException(nameof(value)); + _resources?.RemoveOwner(this); _resources = value; - _resources.ResourcesChanged += ThisResourcesChanged; - - if (hadResources || _resources.Count > 0) - { - ((ILogical)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); - } + _resources.AddOwner(this); } } /// /// Gets the styled element whose lookless template this styled element is part of. /// - public ITemplatedControl TemplatedParent + public ITemplatedControl? TemplatedParent { get => _templatedParent; internal set => SetAndRaise(TemplatedParentProperty, ref _templatedParent, value); @@ -288,7 +243,7 @@ namespace Avalonia var list = new AvaloniaList { ResetBehavior = ResetBehavior.Remove, - Validate = ValidateLogicalChild + Validate = logical => ValidateLogicalChild(logical) }; list.CollectionChanged += LogicalChildrenCollectionChanged; _logicalChildren = list; @@ -307,17 +262,17 @@ namespace Avalonia /// /// Gets a value indicating whether the element is attached to a rooted logical tree. /// - bool ILogical.IsAttachedToLogicalTree => _isAttachedToLogicalTree; + bool ILogical.IsAttachedToLogicalTree => _logicalRoot != null; /// /// Gets the styled element's logical parent. /// - public IStyledElement Parent { get; private set; } + public IStyledElement? Parent { get; private set; } /// /// Gets the styled element's logical parent. /// - ILogical ILogical.LogicalParent => Parent; + ILogical? ILogical.LogicalParent => Parent; /// /// Gets the styled element's logical children. @@ -325,10 +280,8 @@ namespace Avalonia IAvaloniaReadOnlyList ILogical.LogicalChildren => LogicalChildren; /// - bool IResourceProvider.HasResources => _resources?.Count > 0 || Styles.HasResources; - - /// - IResourceNode IResourceNode.ResourceParent => ((IStyleHost)this).StylingParent as IResourceNode; + bool IResourceNode.HasResources => (_resources?.HasResources ?? false) || + (((IResourceNode?)_styles)?.HasResources ?? false); /// IAvaloniaReadOnlyList IStyleable.Classes => Classes; @@ -344,14 +297,11 @@ namespace Avalonia /// Type IStyleable.StyleKey => GetType(); - /// - IObservable IStyleable.StyleDetach => _styleDetach; - /// bool IStyleHost.IsStylesInitialized => _styles != null; /// - IStyleHost IStyleHost.StylingParent => (IStyleHost)InheritanceParent; + IStyleHost? IStyleHost.StylingParent => (IStyleHost)InheritanceParent; /// public virtual void BeginInit() @@ -367,23 +317,36 @@ namespace Avalonia throw new InvalidOperationException("BeginInit was not called."); } - if (--_initCount == 0 && _isAttachedToLogicalTree) + if (--_initCount == 0 && _logicalRoot != null) { - InitializeStylesIfNeeded(); - + ApplyStyling(); InitializeIfNeeded(); } } - private void InitializeStylesIfNeeded(bool force = false) + /// + /// Applies styling to the control if the control is initialized and styling is not + /// already applied. + /// + /// + /// A value indicating whether styling is now applied to the control. + /// + protected bool ApplyStyling() { - if (_initCount == 0 && (!_styled || force)) + if (_initCount == 0 && !_styled) { - ApplyStyling(); + AvaloniaLocator.Current.GetService()?.ApplyStyles(this); _styled = true; } + + return _styled; } + /// + /// Detaches all styles from the element and queues a restyle. + /// + protected virtual void InvalidateStyles() => DetachStyles(); + protected void InitializeIfNeeded() { if (_initCount == 0 && !IsInitialized) @@ -397,23 +360,23 @@ namespace Avalonia /// void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { - this.OnAttachedToLogicalTreeCore(e); + OnAttachedToLogicalTreeCore(e); } /// void ILogical.NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { - this.OnDetachedFromLogicalTreeCore(e); + OnDetachedFromLogicalTreeCore(e); } /// - void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) - { - ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); - } + void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e); /// - bool IResourceProvider.TryGetResource(object key, out object value) + void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e); + + /// + bool IResourceNode.TryGetResource(object key, out object? value) { value = null; return (_resources?.TryGetResource(key, out value) ?? false) || @@ -424,7 +387,7 @@ namespace Avalonia /// Sets the styled element's logical parent. /// /// The parent. - void ISetLogicalParent.SetParent(ILogical parent) + void ISetLogicalParent.SetParent(ILogical? parent) { var old = Parent; @@ -435,50 +398,46 @@ namespace Avalonia throw new InvalidOperationException("The Control already has a parent."); } - if (_isAttachedToLogicalTree) - { - var oldRoot = FindStyleRoot(old) ?? this as IStyleRoot; - - if (oldRoot == null) - { - throw new AvaloniaInternalException("Was attached to logical tree but cannot find root."); - } - - var e = new LogicalTreeAttachmentEventArgs(oldRoot); - OnDetachedFromLogicalTreeCore(e); - } - if (InheritanceParent == null || parent == null) { InheritanceParent = parent as AvaloniaObject; } - Parent = (IStyledElement)parent; + Parent = (IStyledElement?)parent; - if (old != null) + if (_logicalRoot != null) { - old.ResourcesChanged -= ThisResourcesChanged; - } - if (Parent != null) - { - Parent.ResourcesChanged += ThisResourcesChanged; + var e = new LogicalTreeAttachmentEventArgs(_logicalRoot, this, old); + OnDetachedFromLogicalTreeCore(e); } - ((ILogical)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); - if (Parent is IStyleRoot || Parent?.IsAttachedToLogicalTree == true || this is IStyleRoot) - { - var newRoot = FindStyleRoot(this); - - if (newRoot == null) - { - throw new AvaloniaInternalException("Parent is attached to logical tree but cannot find root."); - } + var newRoot = FindLogicalRoot(this); - var e = new LogicalTreeAttachmentEventArgs(newRoot); + if (newRoot is object) + { + var e = new LogicalTreeAttachmentEventArgs(newRoot, this, parent); OnAttachedToLogicalTreeCore(e); } + else if (parent is null) + { + // If we were attached to the logical tree, we piggyback on the tree traversal + // there to raise resources changed notifications. If we're being removed from + // the logical tree, then traverse the tree raising notifications now. + // + // We don't raise resources changed notifications if we're being attached to a + // non-rooted control beacuse it's unlikely that dynamic resources need to be + // correct until the control is added to the tree, and it causes a *lot* of + // notifications. + NotifyResourcesChanged(); + } - RaisePropertyChanged(ParentProperty, old, Parent, BindingPriority.LocalValue); +#nullable disable + RaisePropertyChanged( + ParentProperty, + new Optional(old), + new BindingValue(Parent), + BindingPriority.LocalValue); +#nullable enable } } @@ -491,81 +450,29 @@ namespace Avalonia InheritanceParent = parent; } - /// - /// Adds a pseudo-class to be set when a property is true. - /// - /// The property. - /// The pseudo-class. - [Obsolete("Use PseudoClass and specify the control type.")] - protected static void PseudoClass(AvaloniaProperty property, string className) + void IStyleable.StyleApplied(IStyleInstance instance) { - PseudoClass(property, className); - } + instance = instance ?? throw new ArgumentNullException(nameof(instance)); - /// - /// Adds a pseudo-class to be set when a property is true. - /// - /// The type to apply the pseudo-class to. - /// The property. - /// The pseudo-class. - protected static void PseudoClass(AvaloniaProperty property, string className) - where TOwner : class, IStyledElement - { - PseudoClass(property, x => x, className); + _appliedStyles ??= new List(); + _appliedStyles.Add(instance); } - /// - /// Adds a pseudo-class to be set when a property equals a certain value. - /// - /// The type of the property. - /// The property. - /// Returns a boolean value based on the property value. - /// The pseudo-class. - [Obsolete("Use PseudoClass and specify the control type.")] - protected static void PseudoClass( - AvaloniaProperty property, - Func selector, - string className) + void IStyleable.DetachStyles() => DetachStyles(); + + void IStyleable.DetachStyles(IReadOnlyList styles) => DetachStyles(styles); + + void IStyleable.InvalidateStyles() => InvalidateStyles(); + + void IStyleHost.StylesAdded(IReadOnlyList styles) { - PseudoClass(property, selector, className); + InvalidateStylesOnThisAndDescendents(); } - /// - /// Adds a pseudo-class to be set when a property equals a certain value. - /// - /// The type of the property. - /// The type to apply the pseudo-class to. - /// The property. - /// Returns a boolean value based on the property value. - /// The pseudo-class. - protected static void PseudoClass( - AvaloniaProperty property, - Func selector, - string className) - where TOwner : class, IStyledElement + void IStyleHost.StylesRemoved(IReadOnlyList styles) { - Contract.Requires(property != null); - Contract.Requires(selector != null); - Contract.Requires(className != null); - - if (string.IsNullOrWhiteSpace(className)) - { - throw new ArgumentException("Cannot supply an empty className."); - } - - property.Changed.Merge(property.Initialized) - .Where(e => e.Sender is TOwner) - .Subscribe(e => - { - if (selector((TProperty)e.NewValue)) - { - ((StyledElement)e.Sender).PseudoClasses.Add(className); - } - else - { - ((StyledElement)e.Sender).PseudoClasses.Remove(className); - } - }); + var allStyles = RecurseStyles(styles); + DetachStylesFromThisAndDescendents(allStyles); } protected virtual void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) @@ -590,6 +497,28 @@ namespace Avalonia } } + /// + /// Notifies child controls that a change has been made to resources that apply to them. + /// + /// The event args. + protected virtual void NotifyChildResourcesChanged(ResourcesChangedEventArgs e) + { + if (_logicalChildren is object) + { + var count = _logicalChildren.Count; + + if (count > 0) + { + e ??= ResourcesChangedEventArgs.Empty; + + for (var i = 0; i < count; ++i) + { + _logicalChildren[i].NotifyResourcesChanged(e); + } + } + } + } + /// /// Called when the styled element is added to a rooted logical tree. /// @@ -653,9 +582,12 @@ namespace Avalonia element._dataContextUpdating = true; element.OnDataContextBeginUpdate(); - foreach (var child in element.LogicalChildren) + var logicalChildren = element.LogicalChildren; + var logicalChildrenCount = logicalChildren.Count; + + for (var i = 0; i < logicalChildrenCount; i++) { - if (child is StyledElement s && + if (element.LogicalChildren[i] is StyledElement s && s.InheritanceParent == element && !s.IsSet(DataContextProperty)) { @@ -674,11 +606,11 @@ namespace Avalonia } } - private static IStyleRoot FindStyleRoot(IStyleHost e) + private static ILogicalRoot? FindLogicalRoot(IStyleHost? e) { while (e != null) { - if (e is IStyleRoot root) + if (e is ILogicalRoot root) { return root; } @@ -689,11 +621,6 @@ namespace Avalonia return null; } - private void ApplyStyling() - { - AvaloniaLocator.Current.GetService()?.ApplyStyles(this); - } - private static void ValidateLogicalChild(ILogical c) { if (c == null) @@ -704,47 +631,65 @@ namespace Avalonia private void OnAttachedToLogicalTreeCore(LogicalTreeAttachmentEventArgs e) { + if (this.GetLogicalParent() == null && !(this is ILogicalRoot)) + { + throw new InvalidOperationException( + $"AttachedToLogicalTreeCore called for '{GetType().Name}' but control has no logical parent."); + } + // This method can be called when a control is already attached to the logical tree // in the following scenario: // - ListBox gets assigned Items containing ListBoxItem // - ListBox makes ListBoxItem a logical child // - ListBox template gets applied; making its Panel get attached to logical tree // - That AttachedToLogicalTree signal travels down to the ListBoxItem - if (!_isAttachedToLogicalTree) + if (_logicalRoot == null) { - _isAttachedToLogicalTree = true; + _logicalRoot = e.Root; - InitializeStylesIfNeeded(true); + ApplyStyling(); + NotifyResourcesChanged(propagate: false); OnAttachedToLogicalTree(e); AttachedToLogicalTree?.Invoke(this, e); } - foreach (var child in LogicalChildren.OfType()) + var logicalChildren = LogicalChildren; + var logicalChildrenCount = logicalChildren.Count; + + for (var i = 0; i < logicalChildrenCount; i++) { - child.OnAttachedToLogicalTreeCore(e); + if (logicalChildren[i] is StyledElement child) + { + child.OnAttachedToLogicalTreeCore(e); + } } } private void OnDetachedFromLogicalTreeCore(LogicalTreeAttachmentEventArgs e) { - if (_isAttachedToLogicalTree) + if (_logicalRoot != null) { - _isAttachedToLogicalTree = false; - _styleDetach.OnNext(this); + _logicalRoot = null; + DetachStyles(); OnDetachedFromLogicalTree(e); DetachedFromLogicalTree?.Invoke(this, e); - foreach (var child in LogicalChildren.OfType()) + var logicalChildren = LogicalChildren; + var logicalChildrenCount = logicalChildren.Count; + + for (var i = 0; i < logicalChildrenCount; i++) { - child.OnDetachedFromLogicalTreeCore(e); + if (logicalChildren[i] is StyledElement child) + { + child.OnDetachedFromLogicalTreeCore(e); + } } #if DEBUG - if (((INotifyCollectionChangedDebug)_classes).GetCollectionChangedSubscribers()?.Length > 0) + if (((INotifyCollectionChangedDebug)Classes).GetCollectionChangedSubscribers()?.Length > 0) { - Logger.TryGet(LogEventLevel.Warning)?.Log( - LogArea.Control, + Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log( this, "{Type} detached from logical tree but still has class listeners", GetType()); @@ -769,6 +714,77 @@ namespace Avalonia } } + private void DetachStyles() + { + if (_appliedStyles is object) + { + foreach (var i in _appliedStyles) + { + i.Dispose(); + } + + _appliedStyles.Clear(); + } + + _styled = false; + } + + private void DetachStyles(IReadOnlyList styles) + { + styles = styles ?? throw new ArgumentNullException(nameof(styles)); + + if (_appliedStyles is null) + { + return; + } + + var count = styles.Count; + + for (var i = 0; i < count; ++i) + { + for (var j = _appliedStyles.Count - 1; j >= 0; --j) + { + var applied = _appliedStyles[j]; + + if (applied.Source == styles[i]) + { + applied.Dispose(); + _appliedStyles.RemoveAt(j); + } + } + } + } + + private void InvalidateStylesOnThisAndDescendents() + { + InvalidateStyles(); + + if (_logicalChildren is object) + { + var childCount = _logicalChildren.Count; + + for (var i = 0; i < childCount; ++i) + { + (_logicalChildren[i] as StyledElement)?.InvalidateStylesOnThisAndDescendents(); + } + } + } + + private void DetachStylesFromThisAndDescendents(IReadOnlyList styles) + { + DetachStyles(styles); + + if (_logicalChildren is object) + { + var childCount = _logicalChildren.Count; + + for (var i = 0; i < childCount; ++i) + { + (_logicalChildren[i] as StyledElement)?.DetachStylesFromThisAndDescendents(styles); + } + } + } + private void ClearLogicalParent(IEnumerable children) { foreach (var i in children) @@ -780,9 +796,56 @@ namespace Avalonia } } - private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e) + private void NotifyResourcesChanged( + ResourcesChangedEventArgs? e = null, + bool propagate = true) { - ((ILogical)this).NotifyResourcesChanged(e); + if (ResourcesChanged is object) + { + e ??= ResourcesChangedEventArgs.Empty; + ResourcesChanged(this, e); + } + + if (propagate) + { + e ??= ResourcesChangedEventArgs.Empty; + NotifyChildResourcesChanged(e); + } + } + + private static IReadOnlyList RecurseStyles(IReadOnlyList styles) + { + var count = styles.Count; + List? result = null; + + for (var i = 0; i < count; ++i) + { + var style = styles[i]; + + if (style.Children.Count > 0) + { + if (result is null) + { + result = new List(styles); + } + + RecurseStyles(style.Children, result); + } + } + + return result ?? styles; + } + + private static void RecurseStyles(IReadOnlyList styles, List result) + { + var count = styles.Count; + + for (var i = 0; i < count; ++i) + { + var style = styles[i]; + result.Add(style); + RecurseStyles(style.Children, result); + } } } } diff --git a/src/Avalonia.Styling/Styling/ActivatedObservable.cs b/src/Avalonia.Styling/Styling/ActivatedObservable.cs deleted file mode 100644 index 5b2774943a..0000000000 --- a/src/Avalonia.Styling/Styling/ActivatedObservable.cs +++ /dev/null @@ -1,77 +0,0 @@ -// 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 System; - -namespace Avalonia.Styling -{ - /// - /// An observable which is switched on or off according to an activator observable. - /// - /// - /// An has two inputs: an activator observable and a - /// observable which produces the activated value. When the activator - /// produces true, the will produce the current activated - /// value. When the activator produces false it will produce - /// . - /// - internal class ActivatedObservable : ActivatedValue, IDescription - { - private IDisposable _sourceSubscription; - - /// - /// Initializes a new instance of the class. - /// - /// The activator. - /// An observable that produces the activated value. - /// The binding description. - public ActivatedObservable( - IObservable activator, - IObservable source, - string description) - : base(activator, AvaloniaProperty.UnsetValue, description) - { - Contract.Requires(source != null); - - Source = source; - } - - /// - /// Gets an observable which produces the . - /// - public IObservable Source { get; } - - protected override ActivatorListener CreateListener() => new ValueListener(this); - - protected override void Deinitialize() - { - base.Deinitialize(); - _sourceSubscription.Dispose(); - _sourceSubscription = null; - } - - protected override void Initialize() - { - base.Initialize(); - _sourceSubscription = Source.Subscribe((ValueListener)Listener); - } - - protected virtual void NotifyValue(object value) - { - Value = value; - } - - private class ValueListener : ActivatorListener, IObserver - { - public ValueListener(ActivatedObservable parent) - : base(parent) - { - } - protected new ActivatedObservable Parent => (ActivatedObservable)base.Parent; - - void IObserver.OnCompleted() => Parent.CompletedReceived(); - void IObserver.OnError(Exception error) => Parent.ErrorReceived(error); - void IObserver.OnNext(object value) => Parent.NotifyValue(value); - } - } -} diff --git a/src/Avalonia.Styling/Styling/ActivatedSubject.cs b/src/Avalonia.Styling/Styling/ActivatedSubject.cs deleted file mode 100644 index a8446c4bfb..0000000000 --- a/src/Avalonia.Styling/Styling/ActivatedSubject.cs +++ /dev/null @@ -1,110 +0,0 @@ -// 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 System; -using System.Reactive.Subjects; - -namespace Avalonia.Styling -{ - /// - /// A subject which is switched on or off according to an activator observable. - /// - /// - /// An extends to - /// be an . When the object is active then values - /// received via will be passed to the source subject. - /// - internal class ActivatedSubject : ActivatedObservable, ISubject, IDescription - { - private bool _completed; - private object _pushValue; - - /// - /// Initializes a new instance of the class. - /// - /// The activator. - /// An observable that produces the activated value. - /// The binding description. - public ActivatedSubject( - IObservable activator, - ISubject source, - string description) - : base(activator, source, description) - { - } - - /// - /// Gets the underlying subject. - /// - public new ISubject Source - { - get { return (ISubject)base.Source; } - } - - public void OnCompleted() - { - Source.OnCompleted(); - } - - public void OnError(Exception error) - { - Source.OnError(error); - } - - public void OnNext(object value) - { - _pushValue = value; - - if (IsActive == true && !_completed) - { - Source.OnNext(_pushValue); - } - } - - protected override void ActiveChanged(bool active) - { - bool first = !IsActive.HasValue; - - base.ActiveChanged(active); - - if (!first) - { - Source.OnNext(active ? _pushValue : AvaloniaProperty.UnsetValue); - } - } - - protected override void CompletedReceived() - { - base.CompletedReceived(); - - if (!_completed) - { - Source.OnCompleted(); - _completed = true; - } - } - - protected override void ErrorReceived(Exception error) - { - base.ErrorReceived(error); - - if (!_completed) - { - Source.OnError(error); - _completed = true; - } - } - - private void ActivatorCompleted() - { - _completed = true; - Source.OnCompleted(); - } - - private void ActivatorError(Exception e) - { - _completed = true; - Source.OnError(e); - } - } -} diff --git a/src/Avalonia.Styling/Styling/ActivatedValue.cs b/src/Avalonia.Styling/Styling/ActivatedValue.cs deleted file mode 100644 index 908d89b751..0000000000 --- a/src/Avalonia.Styling/Styling/ActivatedValue.cs +++ /dev/null @@ -1,133 +0,0 @@ -// 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 System; -using Avalonia.Reactive; - -namespace Avalonia.Styling -{ - /// - /// An value which is switched on or off according to an activator observable. - /// - /// - /// An has two inputs: an activator observable and an - /// . When the activator produces true, the - /// will produce the current value. When the activator - /// produces false it will produce . - /// - internal class ActivatedValue : LightweightObservableBase, IDescription - { - private static readonly object NotSent = new object(); - private IDisposable _activatorSubscription; - private object _value; - private object _last = NotSent; - - /// - /// Initializes a new instance of the class. - /// - /// The activator. - /// The activated value. - /// The binding description. - public ActivatedValue( - IObservable activator, - object value, - string description) - { - Contract.Requires(activator != null); - - Activator = activator; - Value = value; - Description = description; - Listener = CreateListener(); - } - - /// - /// Gets the activator observable. - /// - public IObservable Activator { get; } - - /// - /// Gets a description of the binding. - /// - public string Description { get; } - - /// - /// Gets a value indicating whether the activator is active. - /// - public bool? IsActive { get; private set; } - - /// - /// Gets the value that will be produced when is true. - /// - public object Value - { - get => _value; - protected set - { - _value = value; - PublishValue(); - } - } - - protected ActivatorListener Listener { get; } - - protected virtual void ActiveChanged(bool active) - { - IsActive = active; - PublishValue(); - } - - protected virtual void CompletedReceived() => PublishCompleted(); - - protected virtual ActivatorListener CreateListener() => new ActivatorListener(this); - - protected override void Deinitialize() - { - _activatorSubscription.Dispose(); - _activatorSubscription = null; - } - - protected virtual void ErrorReceived(Exception error) => PublishError(error); - - protected override void Initialize() - { - _activatorSubscription = Activator.Subscribe(Listener); - } - - protected override void Subscribed(IObserver observer, bool first) - { - if (IsActive == true && !first) - { - observer.OnNext(Value); - } - } - - private void PublishValue() - { - if (IsActive.HasValue) - { - var v = IsActive.Value ? Value : AvaloniaProperty.UnsetValue; - - if (!Equals(v, _last)) - { - PublishNext(v); - _last = v; - } - } - } - - protected class ActivatorListener : IObserver - { - public ActivatorListener(ActivatedValue parent) - { - Parent = parent; - } - - protected ActivatedValue Parent { get; } - - void IObserver.OnCompleted() => Parent.CompletedReceived(); - void IObserver.OnError(Exception error) => Parent.ErrorReceived(error); - void IObserver.OnNext(bool value) => Parent.ActiveChanged(value); - } - } -} diff --git a/src/Avalonia.Styling/Styling/Activators/AndActivator.cs b/src/Avalonia.Styling/Styling/Activators/AndActivator.cs new file mode 100644 index 0000000000..0e1e3b565b --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/AndActivator.cs @@ -0,0 +1,71 @@ +#nullable enable + +using System.Collections.Generic; + +namespace Avalonia.Styling.Activators +{ + /// + /// An aggregate which is active when all of its inputs are + /// active. + /// + internal class AndActivator : StyleActivatorBase, IStyleActivatorSink + { + private List? _sources; + private ulong _flags; + private ulong _mask; + + public int Count => _sources?.Count ?? 0; + + public void Add(IStyleActivator activator) + { + _sources ??= new List(); + _sources.Add(activator); + } + + void IStyleActivatorSink.OnNext(bool value, int tag) + { + if (value) + { + _flags |= 1ul << tag; + } + else + { + _flags &= ~(1ul << tag); + } + + if (_mask != 0) + { + PublishNext(_flags == _mask); + } + } + + protected override void Initialize() + { + if (_sources is object) + { + var i = 0; + + foreach (var source in _sources) + { + source.Subscribe(this, i++); + } + + _mask = (1ul << Count) - 1; + PublishNext(_flags == _mask); + } + } + + protected override void Deinitialize() + { + if (_sources is object) + { + foreach (var source in _sources) + { + source.Unsubscribe(this); + } + } + + _mask = 0; + } + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/AndActivatorBuilder.cs b/src/Avalonia.Styling/Styling/Activators/AndActivatorBuilder.cs new file mode 100644 index 0000000000..b831c330f8 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/AndActivatorBuilder.cs @@ -0,0 +1,43 @@ +#nullable enable + +namespace Avalonia.Styling.Activators +{ + /// + /// Builds an . + /// + /// + /// When ANDing style activators, if there is more than one input then creates an instance of + /// . If there is only one input, returns the input directly. + /// + internal struct AndActivatorBuilder + { + private IStyleActivator? _single; + private AndActivator? _multiple; + + public void Add(IStyleActivator? activator) + { + if (activator == null) + { + return; + } + + if (_single is null && _multiple is null) + { + _single = activator; + } + else + { + if (_multiple is null) + { + _multiple = new AndActivator(); + _multiple.Add(_single!); + _single = null; + } + + _multiple.Add(activator); + } + } + + public IStyleActivator Get() => _single ?? _multiple!; + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs b/src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs new file mode 100644 index 0000000000..479100ed8a --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs @@ -0,0 +1,33 @@ +#nullable enable + +using System; + +namespace Avalonia.Styling.Activators +{ + /// + /// Defines a style activator. + /// + /// + /// A style activator is very similar to an `IObservable{bool}` but is optimized for the + /// particular use-case of activating a style according to a selector. It differs from + /// an observable in two major ways: + /// + /// - Can only have a single subscription + /// - The subscription can have a tag associated with it, allowing a subscriber to index + /// into a list of subscriptions without having to allocate additional objects. + /// + public interface IStyleActivator : IDisposable + { + /// + /// Subscribes to the activator. + /// + /// The listener. + /// An optional tag. + void Subscribe(IStyleActivatorSink sink, int tag = 0); + + /// + /// Unsubscribes from the activator. + /// + void Unsubscribe(IStyleActivatorSink sink); + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs b/src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs new file mode 100644 index 0000000000..a1a6ef5c28 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs @@ -0,0 +1,17 @@ +#nullable enable + +namespace Avalonia.Styling.Activators +{ + /// + /// Receives notifications from an . + /// + public interface IStyleActivatorSink + { + /// + /// Called when the subscribed activator value changes. + /// + /// The new value. + /// The subscription tag. + void OnNext(bool value, int tag); + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/NotActivator.cs b/src/Avalonia.Styling/Styling/Activators/NotActivator.cs new file mode 100644 index 0000000000..1bb6ed3cd2 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/NotActivator.cs @@ -0,0 +1,16 @@ +#nullable enable + +namespace Avalonia.Styling.Activators +{ + /// + /// An which inverts the state of an input activator. + /// + internal class NotActivator : StyleActivatorBase, IStyleActivatorSink + { + private readonly IStyleActivator _source; + public NotActivator(IStyleActivator source) => _source = source; + void IStyleActivatorSink.OnNext(bool value, int tag) => PublishNext(!value); + protected override void Initialize() => _source.Subscribe(this, 0); + protected override void Deinitialize() => _source.Unsubscribe(this); + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/OrActivator.cs b/src/Avalonia.Styling/Styling/Activators/OrActivator.cs new file mode 100644 index 0000000000..fcb7d71e60 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/OrActivator.cs @@ -0,0 +1,71 @@ +#nullable enable + +using System.Collections.Generic; + +namespace Avalonia.Styling.Activators +{ + /// + /// An aggregate which is active when any of its inputs are + /// active. + /// + internal class OrActivator : StyleActivatorBase, IStyleActivatorSink + { + private List? _sources; + private ulong _flags; + private bool _initializing; + + public int Count => _sources?.Count ?? 0; + + public void Add(IStyleActivator activator) + { + _sources ??= new List(); + _sources.Add(activator); + } + + void IStyleActivatorSink.OnNext(bool value, int tag) + { + if (value) + { + _flags |= 1ul << tag; + } + else + { + _flags &= ~(1ul << tag); + } + + if (!_initializing) + { + PublishNext(_flags != 0); + } + } + + protected override void Initialize() + { + if (_sources is object) + { + var i = 0; + + _initializing = true; + + foreach (var source in _sources) + { + source.Subscribe(this, i++); + } + + _initializing = false; + PublishNext(_flags != 0); + } + } + + protected override void Deinitialize() + { + if (_sources is object) + { + foreach (var source in _sources) + { + source.Unsubscribe(this); + } + } + } + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/OrActivatorBuilder.cs b/src/Avalonia.Styling/Styling/Activators/OrActivatorBuilder.cs new file mode 100644 index 0000000000..4b3fbcfca5 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/OrActivatorBuilder.cs @@ -0,0 +1,45 @@ +#nullable enable + +namespace Avalonia.Styling.Activators +{ + /// + /// Builds an . + /// + /// + /// When ORing style activators, if there is more than one input then creates an instance of + /// . If there is only one input, returns the input directly. + /// + internal struct OrActivatorBuilder + { + private IStyleActivator? _single; + private OrActivator? _multiple; + + public int Count => _multiple?.Count ?? (_single is object ? 1 : 0); + + public void Add(IStyleActivator? activator) + { + if (activator == null) + { + return; + } + + if (_single is null && _multiple is null) + { + _single = activator; + } + else + { + if (_multiple is null) + { + _multiple = new OrActivator(); + _multiple.Add(_single!); + _single = null; + } + + _multiple.Add(activator); + } + } + + public IStyleActivator Get() => _single ?? _multiple!; + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs b/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs new file mode 100644 index 0000000000..9e30e4fa14 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs @@ -0,0 +1,38 @@ +using System; + +#nullable enable + +namespace Avalonia.Styling.Activators +{ + /// + /// An which listens to a property value on a control. + /// + internal class PropertyEqualsActivator : StyleActivatorBase, IObserver + { + private readonly IStyleable _control; + private readonly AvaloniaProperty _property; + private readonly object? _value; + private IDisposable? _subscription; + + public PropertyEqualsActivator( + IStyleable control, + AvaloniaProperty property, + object? value) + { + _control = control ?? throw new ArgumentNullException(nameof(control)); + _property = property ?? throw new ArgumentNullException(nameof(property)); + _value = value; + } + + protected override void Initialize() + { + _subscription = _control.GetObservable(_property).Subscribe(this); + } + + protected override void Deinitialize() => _subscription?.Dispose(); + + void IObserver.OnCompleted() { } + void IObserver.OnError(Exception error) { } + void IObserver.OnNext(object value) => PublishNext(Equals(value, _value)); + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/StyleActivatorBase.cs b/src/Avalonia.Styling/Styling/Activators/StyleActivatorBase.cs new file mode 100644 index 0000000000..578098b2b0 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/StyleActivatorBase.cs @@ -0,0 +1,58 @@ +#nullable enable + +namespace Avalonia.Styling.Activators +{ + /// + /// Base class implementation of . + /// + internal abstract class StyleActivatorBase : IStyleActivator + { + private IStyleActivatorSink? _sink; + private int _tag; + private bool? _value; + + public void Subscribe(IStyleActivatorSink sink, int tag = 0) + { + if (_sink is null) + { + _sink = sink; + _tag = tag; + _value = null; + Initialize(); + } + else + { + throw new AvaloniaInternalException("Cannot subscribe to a StyleActivator more than once."); + } + } + + public void Unsubscribe(IStyleActivatorSink sink) + { + if (_sink != sink) + { + throw new AvaloniaInternalException("StyleActivatorSink is not subscribed."); + } + + _sink = null; + Deinitialize(); + } + + public void PublishNext(bool value) + { + if (_value != value) + { + _value = value; + _sink?.OnNext(value, _tag); + } + } + + public void Dispose() + { + _sink = null; + Deinitialize(); + } + + protected abstract void Initialize(); + protected abstract void Deinitialize(); + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs b/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs new file mode 100644 index 0000000000..7906a29cb5 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Collections.Specialized; +using Avalonia.Collections; + +#nullable enable + +namespace Avalonia.Styling.Activators +{ + /// + /// An which is active when a set of classes match those on a + /// control. + /// + internal sealed class StyleClassActivator : StyleActivatorBase + { + private readonly IList _match; + private readonly IAvaloniaReadOnlyList _classes; + private NotifyCollectionChangedEventHandler? _classesChangedHandler; + + public StyleClassActivator(IAvaloniaReadOnlyList classes, IList match) + { + _classes = classes; + _match = match; + } + + private NotifyCollectionChangedEventHandler ClassesChangedHandler => + _classesChangedHandler ??= ClassesChanged; + + public static bool AreClassesMatching(IReadOnlyList classes, IList toMatch) + { + int remainingMatches = toMatch.Count; + int classesCount = classes.Count; + + // Early bail out - we can't match if control does not have enough classes. + if (classesCount < remainingMatches) + { + return false; + } + + for (var i = 0; i < classesCount; i++) + { + var c = classes[i]; + + if (toMatch.Contains(c)) + { + --remainingMatches; + + // Already matched so we can skip checking other classes. + if (remainingMatches == 0) + { + break; + } + } + } + + return remainingMatches == 0; + } + + protected override void Initialize() + { + PublishNext(IsMatching()); + _classes.CollectionChanged += ClassesChangedHandler; + } + + protected override void Deinitialize() + { + _classes.CollectionChanged -= ClassesChangedHandler; + } + + private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action != NotifyCollectionChangedAction.Move) + { + PublishNext(IsMatching()); + } + } + + private bool IsMatching() => AreClassesMatching(_classes, _match); + } +} diff --git a/src/Avalonia.Styling/Styling/ChildSelector.cs b/src/Avalonia.Styling/Styling/ChildSelector.cs index 8b1d2a8553..85e7aaabde 100644 --- a/src/Avalonia.Styling/Styling/ChildSelector.cs +++ b/src/Avalonia.Styling/Styling/ChildSelector.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.LogicalTree; diff --git a/src/Avalonia.Styling/Styling/DescendentSelector.cs b/src/Avalonia.Styling/Styling/DescendentSelector.cs index a81908f23d..dde88b3436 100644 --- a/src/Avalonia.Styling/Styling/DescendentSelector.cs +++ b/src/Avalonia.Styling/Styling/DescendentSelector.cs @@ -1,25 +1,19 @@ -// 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 System; -using System.Collections.Generic; using Avalonia.LogicalTree; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { internal class DescendantSelector : Selector { private readonly Selector _parent; - private string _selectorString; + private string? _selectorString; - public DescendantSelector(Selector parent) + public DescendantSelector(Selector? parent) { - if (parent == null) - { - throw new InvalidOperationException("Descendant selector must be preceeded by a selector."); - } - - _parent = parent; + _parent = parent ?? throw new InvalidOperationException("Descendant selector must be preceeded by a selector."); } /// @@ -29,7 +23,7 @@ namespace Avalonia.Styling public override bool InTemplate => _parent.InTemplate; /// - public override Type TargetType => null; + public override Type? TargetType => null; public override string ToString() { @@ -43,8 +37,8 @@ namespace Avalonia.Styling protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) { - ILogical c = (ILogical)control; - List> descendantMatches = new List>(); + var c = (ILogical)control; + var descendantMatches = new OrActivatorBuilder(); while (c != null) { @@ -67,7 +61,7 @@ namespace Avalonia.Styling if (descendantMatches.Count > 0) { - return new SelectorMatch(StyleActivator.Or(descendantMatches)); + return new SelectorMatch(descendantMatches.Get()); } else { @@ -75,6 +69,6 @@ namespace Avalonia.Styling } } - protected override Selector MovePrevious() => null; + protected override Selector? MovePrevious() => null; } } diff --git a/src/Avalonia.Styling/Styling/IGlobalStyles.cs b/src/Avalonia.Styling/Styling/IGlobalStyles.cs index 8bdf6f869b..91a04932de 100644 --- a/src/Avalonia.Styling/Styling/IGlobalStyles.cs +++ b/src/Avalonia.Styling/Styling/IGlobalStyles.cs @@ -1,12 +1,23 @@ -// 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 System; +using System.Collections.Generic; + +#nullable enable namespace Avalonia.Styling { /// /// Defines the style host that provides styles global to the application. /// - public interface IGlobalStyles : IStyleRoot + public interface IGlobalStyles : IStyleHost { + /// + /// Raised when styles are added to or a nested styles collection. + /// + public event Action> GlobalStylesAdded; + + /// + /// Raised when styles are removed from or a nested styles collection. + /// + public event Action> GlobalStylesRemoved; } } diff --git a/src/Avalonia.Styling/Styling/ISetStyleParent.cs b/src/Avalonia.Styling/Styling/ISetStyleParent.cs deleted file mode 100644 index bca3d9d714..0000000000 --- a/src/Avalonia.Styling/Styling/ISetStyleParent.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Avalonia.Controls; - -namespace Avalonia.Styling -{ - /// - /// Defines an interface through which a 's parent can be set. - /// - /// - /// You should not usually need to use this interface - it is for internal use only. - /// - public interface ISetStyleParent : IStyle - { - /// - /// Sets the style parent. - /// - /// The parent. - void SetParent(IResourceNode parent); - - /// - /// Notifies the style that a change has been made to resources that apply to it. - /// - /// The event args. - /// - /// This method will be called automatically by the framework, you should not need to call - /// this method yourself. - /// - void NotifyResourcesChanged(ResourcesChangedEventArgs e); - } -} diff --git a/src/Avalonia.Styling/Styling/ISetter.cs b/src/Avalonia.Styling/Styling/ISetter.cs index da97638f07..d588817be8 100644 --- a/src/Avalonia.Styling/Styling/ISetter.cs +++ b/src/Avalonia.Styling/Styling/ISetter.cs @@ -1,8 +1,7 @@ -// 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 System; +#nullable enable + namespace Avalonia.Styling { /// @@ -11,11 +10,15 @@ namespace Avalonia.Styling public interface ISetter { /// - /// Applies the setter to a control. + /// Instances a setter on a control. /// - /// The style that is being applied. - /// The control. - /// An optional activator. - IDisposable Apply(IStyle style, IStyleable control, IObservable activator); + /// The control. + /// An . + /// + /// This method should return an which can be used to apply + /// the setter to the specified control. Note that it should not apply the setter value + /// until is called. + /// + ISetterInstance Instance(IStyleable target); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Styling/Styling/ISetterInstance.cs b/src/Avalonia.Styling/Styling/ISetterInstance.cs new file mode 100644 index 0000000000..a299a87b64 --- /dev/null +++ b/src/Avalonia.Styling/Styling/ISetterInstance.cs @@ -0,0 +1,40 @@ +#nullable enable + +using System; + +namespace Avalonia.Styling +{ + /// + /// Represents a setter that has been instanced on a control. + /// + public interface ISetterInstance : IDisposable + { + /// + /// Starts the setter instance. + /// + /// Whether the parent style has an activator. + /// + /// If is false then the setter should be immediately + /// applied and and should not be called. + /// If true, then bindings etc should be initiated but not produce a value until + /// called. + /// + public void Start(bool hasActivator); + + /// + /// Activates the setter. + /// + /// + /// Should only be called if hasActivator was true when was called. + /// + public void Activate(); + + /// + /// Deactivates the setter. + /// + /// + /// Should only be called if hasActivator was true when was called. + /// + public void Deactivate(); + } +} diff --git a/src/Avalonia.Styling/Styling/IStyle.cs b/src/Avalonia.Styling/Styling/IStyle.cs index da2a08f04d..78fbe0f2b5 100644 --- a/src/Avalonia.Styling/Styling/IStyle.cs +++ b/src/Avalonia.Styling/Styling/IStyle.cs @@ -1,29 +1,28 @@ -// 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 System.Collections.Generic; using Avalonia.Controls; +#nullable enable + namespace Avalonia.Styling { /// /// Defines the interface for styles. /// - public interface IStyle : IResourceNode + public interface IStyle { /// - /// Attaches the style to a control if the style's selector matches. + /// Gets a collection of child styles. + /// + IReadOnlyList Children { get; } + + /// + /// Attaches the style and any child styles to a control if the style's selector matches. /// - /// The control to attach to. - /// - /// The control that contains this style. May be null. - /// + /// The control to attach to. + /// The element that hosts the style. /// - /// True if the style can match a control of type - /// (even if it does not match this control specifically); false if the style - /// can never match. + /// A describing how the style matches the control. /// - bool Attach(IStyleable control, IStyleHost container); - - void Detach(); + SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host); } } diff --git a/src/Avalonia.Styling/Styling/IStyleHost.cs b/src/Avalonia.Styling/Styling/IStyleHost.cs index fa722aeb41..360b40d9a1 100644 --- a/src/Avalonia.Styling/Styling/IStyleHost.cs +++ b/src/Avalonia.Styling/Styling/IStyleHost.cs @@ -1,6 +1,7 @@ -// 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 System.Collections.Generic; + +#nullable enable namespace Avalonia.Styling { @@ -26,6 +27,18 @@ namespace Avalonia.Styling /// /// Gets the parent style host element. /// - IStyleHost StylingParent { get; } + IStyleHost? StylingParent { get; } + + /// + /// Called when styles are added to or a nested styles collection. + /// + /// The added styles. + void StylesAdded(IReadOnlyList styles); + + /// + /// Called when styles are removed from or a nested styles collection. + /// + /// The removed styles. + void StylesRemoved(IReadOnlyList styles); } } diff --git a/src/Avalonia.Styling/Styling/IStyleInstance.cs b/src/Avalonia.Styling/Styling/IStyleInstance.cs new file mode 100644 index 0000000000..cb094badd2 --- /dev/null +++ b/src/Avalonia.Styling/Styling/IStyleInstance.cs @@ -0,0 +1,22 @@ +using System; + +#nullable enable + +namespace Avalonia.Styling +{ + /// + /// Represents a style that has been instanced on a control. + /// + public interface IStyleInstance : IDisposable + { + /// + /// Gets the source style. + /// + IStyle Source { get; } + + /// + /// Instructs the style to start acting upon the control. + /// + void Start(); + } +} diff --git a/src/Avalonia.Styling/Styling/IStyleRoot.cs b/src/Avalonia.Styling/Styling/IStyleRoot.cs deleted file mode 100644 index 56778dcb9f..0000000000 --- a/src/Avalonia.Styling/Styling/IStyleRoot.cs +++ /dev/null @@ -1,12 +0,0 @@ -// 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. - -namespace Avalonia.Styling -{ - /// - /// Denotes the root in a tree. - /// - public interface IStyleRoot : IStyleHost - { - } -} diff --git a/src/Avalonia.Styling/Styling/IStyleable.cs b/src/Avalonia.Styling/Styling/IStyleable.cs index 5ad97d8a61..a3df779057 100644 --- a/src/Avalonia.Styling/Styling/IStyleable.cs +++ b/src/Avalonia.Styling/Styling/IStyleable.cs @@ -1,9 +1,9 @@ -// 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 System; +using System.Collections.Generic; using Avalonia.Collections; +#nullable enable + namespace Avalonia.Styling { /// @@ -11,11 +11,6 @@ namespace Avalonia.Styling /// public interface IStyleable : IAvaloniaObject, INamed { - /// - /// Signaled when the control's style should be removed. - /// - IObservable StyleDetach { get; } - /// /// Gets the list of classes for the control. /// @@ -29,6 +24,27 @@ namespace Avalonia.Styling /// /// Gets the template parent of this element if the control comes from a template. /// - ITemplatedControl TemplatedParent { get; } + ITemplatedControl? TemplatedParent { get; } + + /// + /// Notifies the element that a style has been applied. + /// + /// The style instance. + void StyleApplied(IStyleInstance instance); + + /// + /// Detaches all styles applied to the element. + /// + void DetachStyles(); + + /// + /// Detaches a collection of styles, if applied to the element. + /// + void DetachStyles(IReadOnlyList styles); + + /// + /// Detaches all styles from the element and queues a restyle. + /// + void InvalidateStyles(); } } diff --git a/src/Avalonia.Styling/Styling/IStyler.cs b/src/Avalonia.Styling/Styling/IStyler.cs index 97fe1497c8..d6477d169e 100644 --- a/src/Avalonia.Styling/Styling/IStyler.cs +++ b/src/Avalonia.Styling/Styling/IStyler.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Styling { diff --git a/src/Avalonia.Styling/Styling/ITemplate.cs b/src/Avalonia.Styling/Styling/ITemplate.cs index 1593326e37..8a130cb3b4 100644 --- a/src/Avalonia.Styling/Styling/ITemplate.cs +++ b/src/Avalonia.Styling/Styling/ITemplate.cs @@ -1,7 +1,4 @@ -// 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. - -namespace Avalonia.Styling +namespace Avalonia.Styling { public interface ITemplate { diff --git a/src/Avalonia.Styling/Styling/ITemplatedControl.cs b/src/Avalonia.Styling/Styling/ITemplatedControl.cs index 4e1a51e18a..5485babb62 100644 --- a/src/Avalonia.Styling/Styling/ITemplatedControl.cs +++ b/src/Avalonia.Styling/Styling/ITemplatedControl.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Styling { diff --git a/src/Avalonia.Styling/Styling/NotSelector.cs b/src/Avalonia.Styling/Styling/NotSelector.cs index bcf76620be..ab4e9d5d7f 100644 --- a/src/Avalonia.Styling/Styling/NotSelector.cs +++ b/src/Avalonia.Styling/Styling/NotSelector.cs @@ -1,8 +1,7 @@ -// 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 System; -using System.Reactive.Linq; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -11,16 +10,16 @@ namespace Avalonia.Styling /// internal class NotSelector : Selector { - private readonly Selector _previous; + private readonly Selector? _previous; private readonly Selector _argument; - private string _selectorString; + private string? _selectorString; /// /// Initializes a new instance of the class. /// /// The previous selector. /// The selector to be not-ed. - public NotSelector(Selector previous, Selector argument) + public NotSelector(Selector? previous, Selector argument) { _previous = previous; _argument = argument ?? throw new InvalidOperationException("Not selector must have a selector argument."); @@ -33,14 +32,14 @@ namespace Avalonia.Styling public override bool IsCombinator => false; /// - public override Type TargetType => _previous?.TargetType; + public override Type? TargetType => _previous?.TargetType; /// public override string ToString() { if (_selectorString == null) { - _selectorString = ":not(" + _argument.ToString() + ")"; + _selectorString = $"{_previous?.ToString()}:not({_argument})"; } return _selectorString; @@ -61,12 +60,12 @@ namespace Avalonia.Styling case SelectorMatchResult.NeverThisType: return SelectorMatch.AlwaysThisType; case SelectorMatchResult.Sometimes: - return new SelectorMatch(innerResult.Activator.Select(x => !x)); + return new SelectorMatch(new NotActivator(innerResult.Activator!)); default: throw new InvalidOperationException("Invalid SelectorMatchResult."); } } - protected override Selector MovePrevious() => _previous; + protected override Selector? MovePrevious() => _previous; } } diff --git a/src/Avalonia.Styling/Styling/OrSelector.cs b/src/Avalonia.Styling/Styling/OrSelector.cs index 58c5c778fb..8251915504 100644 --- a/src/Avalonia.Styling/Styling/OrSelector.cs +++ b/src/Avalonia.Styling/Styling/OrSelector.cs @@ -1,8 +1,8 @@ -// 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 System; using System.Collections.Generic; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -12,8 +12,8 @@ namespace Avalonia.Styling internal class OrSelector : Selector { private readonly IReadOnlyList _selectors; - private string _selectorString; - private Type _targetType; + private string? _selectorString; + private Type? _targetType; /// /// Initializes a new instance of the class. @@ -21,8 +21,15 @@ namespace Avalonia.Styling /// The selectors to OR. public OrSelector(IReadOnlyList selectors) { - Contract.Requires(selectors != null); - Contract.Requires(selectors.Count > 1); + if (selectors is null) + { + throw new ArgumentNullException(nameof(selectors)); + } + + if (selectors.Count <= 1) + { + throw new ArgumentException("Need more than one selector to OR."); + } _selectors = selectors; } @@ -34,7 +41,7 @@ namespace Avalonia.Styling public override bool IsCombinator => false; /// - public override Type TargetType + public override Type? TargetType { get { @@ -60,7 +67,7 @@ namespace Avalonia.Styling protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) { - var activators = new List>(); + var activators = new OrActivatorBuilder(); var neverThisInstance = false; foreach (var selector in _selectors) @@ -76,18 +83,14 @@ namespace Avalonia.Styling neverThisInstance = true; break; case SelectorMatchResult.Sometimes: - activators.Add(match.Activator); + activators.Add(match.Activator!); break; } } - if (activators.Count > 1) - { - return new SelectorMatch(StyleActivator.Or(activators)); - } - else if (activators.Count == 1) + if (activators.Count > 0) { - return new SelectorMatch(activators[0]); + return new SelectorMatch(activators.Get()); } else if (neverThisInstance) { @@ -99,11 +102,11 @@ namespace Avalonia.Styling } } - protected override Selector MovePrevious() => null; + protected override Selector? MovePrevious() => null; - private Type EvaluateTargetType() + private Type? EvaluateTargetType() { - var result = default(Type); + Type? result = null; foreach (var selector in _selectors) { diff --git a/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs index cfc0998fe0..cdd985ac80 100644 --- a/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs +++ b/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs @@ -1,9 +1,8 @@ -// 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 System; -using System.Reactive.Linq; using System.Text; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -13,14 +12,14 @@ namespace Avalonia.Styling /// internal class PropertyEqualsSelector : Selector { - private readonly Selector _previous; + private readonly Selector? _previous; private readonly AvaloniaProperty _property; - private readonly object _value; - private string _selectorString; + private readonly object? _value; + private string? _selectorString; - public PropertyEqualsSelector(Selector previous, AvaloniaProperty property, object value) + public PropertyEqualsSelector(Selector? previous, AvaloniaProperty property, object? value) { - Contract.Requires(property != null); + property = property ?? throw new ArgumentNullException(nameof(property)); _previous = previous; _property = property; @@ -33,13 +32,8 @@ namespace Avalonia.Styling /// public override bool IsCombinator => false; - /// - /// Gets the name of the control to match. - /// - public string Name { get; private set; } - /// - public override Type TargetType => _previous?.TargetType; + public override Type? TargetType => _previous?.TargetType; /// public override string ToString() @@ -77,7 +71,7 @@ namespace Avalonia.Styling { if (subscribe) { - return new SelectorMatch(control.GetObservable(_property).Select(v => Equals(v ?? string.Empty, _value))); + return new SelectorMatch(new PropertyEqualsActivator(control, _property, _value)); } else { @@ -86,6 +80,6 @@ namespace Avalonia.Styling } } - protected override Selector MovePrevious() => _previous; + protected override Selector? MovePrevious() => _previous; } } diff --git a/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs b/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs new file mode 100644 index 0000000000..e177993d13 --- /dev/null +++ b/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs @@ -0,0 +1,184 @@ +using System; +using System.Reactive.Subjects; +using Avalonia.Data; +using Avalonia.Reactive; + +#nullable enable + +namespace Avalonia.Styling +{ + /// + /// A which has been instanced on a control and has an + /// as its value. + /// + /// The target property type. + internal class PropertySetterBindingInstance : SingleSubscriberObservableBase>, + ISubject>, + ISetterInstance + { + private readonly IStyleable _target; + private readonly StyledPropertyBase? _styledProperty; + private readonly DirectPropertyBase? _directProperty; + private readonly InstancedBinding _binding; + private readonly Inner _inner; + private BindingValue _value; + private IDisposable? _subscription; + private IDisposable? _subscriptionTwoWay; + private IDisposable? _innerSubscription; + private bool _isActive; + + public PropertySetterBindingInstance( + IStyleable target, + StyledPropertyBase property, + IBinding binding) + { + _target = target; + _styledProperty = property; + _binding = binding.Initiate(_target, property); + + if (_binding.Mode == BindingMode.OneTime) + { + // For the moment, we don't support OneTime bindings in setters, because I'm not + // sure what the semantics should be in the case of activation/deactivation. + throw new NotSupportedException("OneTime bindings are not supported in setters."); + } + + _inner = new Inner(this); + } + + public PropertySetterBindingInstance( + IStyleable target, + DirectPropertyBase property, + IBinding binding) + { + _target = target; + _directProperty = property; + _binding = binding.Initiate(_target, property); + _inner = new Inner(this); + } + + public void Start(bool hasActivator) + { + _isActive = !hasActivator; + + if (_styledProperty is object) + { + if (_binding.Mode != BindingMode.OneWayToSource) + { + var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style; + _subscription = _target.Bind(_styledProperty, this, priority); + } + + if (_binding.Mode == BindingMode.TwoWay) + { + _subscriptionTwoWay = _target.GetBindingObservable(_styledProperty).Subscribe(this); + } + } + else + { + if (_binding.Mode != BindingMode.OneWayToSource) + { + _subscription = _target.Bind(_directProperty!, this); + } + + if (_binding.Mode == BindingMode.TwoWay) + { + _subscriptionTwoWay = _target.GetBindingObservable(_directProperty!).Subscribe(this); + } + } + } + + public void Activate() + { + if (!_isActive) + { + _isActive = true; + PublishNext(); + } + } + + public void Deactivate() + { + if (_isActive) + { + _isActive = false; + PublishNext(); + } + } + + public override void Dispose() + { + if (_subscription is object) + { + var sub = _subscription; + _subscription = null; + sub.Dispose(); + } + + if (_subscriptionTwoWay is object) + { + var sub = _subscriptionTwoWay; + _subscriptionTwoWay = null; + sub.Dispose(); + } + + _innerSubscription?.Dispose(); + _innerSubscription = null; + + base.Dispose(); + } + + void IObserver>.OnCompleted() + { + // This is the observable coming from the target control. It should not complete. + } + + void IObserver>.OnError(Exception error) + { + // This is the observable coming from the target control. It should not error. + } + + void IObserver>.OnNext(BindingValue value) + { + if (value.HasValue && _isActive) + { + _binding.Subject.OnNext(value.Value); + } + } + + protected override void Subscribed() + { + _innerSubscription = _binding.Observable.Subscribe(_inner); + } + + protected override void Unsubscribed() + { + _innerSubscription?.Dispose(); + _innerSubscription = null; + } + + private void PublishNext() + { + PublishNext(_isActive ? _value : default); + } + + private void ConvertAndPublishNext(object? value) + { + _value = value is T v ? v : BindingValue.FromUntyped(value); + + if (_isActive) + { + PublishNext(); + } + } + + private class Inner : IObserver + { + private readonly PropertySetterBindingInstance _owner; + public Inner(PropertySetterBindingInstance owner) => _owner = owner; + public void OnCompleted() => _owner.PublishCompleted(); + public void OnError(Exception error) => _owner.PublishError(error); + public void OnNext(object? value) => _owner.ConvertAndPublishNext(value); + } + } +} diff --git a/src/Avalonia.Styling/Styling/PropertySetterInstance.cs b/src/Avalonia.Styling/Styling/PropertySetterInstance.cs new file mode 100644 index 0000000000..689438dfd7 --- /dev/null +++ b/src/Avalonia.Styling/Styling/PropertySetterInstance.cs @@ -0,0 +1,118 @@ +using System; +using Avalonia.Data; +using Avalonia.Reactive; + +#nullable enable + +namespace Avalonia.Styling +{ + /// + /// A which has been instance on a control. + /// + /// The target property type. + internal class PropertySetterInstance : SingleSubscriberObservableBase>, + ISetterInstance + { + private readonly IStyleable _target; + private readonly StyledPropertyBase? _styledProperty; + private readonly DirectPropertyBase? _directProperty; + private readonly T _value; + private IDisposable? _subscription; + private bool _isActive; + + public PropertySetterInstance( + IStyleable target, + StyledPropertyBase property, + T value) + { + _target = target; + _styledProperty = property; + _value = value; + } + + public PropertySetterInstance( + IStyleable target, + DirectPropertyBase property, + T value) + { + _target = target; + _directProperty = property; + _value = value; + } + + public void Start(bool hasActivator) + { + if (hasActivator) + { + if (_styledProperty is object) + { + _subscription = _target.Bind(_styledProperty, this, BindingPriority.StyleTrigger); + } + else + { + _subscription = _target.Bind(_directProperty, this); + } + } + else + { + if (_styledProperty is object) + { + _subscription = _target.SetValue(_styledProperty, _value, BindingPriority.Style); + } + else + { + _target.SetValue(_directProperty!, _value); + } + } + } + + public void Activate() + { + if (!_isActive) + { + _isActive = true; + PublishNext(); + } + } + + public void Deactivate() + { + if (_isActive) + { + _isActive = false; + PublishNext(); + } + } + + public override void Dispose() + { + if (_subscription is object) + { + var sub = _subscription; + _subscription = null; + sub.Dispose(); + } + else if (_isActive) + { + if (_styledProperty is object) + { + _target.ClearValue(_styledProperty); + } + else + { + _target.ClearValue(_directProperty); + } + } + + base.Dispose(); + } + + protected override void Subscribed() => PublishNext(); + protected override void Unsubscribed() { } + + private void PublishNext() + { + PublishNext(_isActive ? new BindingValue(_value) : default); + } + } +} diff --git a/src/Avalonia.Styling/Styling/Selector.cs b/src/Avalonia.Styling/Styling/Selector.cs index af209ea970..0740e0f891 100644 --- a/src/Avalonia.Styling/Styling/Selector.cs +++ b/src/Avalonia.Styling/Styling/Selector.cs @@ -1,8 +1,7 @@ -// 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 System; -using System.Collections.Generic; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -28,7 +27,7 @@ namespace Avalonia.Styling /// /// Gets the target type of the selector, if available. /// - public abstract Type TargetType { get; } + public abstract Type? TargetType { get; } /// /// Tries to match the selector with a control. @@ -41,47 +40,28 @@ namespace Avalonia.Styling /// A . public SelectorMatch Match(IStyleable control, bool subscribe = true) { - var inputs = new List>(); - var selector = this; - var alwaysThisType = true; - var hitCombinator = false; - - while (selector != null) + // First match the selector until a combinator is found. Selectors are stored from + // right-to-left, so MatchUntilCombinator reverses this order because the type selector + // will be on the left. + var match = MatchUntilCombinator(control, this, subscribe, out var combinator); + + // If the pre-combinator selector matches, we can now match the combinator, if any. + if (match.IsMatch && combinator is object) { - hitCombinator |= selector.IsCombinator; + match = match.And(combinator.Match(control, subscribe)); - var match = selector.Evaluate(control, subscribe); - - if (!match.IsMatch) - { - return hitCombinator ? SelectorMatch.NeverThisInstance : match; - } - else if (selector.InTemplate && control.TemplatedParent == null) - { - return SelectorMatch.NeverThisInstance; - } - else if (match.Result == SelectorMatchResult.AlwaysThisInstance) - { - alwaysThisType = false; - } - else if (match.Result == SelectorMatchResult.Sometimes) + // If we have a combinator then we can never say that we always match a control of + // this type, because by definition the combinator matches on things outside of the + // control. + match = match.Result switch { - inputs.Add(match.Activator); - } - - selector = selector.MovePrevious(); + SelectorMatchResult.AlwaysThisType => SelectorMatch.AlwaysThisInstance, + SelectorMatchResult.NeverThisType => SelectorMatch.NeverThisInstance, + _ => match + }; } - if (inputs.Count > 0) - { - return new SelectorMatch(StyleActivator.And(inputs)); - } - else - { - return alwaysThisType && !hitCombinator ? - SelectorMatch.AlwaysThisType : - SelectorMatch.AlwaysThisInstance; - } + return match; } /// @@ -98,6 +78,65 @@ namespace Avalonia.Styling /// /// Moves to the previous selector. /// - protected abstract Selector MovePrevious(); + protected abstract Selector? MovePrevious(); + + private static SelectorMatch MatchUntilCombinator( + IStyleable control, + Selector start, + bool subscribe, + out Selector? combinator) + { + combinator = null; + + var activators = new AndActivatorBuilder(); + var result = Match(control, start, subscribe, ref activators, ref combinator); + + return result == SelectorMatchResult.Sometimes ? + new SelectorMatch(activators.Get()) : + new SelectorMatch(result); + } + + private static SelectorMatchResult Match( + IStyleable control, + Selector selector, + bool subscribe, + ref AndActivatorBuilder activators, + ref Selector? combinator) + { + var previous = selector.MovePrevious(); + + // Selectors are stored from right-to-left, so we recurse into the selector in order to + // reverse this order, because the type selector will be on the left and is our best + // opportunity to exit early. + if (previous != null && !previous.IsCombinator) + { + var previousMatch = Match(control, previous, subscribe, ref activators, ref combinator); + + if (previousMatch < SelectorMatchResult.Sometimes) + { + return previousMatch; + } + } + + // Match this selector. + var match = selector.Evaluate(control, subscribe); + + if (!match.IsMatch) + { + combinator = null; + return match.Result; + } + else if (match.Activator is object) + { + activators.Add(match.Activator!); + } + + if (previous?.IsCombinator == true) + { + combinator = previous; + } + + return match.Result; + } } } diff --git a/src/Avalonia.Styling/Styling/SelectorMatch.cs b/src/Avalonia.Styling/Styling/SelectorMatch.cs index 63b89e9e97..26b525347e 100644 --- a/src/Avalonia.Styling/Styling/SelectorMatch.cs +++ b/src/Avalonia.Styling/Styling/SelectorMatch.cs @@ -1,7 +1,7 @@ -// 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 System; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -21,9 +21,9 @@ namespace Avalonia.Styling NeverThisInstance, /// - /// The selector always matches this type. + /// The selector matches this instance based on the . /// - AlwaysThisType, + Sometimes, /// /// The selector always matches this instance, but doesn't always match this type. @@ -31,9 +31,9 @@ namespace Avalonia.Styling AlwaysThisInstance, /// - /// The selector matches this instance based on the . + /// The selector always matches this type. /// - Sometimes, + AlwaysThisType, } /// @@ -43,7 +43,7 @@ namespace Avalonia.Styling /// A selector match describes whether and how a matches a control, and /// in addition whether the selector can ever match a control of the same type. /// - public class SelectorMatch + public readonly struct SelectorMatch { /// /// A selector match with the result of . @@ -70,20 +70,28 @@ namespace Avalonia.Styling /// result. /// /// The match activator. - public SelectorMatch(IObservable match) + public SelectorMatch(IStyleActivator match) { - Contract.Requires(match != null); + match = match ?? throw new ArgumentNullException(nameof(match)); Result = SelectorMatchResult.Sometimes; Activator = match; } - private SelectorMatch(SelectorMatchResult result) => Result = result; + /// + /// Initializes a new instance of the class with the specified result. + /// + /// The match result. + public SelectorMatch(SelectorMatchResult result) + { + Result = result; + Activator = null; + } /// /// Gets a value indicating whether the match was positive. /// - public bool IsMatch => Result >= SelectorMatchResult.AlwaysThisType; + public bool IsMatch => Result >= SelectorMatchResult.Sometimes; /// /// Gets the result of the match. @@ -91,9 +99,34 @@ namespace Avalonia.Styling public SelectorMatchResult Result { get; } /// - /// Gets an observable which tracks the selector match, in the case of selectors that can + /// Gets an activator which tracks the selector match, in the case of selectors that can /// change over time. /// - public IObservable Activator { get; } + public IStyleActivator? Activator { get; } + + /// + /// Logical ANDs this with another. + /// + /// + /// + public SelectorMatch And(in SelectorMatch other) + { + var result = (SelectorMatchResult)Math.Min((int)Result, (int)other.Result); + + if (result == SelectorMatchResult.Sometimes) + { + var activators = new AndActivatorBuilder(); + activators.Add(Activator); + activators.Add(other.Activator); + return new SelectorMatch(activators.Get()); + } + else + { + return new SelectorMatch(result); + } + } + + /// + public override string ToString() => Result.ToString(); } } diff --git a/src/Avalonia.Styling/Styling/Selectors.cs b/src/Avalonia.Styling/Styling/Selectors.cs index 323f53b85b..762ed7b58c 100644 --- a/src/Avalonia.Styling/Styling/Selectors.cs +++ b/src/Avalonia.Styling/Styling/Selectors.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Linq; diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs index 21b6737b21..c6bdeef317 100644 --- a/src/Avalonia.Styling/Styling/Setter.cs +++ b/src/Avalonia.Styling/Styling/Setter.cs @@ -1,15 +1,11 @@ -// 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 System; -using System.Reactive.Disposables; -using System.Reflection; using Avalonia.Animation; -using Avalonia.Controls; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Metadata; -using Avalonia.Reactive; +using Avalonia.Utilities; + +#nullable enable namespace Avalonia.Styling { @@ -20,9 +16,9 @@ namespace Avalonia.Styling /// A is used to set a value on a /// depending on a condition. /// - public class Setter : ISetter, IAnimationSetter + public class Setter : ISetter, IAnimationSetter, IAvaloniaPropertyVisitor { - private object _value; + private object? _value; /// /// Initializes a new instance of the class. @@ -45,16 +41,7 @@ namespace Avalonia.Styling /// /// Gets or sets the property to set. /// - public AvaloniaProperty Property - { - get; - set; - } - - /// - /// Gets or sets the property path - /// - public PropertyPath PropertyPath { get; set; } + public AvaloniaProperty? Property { get; set; } /// /// Gets or sets the property value. @@ -62,13 +49,9 @@ namespace Avalonia.Styling [Content] [AssignBinding] [DependsOn(nameof(Property))] - public object Value + public object? Value { - get - { - return _value; - } - + get => _value; set { (value as ISetterValue)?.Initialize(this); @@ -76,98 +59,78 @@ namespace Avalonia.Styling } } - /// - /// Applies the setter to a control. - /// - /// The style that is being applied. - /// The control. - /// An optional activator. - public IDisposable Apply(IStyle style, IStyleable control, IObservable activator) + public ISetterInstance Instance(IStyleable target) { - Contract.Requires(control != null); - - var description = style?.ToString(); + target = target ?? throw new ArgumentNullException(nameof(target)); - if (Property == null) + if (Property is null) { throw new InvalidOperationException("Setter.Property must be set."); } var value = Value; - var binding = value as IBinding; - if (binding == null) + if (value is ITemplate template && + !typeof(ITemplate).IsAssignableFrom(Property.PropertyType)) { - var template = value as ITemplate; - bool isPropertyOfTypeITemplate = typeof(ITemplate).GetTypeInfo() - .IsAssignableFrom(Property.PropertyType.GetTypeInfo()); + value = template.Build(); + } - if (template != null && !isPropertyOfTypeITemplate) - { - var materialized = template.Build(); - value = materialized; - } + var data = new SetterVisitorData + { + target = target, + value = value, + }; + + Property.Accept(this, ref data); + return data.result!; + } - if (activator == null) - { - return control.Bind(Property, ObservableEx.SingleValue(value), BindingPriority.Style); - } - else - { - var activated = new ActivatedValue(activator, value, description); - return control.Bind(Property, activated, BindingPriority.StyleTrigger); - } + void IAvaloniaPropertyVisitor.Visit( + StyledPropertyBase property, + ref SetterVisitorData data) + { + if (data.value is IBinding binding) + { + data.result = new PropertySetterBindingInstance( + data.target, + property, + binding); } else { - var source = binding.Initiate(control, Property); - - if (source != null) - { - var cloned = Clone(source, source.Mode == BindingMode.Default ? Property.GetMetadata(control.GetType()).DefaultBindingMode : source.Mode, style, activator); - return BindingOperations.Apply(control, Property, cloned, null); - } + data.result = new PropertySetterInstance( + data.target, + property, + (T)data.value); } - - return Disposable.Empty; } - private InstancedBinding Clone(InstancedBinding sourceInstance, BindingMode mode, IStyle style, IObservable activator) + void IAvaloniaPropertyVisitor.Visit( + DirectPropertyBase property, + ref SetterVisitorData data) { - if (activator != null) + if (data.value is IBinding binding) { - var description = style?.ToString(); - - switch (mode) - { - case BindingMode.OneTime: - if (sourceInstance.Observable != null) - { - var activated = new ActivatedObservable(activator, sourceInstance.Observable, description); - return InstancedBinding.OneTime(activated, BindingPriority.StyleTrigger); - } - else - { - var activated = new ActivatedValue(activator, sourceInstance.Value, description); - return InstancedBinding.OneTime(activated, BindingPriority.StyleTrigger); - } - case BindingMode.OneWay: - { - var activated = new ActivatedObservable(activator, sourceInstance.Observable, description); - return InstancedBinding.OneWay(activated, BindingPriority.StyleTrigger); - } - default: - { - var activated = new ActivatedSubject(activator, sourceInstance.Subject, description); - return new InstancedBinding(activated, sourceInstance.Mode, BindingPriority.StyleTrigger); - } - } - + data.result = new PropertySetterBindingInstance( + data.target, + property, + binding); } else { - return sourceInstance.WithPriority(BindingPriority.Style); + data.result = new PropertySetterInstance( + data.target, + property, + (T)data.value); } } + + private struct SetterVisitorData + { + public IStyleable target; + public object? value; + public ISetterInstance? result; + } } } diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index 3ce82b4160..00819ef7be 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -1,30 +1,22 @@ -// 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 System; using System.Collections.Generic; -using System.Reactive.Disposables; -using System.Reactive.Linq; using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Metadata; +#nullable enable + namespace Avalonia.Styling { /// /// Defines a style. /// - public class Style : AvaloniaObject, IStyle, ISetStyleParent + public class Style : AvaloniaObject, IStyle, IResourceProvider { - private static Dictionary _applied = - new Dictionary(); - private IResourceNode _parent; - - private CompositeDisposable _subscriptions; - - private IResourceDictionary _resources; - - private IList _animations; + private IResourceHost? _owner; + private IResourceDictionary? _resources; + private List? _setters; + private List? _animations; /// /// Initializes a new instance of the class. @@ -37,13 +29,23 @@ namespace Avalonia.Styling /// Initializes a new instance of the class. /// /// The style selector. - public Style(Func selector) + public Style(Func selector) { Selector = selector(null); } - /// - public event EventHandler ResourcesChanged; + public IResourceHost? Owner + { + get => _owner; + private set + { + if (_owner != value) + { + _owner = value; + OwnerChanged?.Invoke(this, EventArgs.Empty); + } + } + } /// /// Gets or sets a dictionary of style resources. @@ -53,22 +55,20 @@ namespace Avalonia.Styling get => _resources ?? (Resources = new ResourceDictionary()); set { - Contract.Requires(value != null); - - var hadResources = false; + value = value ?? throw new ArgumentNullException(nameof(value)); - if (_resources != null) - { - hadResources = _resources.Count > 0; - _resources.ResourcesChanged -= ResourceDictionaryChanged; - } + var hadResources = _resources?.HasResources ?? false; _resources = value; - _resources.ResourcesChanged += ResourceDictionaryChanged; - if (hadResources || _resources.Count > 0) + if (Owner is object) { - ((ISetStyleParent)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); + _resources.AddOwner(Owner); + + if (hadResources || _resources.HasResources) + { + Owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } } } } @@ -76,102 +76,42 @@ namespace Avalonia.Styling /// /// Gets or sets the style's selector. /// - public Selector Selector { get; set; } + public Selector? Selector { get; set; } /// - /// Gets or sets the style's setters. + /// Gets the style's setters. /// [Content] - public IList Setters { get; set; } = new List(); - - public IList Animations - { - get - { - return _animations ?? (_animations = new List()); - } - } + public IList Setters => _setters ??= new List(); - private CompositeDisposable Subscriptions - { - get - { - return _subscriptions ?? (_subscriptions = new CompositeDisposable(2)); - } - } + /// + /// Gets the style's animations. + /// + public IList Animations => _animations ??= new List(); - /// - IResourceNode IResourceNode.ResourceParent => _parent; + bool IResourceNode.HasResources => _resources?.Count > 0; + IReadOnlyList IStyle.Children => Array.Empty(); - /// - bool IResourceProvider.HasResources => _resources?.Count > 0; + public event EventHandler? OwnerChanged; - /// - public bool Attach(IStyleable control, IStyleHost container) + public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) { - if (Selector != null) - { - var match = Selector.Match(control); + target = target ?? throw new ArgumentNullException(nameof(target)); - if (match.IsMatch) - { - var controlSubscriptions = GetSubscriptions(control); - - var subs = new CompositeDisposable(Setters.Count + Animations.Count); + var match = Selector is object ? Selector.Match(target) : + target == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; - if (control is Animatable animatable) - { - foreach (var animation in Animations) - { - var obsMatch = match.Activator; - - if (match.Result == SelectorMatchResult.AlwaysThisType || - match.Result == SelectorMatchResult.AlwaysThisInstance) - { - obsMatch = Observable.Return(true); - } - - var sub = animation.Apply(animatable, null, obsMatch); - subs.Add(sub); - } - } - - foreach (var setter in Setters) - { - var sub = setter.Apply(this, control, match.Activator); - subs.Add(sub); - } - - controlSubscriptions.Add(subs); - controlSubscriptions.Add(Disposable.Create(() => Subscriptions.Remove(subs))); - Subscriptions.Add(subs); - } - - return match.Result != SelectorMatchResult.NeverThisType; - } - else if (control == container) + if (match.IsMatch && (_setters is object || _animations is object)) { - var controlSubscriptions = GetSubscriptions(control); - - var subs = new CompositeDisposable(Setters.Count); - - foreach (var setter in Setters) - { - var sub = setter.Apply(this, control, null); - subs.Add(sub); - } - - controlSubscriptions.Add(subs); - controlSubscriptions.Add(Disposable.Create(() => Subscriptions.Remove(subs))); - Subscriptions.Add(subs); - return true; + var instance = new StyleInstance(this, target, _setters, _animations, match.Activator); + target.StyleApplied(instance); + instance.Start(); } - return false; + return match.Result; } - /// - public bool TryGetResource(object key, out object result) + public bool TryGetResource(object key, out object? result) { result = null; return _resources?.TryGetResource(key, out result) ?? false; @@ -193,63 +133,28 @@ namespace Avalonia.Styling } } - /// - void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) + void IResourceProvider.AddOwner(IResourceHost owner) { - ResourcesChanged?.Invoke(this, e); - } + owner = owner ?? throw new ArgumentNullException(nameof(owner)); - /// - void ISetStyleParent.SetParent(IResourceNode parent) - { - if (_parent != null && parent != null) + if (Owner != null) { throw new InvalidOperationException("The Style already has a parent."); } - if (parent == null) - { - Detach(); - } - - _parent = parent; + Owner = owner; + _resources?.AddOwner(owner); } - public void Detach() + void IResourceProvider.RemoveOwner(IResourceHost owner) { - _subscriptions?.Dispose(); - _subscriptions = null; - } + owner = owner ?? throw new ArgumentNullException(nameof(owner)); - private static CompositeDisposable GetSubscriptions(IStyleable control) - { - if (!_applied.TryGetValue(control, out var subscriptions)) + if (Owner == owner) { - subscriptions = new CompositeDisposable(3); - subscriptions.Add(control.StyleDetach.Subscribe(ControlDetach)); - _applied.Add(control, subscriptions); + Owner = null; + _resources?.RemoveOwner(owner); } - - return subscriptions; - } - - /// - /// Called when a control's is signaled to remove - /// all applied styles. - /// - /// The control. - private static void ControlDetach(IStyleable control) - { - var subscriptions = _applied[control]; - - subscriptions.Dispose(); - - _applied.Remove(control); - } - - private void ResourceDictionaryChanged(object sender, ResourcesChangedEventArgs e) - { - ResourcesChanged?.Invoke(this, e); } } } diff --git a/src/Avalonia.Styling/Styling/StyleActivator.cs b/src/Avalonia.Styling/Styling/StyleActivator.cs deleted file mode 100644 index 63945037d8..0000000000 --- a/src/Avalonia.Styling/Styling/StyleActivator.cs +++ /dev/null @@ -1,56 +0,0 @@ -// 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 System; -using System.Collections.Generic; -using System.Linq; -using System.Reactive; -using System.Reactive.Linq; - -namespace Avalonia.Styling -{ - public enum ActivatorMode - { - And, - Or, - } - - public static class StyleActivator - { - public static IObservable And(IList> inputs) - { - if (inputs.Count == 0) - { - throw new ArgumentException("StyleActivator.And inputs may not be empty."); - } - else if (inputs.Count == 1) - { - return inputs[0]; - } - else - { - return inputs.CombineLatest() - .Select(values => values.All(x => x)) - .DistinctUntilChanged(); - } - } - - public static IObservable Or(IList> inputs) - { - if (inputs.Count == 0) - { - throw new ArgumentException("StyleActivator.Or inputs may not be empty."); - } - else if (inputs.Count == 1) - { - return inputs[0]; - } - else - { - return inputs.CombineLatest() - .Select(values => values.Any(x => x)) - .DistinctUntilChanged(); - } - } - } -} diff --git a/src/Avalonia.Styling/Styling/StyleInstance.cs b/src/Avalonia.Styling/Styling/StyleInstance.cs new file mode 100644 index 0000000000..8ca31d654f --- /dev/null +++ b/src/Avalonia.Styling/Styling/StyleInstance.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Subjects; +using Avalonia.Animation; +using Avalonia.Styling.Activators; + +#nullable enable + +namespace Avalonia.Styling +{ + /// + /// A which has been instanced on a control. + /// + internal class StyleInstance : IStyleInstance, IStyleActivatorSink + { + private readonly List? _setters; + private readonly List? _animations; + private readonly IStyleActivator? _activator; + private readonly Subject? _animationTrigger; + private bool _active; + + public StyleInstance( + IStyle source, + IStyleable target, + IReadOnlyList? setters, + IReadOnlyList? animations, + IStyleActivator? activator = null) + { + Source = source ?? throw new ArgumentNullException(nameof(source)); + Target = target ?? throw new ArgumentNullException(nameof(target)); + _activator = activator; + + if (setters is object) + { + var setterCount = setters.Count; + + _setters = new List(setterCount); + + for (var i = 0; i < setterCount; ++i) + { + _setters.Add(setters[i].Instance(Target)); + } + } + + if (animations is object && target is Animatable animatable) + { + var animationsCount = animations.Count; + + _animations = new List(animationsCount); + _animationTrigger = new Subject(); + + for (var i = 0; i < animationsCount; ++i) + { + _animations.Add(animations[i].Apply(animatable, null, _animationTrigger)); + } + } + } + + public IStyle Source { get; } + public IStyleable Target { get; } + + public void Start() + { + var hasActivator = _activator is object; + + if (_setters is object) + { + foreach (var setter in _setters) + { + setter.Start(hasActivator); + } + } + + if (hasActivator) + { + _activator!.Subscribe(this, 0); + } + else if (_animationTrigger != null) + { + _animationTrigger.OnNext(true); + } + } + + public void Dispose() + { + if (_setters is object) + { + foreach (var setter in _setters) + { + setter.Dispose(); + } + } + + if (_animations is object) + { + foreach (var subscripion in _animations) + { + subscripion.Dispose(); + } + } + + _activator?.Dispose(); + } + + private void ActivatorChanged(bool value) + { + if (_active != value) + { + _active = value; + + _animationTrigger?.OnNext(value); + + if (_setters is object) + { + if (_active) + { + foreach (var setter in _setters) + { + setter.Activate(); + } + } + else + { + foreach (var setter in _setters) + { + setter.Deactivate(); + } + } + } + } + } + + void IStyleActivatorSink.OnNext(bool value, int tag) => ActivatorChanged(value); + } +} diff --git a/src/Avalonia.Styling/Styling/Styler.cs b/src/Avalonia.Styling/Styling/Styler.cs index 7ac5c89005..74cf77ea40 100644 --- a/src/Avalonia.Styling/Styling/Styler.cs +++ b/src/Avalonia.Styling/Styling/Styler.cs @@ -1,37 +1,33 @@ -// 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 System; +#nullable enable + namespace Avalonia.Styling { public class Styler : IStyler { - public void ApplyStyles(IStyleable control) + public void ApplyStyles(IStyleable target) { - var styleHost = control as IStyleHost; + target = target ?? throw new ArgumentNullException(nameof(target)); - if (styleHost != null) + if (target is IStyleHost styleHost) { - ApplyStyles(control, styleHost); + ApplyStyles(target, styleHost); } } - private void ApplyStyles(IStyleable control, IStyleHost styleHost) + private void ApplyStyles(IStyleable target, IStyleHost host) { - Contract.Requires(control != null); - Contract.Requires(styleHost != null); - - var parentContainer = styleHost.StylingParent; + var parent = host.StylingParent; - if (parentContainer != null) + if (parent != null) { - ApplyStyles(control, parentContainer); + ApplyStyles(target, parent); } - if (styleHost.IsStylesInitialized) + if (host.IsStylesInitialized) { - styleHost.Styles.Attach(control, styleHost); + host.Styles.TryAttach(target, host); } } } diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index a4563110a9..7c79060930 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -1,79 +1,57 @@ -// 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 System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using Avalonia.Collections; using Avalonia.Controls; +#nullable enable + namespace Avalonia.Styling { /// /// A style that consists of a number of child styles. /// - public class Styles : AvaloniaObject, IAvaloniaList, IStyle, ISetStyleParent + public class Styles : AvaloniaObject, + IAvaloniaList, + IStyle, + IResourceProvider { - private IResourceNode _parent; - private IResourceDictionary _resources; - private AvaloniaList _styles = new AvaloniaList(); - private Dictionary> _cache; + private readonly AvaloniaList _styles = new AvaloniaList(); + private IResourceHost? _owner; + private IResourceDictionary? _resources; + private Dictionary?>? _cache; + private bool _notifyingResourcesChanged; public Styles() { _styles.ResetBehavior = ResetBehavior.Remove; - _styles.ForEachItem( - x => - { - if (x.ResourceParent == null && x is ISetStyleParent setParent) - { - setParent.SetParent(this); - setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs()); - } - - if (x.HasResources) - { - ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); - } - - x.ResourcesChanged += SubResourceChanged; - _cache = null; - }, - x => - { - if (x.ResourceParent == this && x is ISetStyleParent setParent) - { - setParent.SetParent(null); - setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs()); - } - - if (x.HasResources) - { - ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); - } - - x.ResourcesChanged -= SubResourceChanged; - _cache = null; - }, - () => { }); + _styles.CollectionChanged += OnCollectionChanged; } - public event NotifyCollectionChangedEventHandler CollectionChanged + public Styles(IResourceHost owner) + : this() { - add => _styles.CollectionChanged += value; - remove => _styles.CollectionChanged -= value; + Owner = owner; } - /// - public event EventHandler ResourcesChanged; + public event NotifyCollectionChangedEventHandler? CollectionChanged; + public event EventHandler? OwnerChanged; - /// public int Count => _styles.Count; - /// - public bool HasResources => _resources?.Count > 0 || this.Any(x => x.HasResources); + public IResourceHost? Owner + { + get => _owner; + private set + { + if (_owner != value) + { + _owner = value; + OwnerChanged?.Invoke(this, EventArgs.Empty); + } + } + } /// /// Gets or sets a dictionary of style resources. @@ -83,102 +61,98 @@ namespace Avalonia.Styling get => _resources ?? (Resources = new ResourceDictionary()); set { - Contract.Requires(value != null); + value = value ?? throw new ArgumentNullException(nameof(Resources)); - var hadResources = false; - - if (_resources != null) + if (Owner is object) { - hadResources = _resources.Count > 0; - _resources.ResourcesChanged -= ResourceDictionaryChanged; + _resources?.RemoveOwner(Owner); } _resources = value; - _resources.ResourcesChanged += ResourceDictionaryChanged; - if (hadResources || _resources.Count > 0) + if (Owner is object) { - ((ISetStyleParent)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); + _resources.AddOwner(Owner); } } } - /// - IResourceNode IResourceNode.ResourceParent => _parent; - - /// bool ICollection.IsReadOnly => false; - /// + bool IResourceNode.HasResources + { + get + { + if (_resources?.Count > 0) + { + return true; + } + + foreach (var i in this) + { + if (i is IResourceProvider p && p.HasResources) + { + return true; + } + } + + return false; + } + } + IStyle IReadOnlyList.this[int index] => _styles[index]; - /// + IReadOnlyList IStyle.Children => this; + public IStyle this[int index] { get => _styles[index]; set => _styles[index] = value; } - /// - /// Attaches the style to a control if the style's selector matches. - /// - /// The control to attach to. - /// - /// The control that contains this style. May be null. - /// - public bool Attach(IStyleable control, IStyleHost container) + public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) { - if (_cache == null) - { - _cache = new Dictionary>(); - } + _cache ??= new Dictionary?>(); - if (_cache.TryGetValue(control.StyleKey, out var cached)) + if (_cache.TryGetValue(target.StyleKey, out var cached)) { - if (cached != null) + if (cached is object) { foreach (var style in cached) { - style.Attach(control, container); + style.TryAttach(target, host); } - return true; + return SelectorMatchResult.AlwaysThisType; + } + else + { + return SelectorMatchResult.NeverThisType; } - - return false; } else { - List result = null; + List? matches = null; - foreach (var style in this) + foreach (var child in this) { - if (style.Attach(control, container)) + if (child.TryAttach(target, host) != SelectorMatchResult.NeverThisType) { - if (result == null) - { - result = new List(); - } - - result.Add(style); + matches ??= new List(); + matches.Add(child); } } - _cache.Add(control.StyleKey, result); - return result != null; - } - } - - public void Detach() - { - foreach (IStyle style in this) - { - style.Detach(); + _cache.Add(target.StyleKey, matches); + + return matches is null ? + SelectorMatchResult.NeverThisType : + SelectorMatchResult.AlwaysThisType; } } /// - public bool TryGetResource(object key, out object value) + public bool TryGetResource(object key, out object? value) { if (_resources != null && _resources.TryGetResource(key, out value)) { @@ -187,7 +161,7 @@ namespace Avalonia.Styling for (var i = Count - 1; i >= 0; --i) { - if (this[i].TryGetResource(key, out value)) + if (this[i] is IResourceProvider p && p.TryGetResource(key, out value)) { return true; } @@ -239,54 +213,123 @@ namespace Avalonia.Styling /// public bool Remove(IStyle item) => _styles.Remove(item); + public AvaloniaList.Enumerator GetEnumerator() => _styles.GetEnumerator(); + /// - public IEnumerator GetEnumerator() => _styles.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator(); /// IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator(); /// - void ISetStyleParent.SetParent(IResourceNode parent) + void IResourceProvider.AddOwner(IResourceHost owner) { - if (_parent != null && parent != null) + owner = owner ?? throw new ArgumentNullException(nameof(owner)); + + if (Owner != null) { - throw new InvalidOperationException("The Style already has a parent."); + throw new InvalidOperationException("The Styles already has a owner."); } - _parent = parent; + Owner = owner; + _resources?.AddOwner(owner); + + foreach (var child in this) + { + if (child is IResourceProvider r) + { + r.AddOwner(owner); + } + } } /// - void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) + void IResourceProvider.RemoveOwner(IResourceHost owner) { - ResourcesChanged?.Invoke(this, e); + owner = owner ?? throw new ArgumentNullException(nameof(owner)); + + if (Owner == owner) + { + Owner = null; + _resources?.RemoveOwner(owner); + + foreach (var child in this) + { + if (child is IResourceProvider r) + { + r.RemoveOwner(owner); + } + } + } } - private void ResourceDictionaryChanged(object sender, ResourcesChangedEventArgs e) + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { - foreach (var child in this) + static IReadOnlyList ToReadOnlyList(IList list) { - (child as ISetStyleParent)?.NotifyResourcesChanged(e); + if (list is IReadOnlyList) + { + return (IReadOnlyList)list; + } + else + { + var result = new T[list.Count]; + list.CopyTo(result, 0); + return result; + } } - ResourcesChanged?.Invoke(this, e); - } + void Add(IList items) + { + for (var i = 0; i < items.Count; ++i) + { + var style = (IStyle)items[i]; - private void SubResourceChanged(object sender, ResourcesChangedEventArgs e) - { - var foundSource = false; + if (Owner is object && style is IResourceProvider resourceProvider) + { + resourceProvider.AddOwner(Owner); + } - foreach (var child in this) + _cache = null; + } + + (Owner as IStyleHost)?.StylesAdded(ToReadOnlyList(items)); + } + + void Remove(IList items) { - if (foundSource) + for (var i = 0; i < items.Count; ++i) { - (child as ISetStyleParent)?.NotifyResourcesChanged(e); + var style = (IStyle)items[i]; + + if (Owner is object && style is IResourceProvider resourceProvider) + { + resourceProvider.RemoveOwner(Owner); + } + + _cache = null; } - foundSource |= child == sender; + (Owner as IStyleHost)?.StylesRemoved(ToReadOnlyList(items)); + } + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + Add(e.NewItems); + break; + case NotifyCollectionChangedAction.Remove: + Remove(e.OldItems); + break; + case NotifyCollectionChangedAction.Replace: + Remove(e.OldItems); + Add(e.NewItems); + break; + case NotifyCollectionChangedAction.Reset: + throw new InvalidOperationException("Reset should not be called on Styles."); } - ResourcesChanged?.Invoke(this, e); + CollectionChanged?.Invoke(this, e); } } } diff --git a/src/Avalonia.Styling/Styling/TemplateSelector.cs b/src/Avalonia.Styling/Styling/TemplateSelector.cs index 7530339883..5ea8defeda 100644 --- a/src/Avalonia.Styling/Styling/TemplateSelector.cs +++ b/src/Avalonia.Styling/Styling/TemplateSelector.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Styling @@ -41,12 +38,11 @@ namespace Avalonia.Styling protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) { - IStyleable templatedParent = control.TemplatedParent as IStyleable; + var templatedParent = control.TemplatedParent as IStyleable; if (templatedParent == null) { - throw new InvalidOperationException( - "Cannot call Template selector on control with null TemplatedParent."); + return SelectorMatch.NeverThisInstance; } return _parent.Match(templatedParent, subscribe); diff --git a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs index f1fd2f6c7f..ef48c4a8cd 100644 --- a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs @@ -1,13 +1,9 @@ -// 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 System; using System.Collections.Generic; -using System.Collections.Specialized; -using System.Reflection; using System.Text; -using Avalonia.Collections; -using Avalonia.Reactive; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -17,13 +13,12 @@ namespace Avalonia.Styling /// internal class TypeNameAndClassSelector : Selector { - private readonly Selector _previous; + private readonly Selector? _previous; private readonly Lazy> _classes = new Lazy>(() => new List()); - private Type _targetType; - - private string _selectorString; + private Type? _targetType; + private string? _selectorString; - public static TypeNameAndClassSelector OfType(Selector previous, Type targetType) + public static TypeNameAndClassSelector OfType(Selector? previous, Type targetType) { var result = new TypeNameAndClassSelector(previous); result._targetType = targetType; @@ -32,7 +27,7 @@ namespace Avalonia.Styling return result; } - public static TypeNameAndClassSelector Is(Selector previous, Type targetType) + public static TypeNameAndClassSelector Is(Selector? previous, Type targetType) { var result = new TypeNameAndClassSelector(previous); result._targetType = targetType; @@ -41,7 +36,7 @@ namespace Avalonia.Styling return result; } - public static TypeNameAndClassSelector ForName(Selector previous, string name) + public static TypeNameAndClassSelector ForName(Selector? previous, string name) { var result = new TypeNameAndClassSelector(previous); result.Name = name; @@ -49,7 +44,7 @@ namespace Avalonia.Styling return result; } - public static TypeNameAndClassSelector ForClass(Selector previous, string className) + public static TypeNameAndClassSelector ForClass(Selector? previous, string className) { var result = new TypeNameAndClassSelector(previous); result.Classes.Add(className); @@ -57,7 +52,7 @@ namespace Avalonia.Styling return result; } - protected TypeNameAndClassSelector(Selector previous) + protected TypeNameAndClassSelector(Selector? previous) { _previous = previous; } @@ -68,10 +63,10 @@ namespace Avalonia.Styling /// /// Gets the name of the control to match. /// - public string Name { get; set; } + public string? Name { get; set; } /// - public override Type TargetType => _targetType ?? _previous?.TargetType; + public override Type? TargetType => _targetType ?? _previous?.TargetType; /// public override bool IsCombinator => false; @@ -114,7 +109,7 @@ namespace Avalonia.Styling } else { - if (!TargetType.GetTypeInfo().IsAssignableFrom(controlType.GetTypeInfo())) + if (!TargetType.IsAssignableFrom(controlType)) { return SelectorMatch.NeverThisType; } @@ -130,12 +125,12 @@ namespace Avalonia.Styling { if (subscribe) { - var observable = new ClassObserver(control.Classes, _classes.Value); + var observable = new StyleClassActivator(control.Classes, _classes.Value); return new SelectorMatch(observable); } - if (!AreClassesMatching(control.Classes, Classes)) + if (!StyleClassActivator.AreClassesMatching(control.Classes, Classes)) { return SelectorMatch.NeverThisInstance; } @@ -144,7 +139,7 @@ namespace Avalonia.Styling return Name == null ? SelectorMatch.AlwaysThisType : SelectorMatch.AlwaysThisInstance; } - protected override Selector MovePrevious() => _previous; + protected override Selector? MovePrevious() => _previous; private string BuildSelectorString() { @@ -190,80 +185,5 @@ namespace Avalonia.Styling return builder.ToString(); } - - private static bool AreClassesMatching(IReadOnlyList classes, IList toMatch) - { - int remainingMatches = toMatch.Count; - int classesCount = classes.Count; - - // Early bail out - we can't match if control does not have enough classes. - if (classesCount < remainingMatches) - { - return false; - } - - for (var i = 0; i < classesCount; i++) - { - var c = classes[i]; - - if (toMatch.Contains(c)) - { - --remainingMatches; - - // Already matched so we can skip checking other classes. - if (remainingMatches == 0) - { - break; - } - } - } - - return remainingMatches == 0; - } - - private sealed class ClassObserver : LightweightObservableBase - { - private readonly IList _match; - private readonly IAvaloniaReadOnlyList _classes; - private bool _hasMatch; - - public ClassObserver(IAvaloniaReadOnlyList classes, IList match) - { - _classes = classes; - _match = match; - } - - protected override void Deinitialize() => _classes.CollectionChanged -= ClassesChanged; - - protected override void Initialize() - { - _hasMatch = IsMatching(); - _classes.CollectionChanged += ClassesChanged; - } - - protected override void Subscribed(IObserver observer, bool first) - { - observer.OnNext(_hasMatch); - } - - private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if (e.Action != NotifyCollectionChangedAction.Move) - { - var hasMatch = IsMatching(); - - if (hasMatch != _hasMatch) - { - PublishNext(hasMatch); - _hasMatch = hasMatch; - } - } - } - - private bool IsMatching() - { - return AreClassesMatching(_classes, _match); - } - } } } diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml index 0ed17fae76..ffe3e92202 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml @@ -14,7 +14,9 @@ #FFA0A0A0 #FF282828 #FF505050 + #FF686868 #FF808080 + #FFEFEBEF #FFA8A8A8 #FF828282 #FF505050 @@ -32,7 +34,9 @@ + + @@ -61,6 +65,7 @@ 12 16 - 10 + 18 + 8 diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index 3a8a8ec446..c0e5f47eed 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -12,9 +12,14 @@ #FFAAAAAA #FF888888 #FF333333 - #FFFFFFFF - #FFAAAAAA - #FF888888 + + + #FF868999 + #FFF5F5F5 + #FFC2C3C9 + #FF686868 + #FF5B5B5B + #FFF0F0F0 #FFD0D0D0 #FF808080 @@ -32,7 +37,9 @@ + + @@ -61,6 +68,7 @@ 12 16 - 10 + 18 + 8 diff --git a/src/Avalonia.Themes.Default/DatePicker.xaml b/src/Avalonia.Themes.Default/CalendarDatePicker.xaml similarity index 97% rename from src/Avalonia.Themes.Default/DatePicker.xaml rename to src/Avalonia.Themes.Default/CalendarDatePicker.xaml index 7adb1c2d5f..bc1aba1a03 100644 --- a/src/Avalonia.Themes.Default/DatePicker.xaml +++ b/src/Avalonia.Themes.Default/CalendarDatePicker.xaml @@ -8,7 +8,7 @@ - - diff --git a/src/Avalonia.Themes.Default/ComboBox.xaml b/src/Avalonia.Themes.Default/ComboBox.xaml index 675234c16a..95bd9550a5 100644 --- a/src/Avalonia.Themes.Default/ComboBox.xaml +++ b/src/Avalonia.Themes.Default/ComboBox.xaml @@ -4,6 +4,7 @@ + - - + - + @@ -58,4 +58,7 @@ + diff --git a/src/Avalonia.Themes.Default/ContextMenu.xaml b/src/Avalonia.Themes.Default/ContextMenu.xaml index 53d7c5abb4..9b84253c8a 100644 --- a/src/Avalonia.Themes.Default/ContextMenu.xaml +++ b/src/Avalonia.Themes.Default/ContextMenu.xaml @@ -10,20 +10,12 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}"> - - + - - diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 67279fca99..94d26e798b 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -45,11 +45,12 @@ - + + diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml.cs b/src/Avalonia.Themes.Default/DefaultTheme.xaml.cs index 86647439ce..effdb88537 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml.cs +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml.cs @@ -1,6 +1,3 @@ -// 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.Markup.Xaml; using Avalonia.Styling; diff --git a/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml index 1fd168c009..9ffe51fae8 100644 --- a/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml @@ -3,14 +3,17 @@ - - - - - + + + + + + + + diff --git a/src/Avalonia.Themes.Default/GridSplitter.xaml b/src/Avalonia.Themes.Default/GridSplitter.xaml index 64349222ea..dc5cd002dc 100644 --- a/src/Avalonia.Themes.Default/GridSplitter.xaml +++ b/src/Avalonia.Themes.Default/GridSplitter.xaml @@ -1,51 +1,23 @@ - - - - - - - - + + - - - - - - - + - + diff --git a/src/Avalonia.Themes.Default/ItemsControl.xaml b/src/Avalonia.Themes.Default/ItemsControl.xaml index f3def542fc..8bb0fc297c 100644 --- a/src/Avalonia.Themes.Default/ItemsControl.xaml +++ b/src/Avalonia.Themes.Default/ItemsControl.xaml @@ -1,10 +1,15 @@ diff --git a/src/Avalonia.Themes.Default/ListBox.xaml b/src/Avalonia.Themes.Default/ListBox.xaml index 59c596bcaa..e91d8a6772 100644 --- a/src/Avalonia.Themes.Default/ListBox.xaml +++ b/src/Avalonia.Themes.Default/ListBox.xaml @@ -1,4 +1,5 @@ - + + + diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index a794d15577..d7f367c591 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -1,17 +1,28 @@ - + + + + + diff --git a/src/Avalonia.Themes.Default/PopupRoot.xaml b/src/Avalonia.Themes.Default/PopupRoot.xaml index cf063f5390..9af4f5a910 100644 --- a/src/Avalonia.Themes.Default/PopupRoot.xaml +++ b/src/Avalonia.Themes.Default/PopupRoot.xaml @@ -2,13 +2,16 @@ - - - + + + + + + diff --git a/src/Avalonia.Themes.Default/ProgressBar.xaml b/src/Avalonia.Themes.Default/ProgressBar.xaml index 72271e785a..d3c2f0c784 100644 --- a/src/Avalonia.Themes.Default/ProgressBar.xaml +++ b/src/Avalonia.Themes.Default/ProgressBar.xaml @@ -1,14 +1,25 @@ @@ -22,42 +33,49 @@ + diff --git a/src/Avalonia.Themes.Default/RadioButton.xaml b/src/Avalonia.Themes.Default/RadioButton.xaml index e7cbed8ec5..4cdb116cdd 100644 --- a/src/Avalonia.Themes.Default/RadioButton.xaml +++ b/src/Avalonia.Themes.Default/RadioButton.xaml @@ -13,7 +13,6 @@ Height="18" VerticalAlignment="Center"/> + - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/RepeatButton.xaml b/src/Avalonia.Themes.Default/RepeatButton.xaml index f555209471..702e4e6ebd 100644 --- a/src/Avalonia.Themes.Default/RepeatButton.xaml +++ b/src/Avalonia.Themes.Default/RepeatButton.xaml @@ -33,12 +33,8 @@ - - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/ScrollBar.xaml b/src/Avalonia.Themes.Default/ScrollBar.xaml index c9552a607c..0f8fa4986d 100644 --- a/src/Avalonia.Themes.Default/ScrollBar.xaml +++ b/src/Avalonia.Themes.Default/ScrollBar.xaml @@ -1,128 +1,142 @@ - - - - - - - - + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/ScrollViewer.xaml b/src/Avalonia.Themes.Default/ScrollViewer.xaml index 3e130cad67..2a1aead8ed 100644 --- a/src/Avalonia.Themes.Default/ScrollViewer.xaml +++ b/src/Avalonia.Themes.Default/ScrollViewer.xaml @@ -1,42 +1,105 @@ - + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/Slider.xaml b/src/Avalonia.Themes.Default/Slider.xaml index eb46fdffa2..1d48a946fc 100644 --- a/src/Avalonia.Themes.Default/Slider.xaml +++ b/src/Avalonia.Themes.Default/Slider.xaml @@ -4,7 +4,7 @@ - + @@ -20,7 +20,7 @@ - + @@ -46,7 +46,7 @@ - + @@ -55,7 +55,7 @@ - + @@ -87,4 +87,7 @@ + diff --git a/src/Avalonia.Themes.Default/TabControl.xaml b/src/Avalonia.Themes.Default/TabControl.xaml index dc3597e9d9..ed2e67df28 100644 --- a/src/Avalonia.Themes.Default/TabControl.xaml +++ b/src/Avalonia.Themes.Default/TabControl.xaml @@ -57,6 +57,6 @@ diff --git a/src/Avalonia.Themes.Default/TextBox.xaml b/src/Avalonia.Themes.Default/TextBox.xaml index ec1f6e9e7d..4fb3653e89 100644 --- a/src/Avalonia.Themes.Default/TextBox.xaml +++ b/src/Avalonia.Themes.Default/TextBox.xaml @@ -12,7 +12,9 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> - + - + diff --git a/src/Avalonia.Themes.Default/ToggleSwitch.xaml b/src/Avalonia.Themes.Default/ToggleSwitch.xaml new file mode 100644 index 0000000000..88266ac979 --- /dev/null +++ b/src/Avalonia.Themes.Default/ToggleSwitch.xaml @@ -0,0 +1,294 @@ + + + 0,0,0,6 + 6 + 6 + 154 + 20 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/TreeViewItem.xaml b/src/Avalonia.Themes.Default/TreeViewItem.xaml index 0d826806d0..a3f8d8b7f7 100644 --- a/src/Avalonia.Themes.Default/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Default/TreeViewItem.xaml @@ -17,8 +17,7 @@ BorderThickness="{TemplateBinding BorderThickness}" TemplatedControl.IsTemplateFocusTarget="True"> - - - - - + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/Base.xaml b/src/Avalonia.Themes.Fluent/Accents/Base.xaml new file mode 100644 index 0000000000..6dc59bed21 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/Base.xaml @@ -0,0 +1,24 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml new file mode 100644 index 0000000000..68b33d104a --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml @@ -0,0 +1,360 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml new file mode 100644 index 0000000000..d8a641e8ac --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml @@ -0,0 +1,359 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml new file mode 100644 index 0000000000..b12a639d31 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml @@ -0,0 +1,525 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml new file mode 100644 index 0000000000..31c2f592b9 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml @@ -0,0 +1,528 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml new file mode 100644 index 0000000000..4fe0d52cc4 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -0,0 +1,823 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml new file mode 100644 index 0000000000..b5de500093 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -0,0 +1,821 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml new file mode 100644 index 0000000000..d29db79e7b --- /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..43867f6e97 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml b/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml new file mode 100644 index 0000000000..44e7962e17 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml @@ -0,0 +1,71 @@ + + + + + + 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..84bf799d8d --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -0,0 +1,23 @@ + + + netstandard2.0 + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia.Themes.Fluent/Button.xaml b/src/Avalonia.Themes.Fluent/Button.xaml new file mode 100644 index 0000000000..345a74512c --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Button.xaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..d1d7f36478 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/CheckBox.xaml @@ -0,0 +1,294 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/ComboBox.xaml b/src/Avalonia.Themes.Fluent/ComboBox.xaml new file mode 100644 index 0000000000..1fc657c1a3 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ComboBox.xaml @@ -0,0 +1,192 @@ + + + + + + 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..c26dec3d34 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ComboBoxItem.xaml @@ -0,0 +1,59 @@ + + + + + + Item 1 + Item 2 + + + + Item 1 + Item 2 + + + + + + + + + + + + + + + + + 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..44783a8dea --- /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/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..9ffe51fae8 --- /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..d63f785e77 --- /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..49b2d9561b --- /dev/null +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..2d5e369573 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/FocusAdorner.xaml @@ -0,0 +1,10 @@ + diff --git a/src/Avalonia.Themes.Fluent/GridSplitter.xaml b/src/Avalonia.Themes.Fluent/GridSplitter.xaml new file mode 100644 index 0000000000..dc5cd002dc --- /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..7a87387de8 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ListBox.xaml @@ -0,0 +1,43 @@ + + + + + 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..47d5988e8c --- /dev/null +++ b/src/Avalonia.Themes.Fluent/NotificationCard.xaml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/NumericUpDown.xaml b/src/Avalonia.Themes.Fluent/NumericUpDown.xaml new file mode 100644 index 0000000000..08de50c6e3 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/NumericUpDown.xaml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/OverlayPopupHost.xaml b/src/Avalonia.Themes.Fluent/OverlayPopupHost.xaml new file mode 100644 index 0000000000..36dbc1b761 --- /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..d3c2f0c784 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ProgressBar.xaml @@ -0,0 +1,81 @@ + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/RadioButton.xaml b/src/Avalonia.Themes.Fluent/RadioButton.xaml new file mode 100644 index 0000000000..23bcfc616a --- /dev/null +++ b/src/Avalonia.Themes.Fluent/RadioButton.xaml @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/RepeatButton.xaml b/src/Avalonia.Themes.Fluent/RepeatButton.xaml new file mode 100644 index 0000000000..12ba38d614 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/RepeatButton.xaml @@ -0,0 +1,62 @@ + + + + + + + + + + 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..539c448e0f --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Slider.xaml @@ -0,0 +1,264 @@ + + + + + + + + + + + + + + + + + 0,0,0,4 + 15 + 15 + 32 + 32 + 10 + 20 + 20 + 20 + 20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/TabControl.xaml b/src/Avalonia.Themes.Fluent/TabControl.xaml new file mode 100644 index 0000000000..3cfdf60ae0 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/TabControl.xaml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/TabItem.xaml b/src/Avalonia.Themes.Fluent/TabItem.xaml new file mode 100644 index 0000000000..694e86a184 --- /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..d45f705a40 --- /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..49fc4b59b0 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/TextBox.xaml @@ -0,0 +1,153 @@ + + + 0,0,0,4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/ToggleButton.xaml b/src/Avalonia.Themes.Fluent/ToggleButton.xaml new file mode 100644 index 0000000000..a3c300f6f5 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ToggleButton.xaml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + 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..88266ac979 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml @@ -0,0 +1,294 @@ + + + 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..cf6f32f9bc --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ToolTip.xaml @@ -0,0 +1,78 @@ + + + + + Hover Here + + + + + + ToolTip + A control which pops up a hint when a control is hovered + + + ToolTip bottom placement + + + + + + + + + + + + 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..aee15347eb --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Window.xaml @@ -0,0 +1,22 @@ + diff --git a/src/Avalonia.Themes.Fluent/WindowNotificationManager.xaml b/src/Avalonia.Themes.Fluent/WindowNotificationManager.xaml new file mode 100644 index 0000000000..7c1efa2e82 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/WindowNotificationManager.xaml @@ -0,0 +1,45 @@ + + + + + + + + + + + diff --git a/src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs new file mode 100644 index 0000000000..ff76902425 --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs @@ -0,0 +1,23 @@ +using Avalonia.Media; + +namespace Avalonia.Animation.Animators +{ + public class BoxShadowAnimator : Animator + { + static ColorAnimator s_colorAnimator = new ColorAnimator(); + static DoubleAnimator s_doubleAnimator = new DoubleAnimator(); + static BoolAnimator s_boolAnimator = new BoolAnimator(); + public override BoxShadow Interpolate(double progress, BoxShadow oldValue, BoxShadow newValue) + { + return new BoxShadow + { + OffsetX = s_doubleAnimator.Interpolate(progress, oldValue.OffsetX, newValue.OffsetX), + OffsetY = s_doubleAnimator.Interpolate(progress, oldValue.OffsetY, newValue.OffsetY), + Blur = s_doubleAnimator.Interpolate(progress, oldValue.Blur, newValue.Blur), + Spread = s_doubleAnimator.Interpolate(progress, oldValue.Spread, newValue.Spread), + Color = s_colorAnimator.Interpolate(progress, oldValue.Color, newValue.Color), + IsInset = s_boolAnimator.Interpolate(progress, oldValue.IsInset, newValue.IsInset) + }; + } + } +} diff --git a/src/Avalonia.Visuals/Animation/Animators/BoxShadowsAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/BoxShadowsAnimator.cs new file mode 100644 index 0000000000..c6f96a2d0e --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Animators/BoxShadowsAnimator.cs @@ -0,0 +1,40 @@ +using Avalonia.Media; + +namespace Avalonia.Animation.Animators +{ + public class BoxShadowsAnimator : Animator + { + private static readonly BoxShadowAnimator s_boxShadowAnimator = new BoxShadowAnimator(); + public override BoxShadows Interpolate(double progress, BoxShadows oldValue, BoxShadows newValue) + { + int cnt = progress >= 1d ? newValue.Count : oldValue.Count; + if (cnt == 0) + return new BoxShadows(); + + BoxShadow first; + if (oldValue.Count > 0 && newValue.Count > 0) + first = s_boxShadowAnimator.Interpolate(progress, oldValue[0], newValue[0]); + else if (oldValue.Count > 0) + first = oldValue[0]; + else + first = newValue[0]; + + if (cnt == 1) + return new BoxShadows(first); + + var rest = new BoxShadow[cnt - 1]; + for (var c = 0; c < rest.Length; c++) + { + var idx = c + 1; + if (oldValue.Count > idx && newValue.Count > idx) + rest[c] = s_boxShadowAnimator.Interpolate(progress, oldValue[idx], newValue[idx]); + else if (oldValue.Count > idx) + rest[c] = oldValue[idx]; + else + rest[c] = newValue[idx]; + } + + return new BoxShadows(first, rest); + } + } +} 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/CrossFade.cs b/src/Avalonia.Visuals/Animation/CrossFade.cs index 614d828259..640f401418 100644 --- a/src/Avalonia.Visuals/Animation/CrossFade.cs +++ b/src/Avalonia.Visuals/Animation/CrossFade.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/src/Avalonia.Visuals/Animation/IPageTransition.cs b/src/Avalonia.Visuals/Animation/IPageTransition.cs index 4f598fa08e..659bc12424 100644 --- a/src/Avalonia.Visuals/Animation/IPageTransition.cs +++ b/src/Avalonia.Visuals/Animation/IPageTransition.cs @@ -1,6 +1,3 @@ -// 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 System.Threading.Tasks; namespace Avalonia.Animation diff --git a/src/Avalonia.Visuals/Animation/PageSlide.cs b/src/Avalonia.Visuals/Animation/PageSlide.cs index 0aa85035ae..501c8c0ba4 100644 --- a/src/Avalonia.Visuals/Animation/PageSlide.cs +++ b/src/Avalonia.Visuals/Animation/PageSlide.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs index 4b6703133d..0b0f04ca94 100644 --- a/src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs +++ b/src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Reactive.Linq; diff --git a/src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs index ea0000f960..29db5fc868 100644 --- a/src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs +++ b/src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Reactive.Linq; diff --git a/src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs index 5de59edc53..b40e789915 100644 --- a/src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs +++ b/src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Reactive.Linq; diff --git a/src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs index e6b5e8904b..28d4ea067f 100644 --- a/src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs +++ b/src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Reactive.Linq; 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/Animation/Transitions/VectorTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs index 5271c1378b..c073e8e192 100644 --- a/src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs +++ b/src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Reactive.Linq; diff --git a/src/Avalonia.Visuals/Assets/GraphemeBreak.trie b/src/Avalonia.Visuals/Assets/GraphemeBreak.trie new file mode 100644 index 0000000000..704dea4e86 Binary files /dev/null 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 new file mode 100644 index 0000000000..2e39745646 Binary files /dev/null and b/src/Avalonia.Visuals/Assets/UnicodeData.trie differ diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj index 2cc7741bbb..576200a996 100644 --- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj +++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj @@ -2,7 +2,11 @@ netstandard2.0 Avalonia + true + + + diff --git a/src/Avalonia.Visuals/AvaloniaPropertyExtensions.cs b/src/Avalonia.Visuals/AvaloniaPropertyExtensions.cs new file mode 100644 index 0000000000..6aaa224b00 --- /dev/null +++ b/src/Avalonia.Visuals/AvaloniaPropertyExtensions.cs @@ -0,0 +1,27 @@ +using Avalonia.Media; + +#nullable enable + +namespace Avalonia +{ + /// + /// Extensions for . + /// + public static class AvaloniaPropertyExtensions + { + /// + /// Checks if values of given property can affect rendering (via ). + /// + /// Property to check. + public static bool CanValueAffectRender(this AvaloniaProperty property) + { + var propertyType = property.PropertyType; + + // Only case that we are sure that property value CAN'T affect render are sealed types that don't implement + // the interface. + var cannotAffectRender = propertyType.IsSealed && !typeof(IAffectsRender).IsAssignableFrom(propertyType); + + return !cannotAffectRender; + } + } +} diff --git a/src/Avalonia.Visuals/CornerRadius.cs b/src/Avalonia.Visuals/CornerRadius.cs index ecb9e75d82..c02aacfb4d 100644 --- a/src/Avalonia.Visuals/CornerRadius.cs +++ b/src/Avalonia.Visuals/CornerRadius.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Globalization; using Avalonia.Animation.Animators; diff --git a/src/Avalonia.Visuals/Matrix.cs b/src/Avalonia.Visuals/Matrix.cs index 92b7dae904..206b842220 100644 --- a/src/Avalonia.Visuals/Matrix.cs +++ b/src/Avalonia.Visuals/Matrix.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Globalization; using Avalonia.Utilities; @@ -57,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 @@ -289,7 +286,7 @@ namespace Avalonia { double d = GetDeterminant(); - if (d == 0) + if (MathUtilities.IsZero(d)) { throw new InvalidOperationException("Transform is not invertible."); } @@ -306,7 +303,7 @@ namespace Avalonia /// /// Parses a string. /// - /// The string. + /// Six comma-delimited double values (m11, m12, m21, m22, offsetX, offsetY) that describe the new /// The . public static Matrix Parse(string s) { @@ -322,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/AlignmentX.cs b/src/Avalonia.Visuals/Media/AlignmentX.cs index 89bc1f6258..b2f913c90a 100644 --- a/src/Avalonia.Visuals/Media/AlignmentX.cs +++ b/src/Avalonia.Visuals/Media/AlignmentX.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Media { /// diff --git a/src/Avalonia.Visuals/Media/AlignmentY.cs b/src/Avalonia.Visuals/Media/AlignmentY.cs index b05500e039..0c5829569e 100644 --- a/src/Avalonia.Visuals/Media/AlignmentY.cs +++ b/src/Avalonia.Visuals/Media/AlignmentY.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Media { /// diff --git a/src/Avalonia.Visuals/Media/ArcSegment.cs b/src/Avalonia.Visuals/Media/ArcSegment.cs index 0e9c4548a4..888cb896bb 100644 --- a/src/Avalonia.Visuals/Media/ArcSegment.cs +++ b/src/Avalonia.Visuals/Media/ArcSegment.cs @@ -1,5 +1,4 @@ -// 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 System.Globalization; namespace Avalonia.Media { @@ -99,5 +98,8 @@ namespace Avalonia.Media { ctx.ArcTo(Point, Size, RotationAngle, IsLargeArc, SweepDirection); } + + public override string ToString() + => $"A {Size} {RotationAngle.ToString(CultureInfo.InvariantCulture)} {(IsLargeArc ? 1 : 0)} {(int)SweepDirection} {Point}"; } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/BezierSegment .cs b/src/Avalonia.Visuals/Media/BezierSegment .cs index 8bea03bc23..d4330830d9 100644 --- a/src/Avalonia.Visuals/Media/BezierSegment .cs +++ b/src/Avalonia.Visuals/Media/BezierSegment .cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Media { public sealed class BezierSegment : PathSegment @@ -61,5 +58,8 @@ namespace Avalonia.Media { ctx.CubicBezierTo(Point1, Point2, Point3); } + + public override string ToString() + => $"C {Point1} {Point2} {Point3}"; } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/BoxShadow.cs b/src/Avalonia.Visuals/Media/BoxShadow.cs new file mode 100644 index 0000000000..69395fd3b8 --- /dev/null +++ b/src/Avalonia.Visuals/Media/BoxShadow.cs @@ -0,0 +1,133 @@ +using System; +using System.Globalization; +using Avalonia.Animation.Animators; +using Avalonia.Utilities; + +namespace Avalonia.Media +{ + public struct BoxShadow + { + public double OffsetX { get; set; } + public double OffsetY { get; set; } + public double Blur { get; set; } + public double Spread { get; set; } + public Color Color { get; set; } + public bool IsInset { get; set; } + + static BoxShadow() + { + Animation.Animation.RegisterAnimator(prop => + typeof(BoxShadow).IsAssignableFrom(prop.PropertyType)); + } + + public bool Equals(in BoxShadow other) + { + return OffsetX.Equals(other.OffsetX) && OffsetY.Equals(other.OffsetY) && Blur.Equals(other.Blur) && Spread.Equals(other.Spread) && Color.Equals(other.Color); + } + + public override bool Equals(object obj) + { + return obj is BoxShadow other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = OffsetX.GetHashCode(); + hashCode = (hashCode * 397) ^ OffsetY.GetHashCode(); + hashCode = (hashCode * 397) ^ Blur.GetHashCode(); + hashCode = (hashCode * 397) ^ Spread.GetHashCode(); + hashCode = (hashCode * 397) ^ Color.GetHashCode(); + return hashCode; + } + } + + public bool IsEmpty => OffsetX == 0 && OffsetY == 0 && Blur == 0 && Spread == 0; + + private readonly static char[] s_Separator = new char[] { ' ', '\t' }; + + struct ArrayReader + { + private int _index; + private string[] _arr; + + public ArrayReader(string[] arr) + { + _arr = arr; + _index = 0; + } + + public bool TryReadString(out string s) + { + s = null; + if (_index >= _arr.Length) + return false; + s = _arr[_index]; + _index++; + return true; + } + + public string ReadString() + { + if(!TryReadString(out var rv)) + throw new FormatException(); + return rv; + } + } + public static unsafe BoxShadow Parse(string s) + { + if(s == null) + throw new ArgumentNullException(); + if (s.Length == 0) + throw new FormatException(); + + var p = s.Split(s_Separator, StringSplitOptions.RemoveEmptyEntries); + if (p.Length == 1 && p[0] == "none") + return default; + + if (p.Length < 3 || p.Length > 6) + throw new FormatException(); + + bool inset = false; + + var tokenizer = new ArrayReader(p); + + string firstToken = tokenizer.ReadString(); + if (firstToken == "inset") + { + inset = true; + firstToken = tokenizer.ReadString(); + } + + var offsetX = double.Parse(firstToken, CultureInfo.InvariantCulture); + var offsetY = double.Parse(tokenizer.ReadString(), CultureInfo.InvariantCulture); + double blur = 0; + double spread = 0; + + + tokenizer.TryReadString(out var token3); + tokenizer.TryReadString(out var token4); + tokenizer.TryReadString(out var token5); + + if (token4 != null) + blur = double.Parse(token3, CultureInfo.InvariantCulture); + if (token5 != null) + spread = double.Parse(token4, CultureInfo.InvariantCulture); + + var color = Color.Parse(token5 ?? token4 ?? token3); + return new BoxShadow + { + IsInset = inset, + OffsetX = offsetX, + OffsetY = offsetY, + Blur = blur, + Spread = spread, + Color = color + }; + } + + public Rect TransformBounds(in Rect rect) + => IsInset ? rect : rect.Translate(new Vector(OffsetX, OffsetY)).Inflate(Spread + Blur); + } +} diff --git a/src/Avalonia.Visuals/Media/BoxShadows.cs b/src/Avalonia.Visuals/Media/BoxShadows.cs new file mode 100644 index 0000000000..9e4d6aacb0 --- /dev/null +++ b/src/Avalonia.Visuals/Media/BoxShadows.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Avalonia.Animation.Animators; + +namespace Avalonia.Media +{ + public struct BoxShadows + { + private readonly BoxShadow _first; + private readonly BoxShadow[] _list; + public int Count { get; } + + static BoxShadows() + { + Animation.Animation.RegisterAnimator(prop => + typeof(BoxShadows).IsAssignableFrom(prop.PropertyType)); + } + + public BoxShadows(BoxShadow shadow) + { + _first = shadow; + _list = null; + Count = _first.IsEmpty ? 0 : 1; + } + + public BoxShadows(BoxShadow first, BoxShadow[] rest) + { + _first = first; + _list = rest; + Count = 1 + (rest?.Length ?? 0); + } + + public BoxShadow this[int c] + { + get + { + if (c< 0 || c >= Count) + throw new IndexOutOfRangeException(); + if (c == 0) + return _first; + return _list[c - 1]; + } + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public struct BoxShadowsEnumerator + { + private int _index; + private BoxShadows _shadows; + + public BoxShadowsEnumerator(BoxShadows shadows) + { + _shadows = shadows; + _index = -1; + } + + public BoxShadow Current => _shadows[_index]; + + public bool MoveNext() + { + _index++; + return _index < _shadows.Count; + } + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public BoxShadowsEnumerator GetEnumerator() => new BoxShadowsEnumerator(this); + + private static readonly char[] s_Separators = new[] { ',' }; + public static BoxShadows Parse(string s) + { + var sp = s.Split(s_Separators, StringSplitOptions.RemoveEmptyEntries); + if (sp.Length == 0 + || (sp.Length == 1 && + (string.IsNullOrWhiteSpace(sp[0]) + || sp[0] == "none"))) + return new BoxShadows(); + + var first = BoxShadow.Parse(sp[0]); + if (sp.Length == 1) + return new BoxShadows(first); + + var rest = new BoxShadow[sp.Length - 1]; + for (var c = 0; c < rest.Length; c++) + rest[c] = BoxShadow.Parse(sp[c + 1]); + return new BoxShadows(first, rest); + } + + public Rect TransformBounds(in Rect rect) + { + var final = rect; + foreach (var shadow in this) + final = final.Union(shadow.TransformBounds(rect)); + return final; + } + + public bool HasInsetShadows + { + get + { + foreach(var boxShadow in this) + if (!boxShadow.IsEmpty && boxShadow.IsInset) + return true; + return false; + } + } + + public bool Equals(BoxShadows other) + { + if (other.Count != Count) + return false; + for(var c=0; c public event EventHandler Invalidated; + static Brush() + { + AffectsRender(OpacityProperty); + } + /// /// Gets or sets the opacity of the brush. /// @@ -69,14 +71,14 @@ namespace Avalonia.Media protected static void AffectsRender(params AvaloniaProperty[] properties) where T : Brush { - void Invalidate(AvaloniaPropertyChangedEventArgs e) + static void Invalidate(AvaloniaPropertyChangedEventArgs e) { (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty); } foreach (var property in properties) { - property.Changed.Subscribe(Invalidate); + property.Changed.Subscribe(e => Invalidate(e)); } } diff --git a/src/Avalonia.Visuals/Media/BrushConverter.cs b/src/Avalonia.Visuals/Media/BrushConverter.cs index c79360204a..b3c105ffc7 100644 --- a/src/Avalonia.Visuals/Media/BrushConverter.cs +++ b/src/Avalonia.Visuals/Media/BrushConverter.cs @@ -1,6 +1,3 @@ -// 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 System; using System.ComponentModel; using System.Globalization; diff --git a/src/Avalonia.Visuals/Media/BrushMappingMode.cs b/src/Avalonia.Visuals/Media/BrushMappingMode.cs index eb64a063a1..b8f92696dc 100644 --- a/src/Avalonia.Visuals/Media/BrushMappingMode.cs +++ b/src/Avalonia.Visuals/Media/BrushMappingMode.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Media { diff --git a/src/Avalonia.Visuals/Media/Brushes.cs b/src/Avalonia.Visuals/Media/Brushes.cs index 83ff043397..5957775f39 100644 --- a/src/Avalonia.Visuals/Media/Brushes.cs +++ b/src/Avalonia.Visuals/Media/Brushes.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Media { /// diff --git a/src/Avalonia.Visuals/Media/CharacterHit.cs b/src/Avalonia.Visuals/Media/CharacterHit.cs new file mode 100644 index 0000000000..ba691dad6e --- /dev/null +++ b/src/Avalonia.Visuals/Media/CharacterHit.cs @@ -0,0 +1,65 @@ +using System; + +namespace Avalonia.Media +{ + /// + /// Represents information about a character hit within a glyph run. + /// + /// + /// The CharacterHit structure provides information about the index of the first + /// character that got hit as well as information about leading or trailing edge. + /// + public readonly struct CharacterHit : IEquatable + { + /// + /// Initializes a new instance of the structure. + /// + /// Index of the first character that got hit. + /// In the case of a leading edge, this value is 0. In the case of a trailing edge, + /// this value is the number of code points until the next valid caret position. + public CharacterHit(int firstCharacterIndex, int trailingLength = 0) + { + FirstCharacterIndex = firstCharacterIndex; + + TrailingLength = trailingLength; + } + + /// + /// Gets the index of the first character that got hit. + /// + public int FirstCharacterIndex { get; } + + /// + /// Gets the trailing length value for the character that got hit. + /// + public int TrailingLength { get; } + + public bool Equals(CharacterHit other) + { + return FirstCharacterIndex == other.FirstCharacterIndex && TrailingLength == other.TrailingLength; + } + + public override bool Equals(object obj) + { + return obj is CharacterHit other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return FirstCharacterIndex * 397 ^ TrailingLength; + } + } + + public static bool operator ==(CharacterHit left, CharacterHit right) + { + return left.Equals(right); + } + + public static bool operator !=(CharacterHit left, CharacterHit right) + { + return !left.Equals(right); + } + } +} diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index a37463a0f0..052ee5e1b7 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -1,9 +1,5 @@ -// 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 System; using System.Globalization; -using Avalonia.Animation; using Avalonia.Animation.Animators; namespace Avalonia.Media @@ -11,7 +7,7 @@ namespace Avalonia.Media /// /// An ARGB color. /// - public readonly struct Color + public readonly struct Color : IEquatable { static Color() { @@ -19,22 +15,22 @@ namespace Avalonia.Media } /// - /// Gets or sets the Alpha component of the color. + /// Gets the Alpha component of the color. /// public byte A { get; } /// - /// Gets or sets the Red component of the color. + /// Gets the Red component of the color. /// public byte R { get; } /// - /// Gets or sets the Green component of the color. + /// Gets the Green component of the color. /// public byte G { get; } /// - /// Gets or sets the Blue component of the color. + /// Gets the Blue component of the color. /// public byte B { get; } @@ -93,33 +89,147 @@ 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; + } + + 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)) + { + return color; + } + + throw new FormatException($"Invalid color string: '{s.ToString()}'."); + } + + /// + /// 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) + { + color = knownColor.ToColor(); + + return true; + } + + color = default; + + return false; + } + + /// + /// Parses a color string. + /// + /// The color string. + /// The parsed color + /// The status of the operation. + public static bool TryParse(ReadOnlySpan s, out Color color) + { + 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) + { + alphaComponent = 0xff000000; + } + else if (input.Length != 8) { - or = 0xff000000; + return false; } - else if (s.Length != 9) + + // TODO: (netstandard 2.1) Can use allocation free parsing. + if (!uint.TryParse(input.ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture, + out var parsed)) { - throw new FormatException($"Invalid color string: '{s}'."); + return false; } - return FromUInt32(uint.Parse(s.Substring(1), NumberStyles.HexNumber, CultureInfo.InvariantCulture) | or); + color = FromUInt32(parsed | alphaComponent); + + return true; } - var knownColor = KnownColors.GetKnownColor(s); + 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) { - return knownColor.ToColor(); + 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); } - throw new FormatException($"Invalid color string: '{s}'."); + return TryParseCore(input, ref color); } /// @@ -144,5 +254,40 @@ namespace Avalonia.Media { return ((uint)A << 24) | ((uint)R << 16) | ((uint)G << 8) | (uint)B; } + + /// + /// Check if two colors are equal. + /// + public bool Equals(Color other) + { + return A == other.A && R == other.R && G == other.G && B == other.B; + } + + public override bool Equals(object obj) + { + return obj is Color other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + int hashCode = A.GetHashCode(); + hashCode = (hashCode * 397) ^ R.GetHashCode(); + hashCode = (hashCode * 397) ^ G.GetHashCode(); + hashCode = (hashCode * 397) ^ B.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(Color left, Color right) + { + return left.Equals(right); + } + + public static bool operator !=(Color left, Color right) + { + return !left.Equals(right); + } } } diff --git a/src/Avalonia.Visuals/Media/Colors.cs b/src/Avalonia.Visuals/Media/Colors.cs index ca4a0793cb..394544f451 100644 --- a/src/Avalonia.Visuals/Media/Colors.cs +++ b/src/Avalonia.Visuals/Media/Colors.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Media { diff --git a/src/Avalonia.Visuals/Media/DashStyle.cs b/src/Avalonia.Visuals/Media/DashStyle.cs index 7784c73736..1e813edc13 100644 --- a/src/Avalonia.Visuals/Media/DashStyle.cs +++ b/src/Avalonia.Visuals/Media/DashStyle.cs @@ -14,13 +14,13 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly AvaloniaProperty> DashesProperty = + public static readonly StyledProperty> DashesProperty = AvaloniaProperty.Register>(nameof(Dashes)); /// /// Defines the property. /// - public static readonly AvaloniaProperty OffsetProperty = + public static readonly StyledProperty OffsetProperty = AvaloniaProperty.Register(nameof(Offset)); private static ImmutableDashStyle s_dash; diff --git a/src/Avalonia.Visuals/Media/Drawing.cs b/src/Avalonia.Visuals/Media/Drawing.cs index a60c591edc..6bc808e407 100644 --- a/src/Avalonia.Visuals/Media/Drawing.cs +++ b/src/Avalonia.Visuals/Media/Drawing.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Media +using Avalonia.Platform; + +namespace Avalonia.Media { public abstract class Drawing : AvaloniaObject { @@ -6,4 +8,4 @@ public abstract Rect GetBounds(); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index 4c9bf9ebd4..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 @@ -74,18 +75,29 @@ namespace Avalonia.Media public Matrix CurrentContainerTransform => _currentContainerTransform; /// - /// Draws a bitmap image. + /// Draws an image. /// - /// The bitmap image. - /// The opacity to draw with. + /// The image. + /// The rect in the output to draw to. + public void DrawImage(IImage source, Rect rect) + { + Contract.Requires(source != null); + + DrawImage(source, new Rect(source.Size), rect); + } + + /// + /// Draws an image. + /// + /// The image. /// The rect in the image to draw. /// The rect in the output to draw to. /// The bitmap interpolation mode. - public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default) + public void DrawImage(IImage source, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default) { Contract.Requires(source != null); - PlatformImpl.DrawImage(source.PlatformImpl, opacity, sourceRect, destRect, bitmapInterpolationMode); + source.Draw(this, sourceRect, destRect, bitmapInterpolationMode); } /// @@ -118,6 +130,44 @@ namespace Avalonia.Media } } + /// + /// Draws a rectangle with the specified Brush and Pen. + /// + /// The brush used to fill the rectangle, or null for no fill. + /// The pen used to stroke the rectangle, or null for no stroke. + /// The rectangle bounds. + /// The radius in the X dimension of the rounded corners. + /// This value will be clamped to the range of 0 to Width/2 + /// + /// The radius in the Y dimension of the rounded corners. + /// This value will be clamped to the range of 0 to Height/2 + /// + /// Box shadow effect parameters + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0, + BoxShadows boxShadows = default) + { + if (brush == null && !PenIsVisible(pen)) + { + return; + } + + if (!MathUtilities.IsZero(radiusX)) + { + radiusX = Math.Min(radiusX, rect.Width / 2); + } + + if (!MathUtilities.IsZero(radiusY)) + { + radiusY = Math.Min(radiusY, rect.Height / 2); + } + + PlatformImpl.DrawRectangle(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadows); + } + /// /// Draws the outline of a rectangle. /// @@ -126,10 +176,7 @@ namespace Avalonia.Media /// The corner radius. public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f) { - if (PenIsVisible(pen)) - { - PlatformImpl.DrawRectangle(pen, rect, cornerRadius); - } + DrawRectangle(null, pen, rect, cornerRadius, cornerRadius); } /// @@ -154,6 +201,22 @@ namespace Avalonia.Media } } + /// + /// Draws a glyph run. + /// + /// The foreground brush. + /// The glyph run. + /// The baseline origin of the glyph run. + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + { + Contract.Requires(glyphRun != null); + + if (foreground != null) + { + PlatformImpl.DrawGlyphRun(foreground, glyphRun, baselineOrigin); + } + } + /// /// Draws a filled rectangle. /// @@ -162,10 +225,7 @@ namespace Avalonia.Media /// The corner radius. public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f) { - if (brush != null && rect != Rect.Empty) - { - PlatformImpl.FillRectangle(brush, rect, cornerRadius); - } + DrawRectangle(brush, null, rect, cornerRadius, cornerRadius); } public readonly struct PushedState : IDisposable @@ -223,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. /// @@ -291,7 +357,7 @@ namespace Avalonia.Media /// /// The matrix /// A disposable used to undo the transformation. - PushedState PushSetTransform(Matrix matrix) + public PushedState PushSetTransform(Matrix matrix) { var oldMatrix = CurrentTransform; CurrentTransform = matrix; diff --git a/src/Avalonia.Visuals/Media/DrawingGroup.cs b/src/Avalonia.Visuals/Media/DrawingGroup.cs index 744ff2af03..e581c8c553 100644 --- a/src/Avalonia.Visuals/Media/DrawingGroup.cs +++ b/src/Avalonia.Visuals/Media/DrawingGroup.cs @@ -1,5 +1,6 @@ using Avalonia.Collections; using Avalonia.Metadata; +using Avalonia.Platform; namespace Avalonia.Media { @@ -55,4 +56,4 @@ namespace Avalonia.Media return rect; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Media/DrawingImage.cs b/src/Avalonia.Visuals/Media/DrawingImage.cs new file mode 100644 index 0000000000..56c883014a --- /dev/null +++ b/src/Avalonia.Visuals/Media/DrawingImage.cs @@ -0,0 +1,82 @@ +using System; +using Avalonia.Data; +using Avalonia.Metadata; +using Avalonia.Platform; +using Avalonia.Visuals.Media.Imaging; + +namespace Avalonia.Media +{ + /// + /// An that uses a for content. + /// + public class DrawingImage : AvaloniaObject, IImage, IAffectsRender + { + /// + /// Defines the property. + /// + public static readonly StyledProperty DrawingProperty = + AvaloniaProperty.Register(nameof(Drawing)); + + /// + public event EventHandler Invalidated; + + /// + /// Gets or sets the drawing content. + /// + [Content] + public Drawing Drawing + { + get => GetValue(DrawingProperty); + set => SetValue(DrawingProperty, value); + } + + /// + public Size Size => Drawing?.GetBounds().Size ?? default; + + /// + void IImage.Draw( + DrawingContext context, + Rect sourceRect, + Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode) + { + var drawing = Drawing; + + if (drawing == null) + { + return; + } + + var bounds = drawing.GetBounds(); + var scale = Matrix.CreateScale( + destRect.Width / sourceRect.Width, + destRect.Height / sourceRect.Height); + var translate = Matrix.CreateTranslation( + -sourceRect.X + destRect.X - bounds.X, + -sourceRect.Y + destRect.Y - bounds.Y); + + using (context.PushClip(destRect)) + using (context.PushPreTransform(translate * scale)) + { + Drawing?.Draw(context); + } + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == DrawingProperty) + { + RaiseInvalidated(EventArgs.Empty); + } + } + + /// + /// Raises the event. + /// + /// The event args. + protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e); + } +} diff --git a/src/Avalonia.Visuals/Media/EllipseGeometry.cs b/src/Avalonia.Visuals/Media/EllipseGeometry.cs index c2df9db635..fd73eee69a 100644 --- a/src/Avalonia.Visuals/Media/EllipseGeometry.cs +++ b/src/Avalonia.Visuals/Media/EllipseGeometry.cs @@ -1,6 +1,3 @@ -// 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.Platform; namespace Avalonia.Media diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs index a486723d86..9db49200cd 100644 --- a/src/Avalonia.Visuals/Media/FontFamily.cs +++ b/src/Avalonia.Visuals/Media/FontFamily.cs @@ -1,16 +1,17 @@ -// 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 System; -using System.Collections.Generic; -using System.Linq; +using System; using Avalonia.Media.Fonts; -using Avalonia.Platform; namespace Avalonia.Media { - public class FontFamily + public sealed class FontFamily { + public const string DefaultFontFamilyName = "$Default"; + + static FontFamily() + { + Default = new FontFamily(DefaultFontFamilyName); + } + /// /// /// Initializes a new instance of the class. @@ -30,9 +31,7 @@ namespace Avalonia.Media { if (string.IsNullOrEmpty(name)) { - FamilyNames = new FamilyNameCollection(string.Empty); - - return; + throw new ArgumentNullException(nameof(name)); } var fontFamilySegment = GetFontFamilyIdentifier(name); @@ -53,13 +52,7 @@ namespace Avalonia.Media /// /// Represents the default font family /// - public static FontFamily Default => new FontFamily(string.Empty); - - /// - /// Represents all font families in the system. This can be an expensive call depending on platform implementation. - /// - public static IEnumerable SystemFontFamilies => - AvaloniaLocator.Current.GetService().InstalledFontNames.Select(name => new FontFamily(name)); + public static FontFamily Default { get; } /// /// Gets the primary family name of the font family. @@ -81,10 +74,16 @@ namespace Avalonia.Media /// Gets the key for associated assets. /// /// - /// The family familyNames. + /// The family key. /// + /// Key is only used for custom fonts. public FontFamilyKey Key { get; } + /// + /// Returns True if this instance is the system's default. + /// + public bool IsDefault => Name.Equals(DefaultFontFamilyName); + /// /// Implicit conversion of string to FontFamily /// @@ -179,29 +178,40 @@ namespace Avalonia.Media { unchecked { - var hash = (int)2186146271; - - hash = (hash * 15768619) ^ FamilyNames.GetHashCode(); + return ((FamilyNames != null ? FamilyNames.GetHashCode() : 0) * 397) ^ (Key != null ? Key.GetHashCode() : 0); + } + } - if (Key != null) - { - hash = (hash * 15768619) ^ Key.GetHashCode(); - } + public static bool operator !=(FontFamily a, FontFamily b) + { + return !(a == b); + } - return hash; + public static bool operator ==(FontFamily a, FontFamily b) + { + if (ReferenceEquals(a, b)) + { + return true; } + + return !(a is null) && a.Equals(b); } public override bool Equals(object obj) { + if (ReferenceEquals(this, obj)) + { + return true; + } + if (!(obj is FontFamily other)) { return false; } - if (Key != null) + if (!Equals(Key, other.Key)) { - return other.FamilyNames.Equals(FamilyNames) && other.Key.Equals(Key); + return false; } return other.FamilyNames.Equals(FamilyNames); diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs new file mode 100644 index 0000000000..f9410afe6a --- /dev/null +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Media.Fonts; +using Avalonia.Platform; + +namespace Avalonia.Media +{ + /// + /// The font manager is used to query the system's installed fonts and is responsible for caching loaded fonts. + /// It is also responsible for the font fallback. + /// + public sealed class FontManager + { + private readonly ConcurrentDictionary _typefaceCache = + new ConcurrentDictionary(); + private readonly FontFamily _defaultFontFamily; + + public FontManager(IFontManagerImpl platformImpl) + { + PlatformImpl = platformImpl; + + DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName(); + + if (string.IsNullOrEmpty(DefaultFontFamilyName)) + { + throw new InvalidOperationException("Default font family name can't be null or empty."); + } + + _defaultFontFamily = new FontFamily(DefaultFontFamilyName); + } + + public static FontManager Current + { + get + { + var current = AvaloniaLocator.Current.GetService(); + + if (current != null) + { + return current; + } + + var fontManagerImpl = AvaloniaLocator.Current.GetService(); + + if (fontManagerImpl == null) + throw new InvalidOperationException("No font manager implementation was registered."); + + current = new FontManager(fontManagerImpl); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(current); + + return current; + } + } + + /// + /// + /// + public IFontManagerImpl PlatformImpl { get; } + + /// + /// Gets the system's default font family's name. + /// + public string DefaultFontFamilyName + { + get; + } + + /// + /// Get all installed font family names. + /// + /// If true the font collection is updated. + public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => + PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates); + + /// + /// Returns a new typeface, or an existing one if a matching typeface exists. + /// + /// The font family. + /// The font weight. + /// The font style. + /// + /// The typeface. + /// + public Typeface GetOrAddTypeface(FontFamily fontFamily, FontWeight fontWeight = FontWeight.Normal, FontStyle fontStyle = FontStyle.Normal) + { + while (true) + { + if (fontFamily.IsDefault) + { + fontFamily = _defaultFontFamily; + } + + var key = new FontKey(fontFamily.Name, fontWeight, fontStyle); + + if (_typefaceCache.TryGetValue(key, out var typeface)) + { + return typeface; + } + + typeface = new Typeface(fontFamily, fontWeight, fontStyle); + + if (_typefaceCache.TryAdd(key, typeface)) + { + return typeface; + } + + if (fontFamily == _defaultFontFamily) + { + return null; + } + + fontFamily = _defaultFontFamily; + } + } + + /// + /// Tries to match a specified character to a typeface that supports specified font properties. + /// Returns null if no fallback was found. + /// + /// The codepoint to match against. + /// The font weight. + /// The font style. + /// The font family. This is optional and used for fallback lookup. + /// The culture. + /// + /// The matched typeface. + /// + public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = FontWeight.Normal, + FontStyle fontStyle = FontStyle.Normal, + FontFamily fontFamily = null, CultureInfo culture = null) + { + foreach (var cachedTypeface in _typefaceCache.Values) + { + // First try to find a cached typeface by style and weight to avoid redundant glyph index lookup. + if (cachedTypeface.Style == fontStyle && cachedTypeface.Weight == fontWeight + && cachedTypeface.GlyphTypeface.GetGlyph((uint)codepoint) != 0) + { + return cachedTypeface; + } + } + + var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ? + _typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Weight, key.Style)) : + null; + + return matchedTypeface; + } + } +} diff --git a/src/Avalonia.Visuals/Media/FontStyle.cs b/src/Avalonia.Visuals/Media/FontStyle.cs index 1320f9d70e..cbc92b1a9f 100644 --- a/src/Avalonia.Visuals/Media/FontStyle.cs +++ b/src/Avalonia.Visuals/Media/FontStyle.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Media { /// diff --git a/src/Avalonia.Visuals/Media/FontWeight.cs b/src/Avalonia.Visuals/Media/FontWeight.cs index 776f14d647..5a4a4963a5 100644 --- a/src/Avalonia.Visuals/Media/FontWeight.cs +++ b/src/Avalonia.Visuals/Media/FontWeight.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Media { /// @@ -32,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/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs index acf0bbdb11..96312a5466 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs @@ -1,15 +1,12 @@ -// 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 System; +using System; using System.Collections; using System.Collections.Generic; -using System.Linq; using System.Text; +using Avalonia.Utilities; namespace Avalonia.Media.Fonts { - public class FamilyNameCollection : IEnumerable + public sealed class FamilyNameCollection : IReadOnlyList { /// /// Initializes a new instance of the class. @@ -23,7 +20,7 @@ namespace Avalonia.Media.Fonts throw new ArgumentNullException(nameof(familyNames)); } - Names = familyNames.Split(',').Select(x => x.Trim()).ToArray(); + Names = Array.ConvertAll(familyNames.Split(','), p => p.Trim()); PrimaryFamilyName = Names[0]; @@ -54,25 +51,19 @@ namespace Avalonia.Media.Fonts /// internal IReadOnlyList Names { get; } - /// /// - /// Returns an enumerator that iterates through the collection. + /// Returns an enumerator for the name collection. /// - /// - /// An enumerator that can be used to iterate through the collection. - /// - public IEnumerator GetEnumerator() + public ImmutableReadOnlyListStructEnumerator GetEnumerator() { - return Names.GetEnumerator(); + return new ImmutableReadOnlyListStructEnumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); } - /// - /// - /// Returns an enumerator that iterates through a collection. - /// - /// - /// An object that can be used to iterate through the collection. - /// IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); @@ -111,7 +102,39 @@ namespace Avalonia.Media.Fonts /// public override int GetHashCode() { - return ToString().GetHashCode(); + if (Count == 0) + { + return 0; + } + + unchecked + { + int hash = 17; + + for (var i = 0; i < Names.Count; i++) + { + string name = Names[i]; + + hash = hash * 23 + name.GetHashCode(); + } + + return hash; + } + } + + public static bool operator !=(FamilyNameCollection a, FamilyNameCollection b) + { + return !(a == b); + } + + public static bool operator ==(FamilyNameCollection a, FamilyNameCollection b) + { + if (ReferenceEquals(a, b)) + { + return true; + } + + return !(a is null) && a.Equals(b); } /// @@ -128,7 +151,24 @@ namespace Avalonia.Media.Fonts return false; } - return other.ToString().Equals(ToString()); + if (other.Count != Count) + { + return false; + } + + for (int i = 0; i < Count; i++) + { + if (Names[i] != other.Names[i]) + { + return false; + } + } + + return true; } + + public int Count => Names.Count; + + public string this[int index] => Names[index]; } } diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs index 7733dd7d2a..eee24e856d 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; namespace Avalonia.Media.Fonts { @@ -58,6 +55,21 @@ namespace Avalonia.Media.Fonts } } + public static bool operator !=(FontFamilyKey a, FontFamilyKey b) + { + return !(a == b); + } + + public static bool operator ==(FontFamilyKey a, FontFamilyKey b) + { + if (ReferenceEquals(a, b)) + { + return true; + } + + return !(a is null) && a.Equals(b); + } + /// /// Determines whether the specified , is equal to this instance. /// diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs b/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs index 063fe8f20d..02298417a4 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Collections.Generic; using System.Linq; using Avalonia.Platform; @@ -10,13 +7,6 @@ namespace Avalonia.Media.Fonts { public static class FontFamilyLoader { - private static readonly IAssetLoader s_assetLoader; - - static FontFamilyLoader() - { - s_assetLoader = AvaloniaLocator.Current.GetService(); - } - /// /// Loads all font assets that belong to the specified /// @@ -42,7 +32,9 @@ namespace Avalonia.Media.Fonts /// private static IEnumerable GetFontAssetsBySource(FontFamilyKey fontFamilyKey) { - var availableAssets = s_assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri); + var assetLoader = AvaloniaLocator.Current.GetService(); + + var availableAssets = assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri); var matchingAssets = availableAssets.Where(x => x.AbsolutePath.EndsWith(".ttf") || x.AbsolutePath.EndsWith(".otf")); @@ -58,9 +50,11 @@ namespace Avalonia.Media.Fonts /// private static IEnumerable GetFontAssetsByExpression(FontFamilyKey fontFamilyKey) { + var assetLoader = AvaloniaLocator.Current.GetService(); + var fileName = GetFileName(fontFamilyKey, out var fileExtension, out var location); - var availableResources = s_assetLoader.GetAssets(location, fontFamilyKey.BaseUri); + var availableResources = assetLoader.GetAssets(location, fontFamilyKey.BaseUri); string compareTo; diff --git a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs new file mode 100644 index 0000000000..a8d81648ba --- /dev/null +++ b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs @@ -0,0 +1,40 @@ +using System; + +namespace Avalonia.Media.Fonts +{ + public readonly struct FontKey : IEquatable + { + public FontKey(string familyName, FontWeight weight, FontStyle style) + { + FamilyName = familyName; + Style = style; + Weight = weight; + } + + public string FamilyName { get; } + public FontStyle Style { get; } + public FontWeight Weight { get; } + + public override int GetHashCode() + { + var hash = FamilyName.GetHashCode(); + + hash = hash * 31 + (int)Style; + hash = hash * 31 + (int)Weight; + + return hash; + } + + public override bool Equals(object other) + { + return other is FontKey key && Equals(key); + } + + public bool Equals(FontKey other) + { + return FamilyName == other.FamilyName && + Style == other.Style && + Weight == other.Weight; + } + } +} diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs index e20e03e296..53231ee1dd 100644 --- a/src/Avalonia.Visuals/Media/FormattedText.cs +++ b/src/Avalonia.Visuals/Media/FormattedText.cs @@ -1,6 +1,3 @@ -// 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 System.Collections.Generic; using Avalonia.Platform; @@ -16,9 +13,10 @@ namespace Avalonia.Media private IFormattedTextImpl _platformImpl; private IReadOnlyList _spans; private Typeface _typeface; + private double _fontSize; private string _text; private TextAlignment _textAlignment; - private TextWrapping _wrapping; + private TextWrapping _textWrapping; /// /// Initializes a new instance of the class. @@ -37,6 +35,31 @@ namespace Avalonia.Media _platform = platform; } + /// + /// + /// + /// + /// + /// + /// + /// + /// + public FormattedText(string text, Typeface typeface, double fontSize, TextAlignment textAlignment, + TextWrapping textWrapping, Size constraint) + { + _text = text; + + _typeface = typeface; + + _fontSize = fontSize; + + _textAlignment = textAlignment; + + _textWrapping = textWrapping; + + _constraint = constraint; + } + /// /// Gets the bounds of the text within the . /// @@ -61,6 +84,16 @@ namespace Avalonia.Media set => Set(ref _typeface, value); } + + /// + /// Gets or sets the font size. + /// + public double FontSize + { + get => _fontSize; + set => Set(ref _fontSize, value); + } + /// /// Gets or sets a collection of spans that describe the formatting of subsections of the /// text. @@ -92,10 +125,10 @@ namespace Avalonia.Media /// /// Gets or sets the text wrapping. /// - public TextWrapping Wrapping + public TextWrapping TextWrapping { - get => _wrapping; - set => Set(ref _wrapping, value); + get => _textWrapping; + set => Set(ref _textWrapping, value); } /// @@ -110,8 +143,9 @@ namespace Avalonia.Media _platformImpl = _platform.CreateFormattedText( _text, _typeface, + _fontSize, _textAlignment, - _wrapping, + _textWrapping, _constraint, _spans); } @@ -166,7 +200,13 @@ namespace Avalonia.Media private void Set(ref T field, T value) { + if (field != null && field.Equals(value)) + { + return; + } + field = value; + _platformImpl = null; } } diff --git a/src/Avalonia.Visuals/Media/FormattedTextLine.cs b/src/Avalonia.Visuals/Media/FormattedTextLine.cs index 245e2bc108..42859f698a 100644 --- a/src/Avalonia.Visuals/Media/FormattedTextLine.cs +++ b/src/Avalonia.Visuals/Media/FormattedTextLine.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Media { /// diff --git a/src/Avalonia.Visuals/Media/Geometry.cs b/src/Avalonia.Visuals/Media/Geometry.cs index f9bcea85af..33aafb58fc 100644 --- a/src/Avalonia.Visuals/Media/Geometry.cs +++ b/src/Avalonia.Visuals/Media/Geometry.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Platform; diff --git a/src/Avalonia.Visuals/Media/GeometryDrawing.cs b/src/Avalonia.Visuals/Media/GeometryDrawing.cs index 3dad10fb8f..4df3aa8ae2 100644 --- a/src/Avalonia.Visuals/Media/GeometryDrawing.cs +++ b/src/Avalonia.Visuals/Media/GeometryDrawing.cs @@ -1,10 +1,13 @@ -namespace Avalonia.Media +using Avalonia.Metadata; + +namespace Avalonia.Media { public class GeometryDrawing : Drawing { public static readonly StyledProperty GeometryProperty = AvaloniaProperty.Register(nameof(Geometry)); + [Content] public Geometry Geometry { get => GetValue(GeometryProperty); diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs new file mode 100644 index 0000000000..29c9d93560 --- /dev/null +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -0,0 +1,549 @@ +using System; +using System.Collections.Generic; +using Avalonia.Platform; +using Avalonia.Utility; + +namespace Avalonia.Media +{ + /// + /// Represents a sequence of glyphs from a single face of a single font at a single size, and with a single rendering style. + /// + public sealed class GlyphRun : IDisposable + { + private static readonly IComparer s_ascendingComparer = Comparer.Default; + private static readonly IComparer s_descendingComparer = new ReverseComparer(); + + private IGlyphRunImpl _glyphRunImpl; + private GlyphTypeface _glyphTypeface; + private double _fontRenderingEmSize; + private Rect? _bounds; + private int _biDiLevel; + + private ReadOnlySlice _glyphIndices; + private ReadOnlySlice _glyphAdvances; + private ReadOnlySlice _glyphOffsets; + private ReadOnlySlice _glyphClusters; + private ReadOnlySlice _characters; + + /// + /// Initializes a new instance of the class. + /// + public GlyphRun() + { + + } + + /// + /// Initializes a new instance of the class by specifying properties of the class. + /// + /// The glyph typeface. + /// The rendering em size. + /// The glyph indices. + /// The glyph advances. + /// The glyph offsets. + /// The characters. + /// The glyph clusters. + /// The bidi level. + public GlyphRun( + GlyphTypeface glyphTypeface, + double fontRenderingEmSize, + ReadOnlySlice glyphIndices, + ReadOnlySlice glyphAdvances = default, + ReadOnlySlice glyphOffsets = default, + ReadOnlySlice characters = default, + ReadOnlySlice glyphClusters = default, + int biDiLevel = 0) + { + GlyphTypeface = glyphTypeface; + + FontRenderingEmSize = fontRenderingEmSize; + + GlyphIndices = glyphIndices; + + GlyphAdvances = glyphAdvances; + + GlyphOffsets = glyphOffsets; + + Characters = characters; + + GlyphClusters = glyphClusters; + + BiDiLevel = biDiLevel; + } + + /// + /// Gets or sets the for the . + /// + public GlyphTypeface GlyphTypeface + { + get => _glyphTypeface; + set => Set(ref _glyphTypeface, value); + } + + /// + /// Gets or sets the em size used for rendering the . + /// + public double FontRenderingEmSize + { + get => _fontRenderingEmSize; + set => Set(ref _fontRenderingEmSize, value); + } + + /// + /// Gets or sets an array of values that represent the glyph indices in the rendering physical font. + /// + public ReadOnlySlice GlyphIndices + { + get => _glyphIndices; + set => Set(ref _glyphIndices, value); + } + + /// + /// Gets or sets an array of values that represent the advances corresponding to the glyph indices. + /// + public ReadOnlySlice GlyphAdvances + { + get => _glyphAdvances; + set => Set(ref _glyphAdvances, value); + } + + /// + /// Gets or sets an array of values representing the offsets of the glyphs in the . + /// + public ReadOnlySlice GlyphOffsets + { + get => _glyphOffsets; + set => Set(ref _glyphOffsets, value); + } + + /// + /// Gets or sets the list of UTF16 code points that represent the Unicode content of the . + /// + public ReadOnlySlice Characters + { + get => _characters; + set => Set(ref _characters, value); + } + + /// + /// Gets or sets a list of values representing a mapping from character index to glyph index. + /// + public ReadOnlySlice GlyphClusters + { + get => _glyphClusters; + set => Set(ref _glyphClusters, value); + } + + /// + /// Gets or sets the bidirectional nesting level of the . + /// + public int BiDiLevel + { + get => _biDiLevel; + set => Set(ref _biDiLevel, value); + } + + /// + /// Gets the scale of the current + /// + internal double Scale => FontRenderingEmSize / GlyphTypeface.DesignEmHeight; + + /// + /// Returns true if the text direction is left-to-right. Otherwise, returns false. + /// + public bool IsLeftToRight => ((BiDiLevel & 1) == 0); + + /// + /// Gets or sets the conservative bounding box of the . + /// + public Rect Bounds + { + get + { + if (_bounds == null) + { + _bounds = CalculateBounds(); + } + + return _bounds.Value; + } + } + + /// + /// The platform implementation of the . + /// + public IGlyphRunImpl GlyphRunImpl + { + get + { + if (_glyphRunImpl == null) + { + Initialize(); + } + + return _glyphRunImpl; + } + } + + /// + /// Retrieves the offset from the leading edge of the + /// to the leading or trailing edge of a caret stop containing the specified character hit. + /// + /// The to use for computing the offset. + /// + /// A that represents the offset from the leading edge of the + /// to the leading or trailing edge of a caret stop containing the character hit. + /// + public double GetDistanceFromCharacterHit(CharacterHit characterHit) + { + var distance = 0.0; + + if (characterHit.FirstCharacterIndex + characterHit.TrailingLength > Characters.End) + { + return Bounds.Width; + } + + var glyphIndex = FindGlyphIndex(characterHit.FirstCharacterIndex); + + var currentCluster = _glyphClusters[glyphIndex]; + + if (characterHit.TrailingLength > 0) + { + while (glyphIndex < _glyphClusters.Length && _glyphClusters[glyphIndex] == currentCluster) + { + glyphIndex++; + } + } + + for (var i = 0; i < glyphIndex; i++) + { + if (GlyphAdvances.IsEmpty) + { + var glyph = GlyphIndices[i]; + + distance += GlyphTypeface.GetGlyphAdvance(glyph) * Scale; + } + else + { + distance += GlyphAdvances[i]; + } + } + + return distance; + } + + /// + /// Retrieves the value that represents the character hit of the caret of the . + /// + /// Offset to use for computing the caret character hit. + /// Determines whether the character hit is inside the . + /// + /// A value that represents the character hit that is closest to the distance value. + /// The out parameter isInside returns true if the character hit is inside the ; otherwise, false. + /// + public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside) + { + // Before + if (distance < 0) + { + isInside = false; + + var firstCharacterHit = FindNearestCharacterHit(_glyphClusters[0], out _); + + return IsLeftToRight ? new CharacterHit(firstCharacterHit.FirstCharacterIndex) : firstCharacterHit; + } + + //After + if (distance > Bounds.Size.Width) + { + isInside = false; + + var lastCharacterHit = FindNearestCharacterHit(_glyphClusters[_glyphClusters.Length - 1], out _); + + return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex); + } + + //Within + var currentX = 0.0; + var index = 0; + + if (GlyphTypeface.IsFixedPitch) + { + var glyph = GlyphIndices[index]; + + var advance = GlyphTypeface.GetGlyphAdvance(glyph) * Scale; + + index = Math.Min(GlyphIndices.Length - 1, + (int)Math.Round(distance / advance, MidpointRounding.AwayFromZero)); + } + else + { + for (; index < GlyphIndices.Length; index++) + { + double advance; + + if (GlyphAdvances.IsEmpty) + { + var glyph = GlyphIndices[index]; + + advance = GlyphTypeface.GetGlyphAdvance(glyph) * Scale; + } + else + { + advance = GlyphAdvances[index]; + } + + if (currentX + advance >= distance) + { + break; + } + + currentX += advance; + } + } + + var characterHit = FindNearestCharacterHit(GlyphClusters[index], out var width); + + var offset = GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex)); + + isInside = true; + + var isTrailing = distance > offset + width / 2; + + return isTrailing ? characterHit : new CharacterHit(characterHit.FirstCharacterIndex); + } + + /// + /// Retrieves the next valid caret character hit in the logical direction in the . + /// + /// The to use for computing the next hit value. + /// + /// A that represents the next valid caret character hit in the logical direction. + /// If the return value is equal to characterHit, no further navigation is possible in the . + /// + public CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) + { + if (characterHit.TrailingLength == 0) + { + return FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _); + } + + var nextCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); + + return new CharacterHit(nextCharacterHit.FirstCharacterIndex); + } + + /// + /// Retrieves the previous valid caret character hit in the logical direction in the . + /// + /// The to use for computing the previous hit value. + /// + /// A cref="CharacterHit"/> that represents the previous valid caret character hit in the logical direction. + /// If the return value is equal to characterHit, no further navigation is possible in the . + /// + public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) + { + if (characterHit.TrailingLength != 0) + { + return new CharacterHit(characterHit.FirstCharacterIndex); + } + + return characterHit.FirstCharacterIndex == Characters.Start ? + new CharacterHit(Characters.Start) : + FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _); + } + + private class ReverseComparer : IComparer + { + public int Compare(T x, T y) + { + return Comparer.Default.Compare(y, x); + } + } + + /// + /// Finds a glyph index for given character index. + /// + /// The character index. + /// + /// The glyph index. + /// + public int FindGlyphIndex(int characterIndex) + { + if (IsLeftToRight) + { + if (characterIndex < _glyphClusters[0]) + { + return 0; + } + + if (characterIndex > _glyphClusters[_glyphClusters.Length - 1]) + { + return _glyphClusters.End; + } + } + else + { + if (characterIndex < _glyphClusters[_glyphClusters.Length - 1]) + { + return _glyphClusters.End; + } + + if (characterIndex > _glyphClusters[0]) + { + return 0; + } + } + + var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer; + + var clusters = _glyphClusters.Buffer.Span; + + // Find the start of the cluster at the character index. + var start = clusters.BinarySearch((ushort)characterIndex, comparer); + + // No cluster found. + if (start < 0) + { + while (characterIndex > 0 && start < 0) + { + characterIndex--; + + start = clusters.BinarySearch((ushort)characterIndex, comparer); + } + + if (start < 0) + { + return -1; + } + } + + while (start > 0 && clusters[start - 1] == clusters[start]) + { + start--; + } + + return start; + } + + /// + /// Finds the nearest at given index. + /// + /// The index. + /// The width of found cluster. + /// + /// The nearest . + /// + public CharacterHit FindNearestCharacterHit(int index, out double width) + { + width = 0.0; + + var start = FindGlyphIndex(index); + + var currentCluster = _glyphClusters[start]; + + var trailingLength = 0; + + while (start < _glyphClusters.Length && _glyphClusters[start] == currentCluster) + { + if (GlyphAdvances.IsEmpty) + { + var glyph = GlyphIndices[start]; + + width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale; + } + else + { + width += GlyphAdvances[start]; + } + + trailingLength++; + start++; + } + + if (start == _glyphClusters.Length && + currentCluster + trailingLength != Characters.Start + Characters.Length) + { + trailingLength = Characters.Start + Characters.Length - currentCluster; + } + + return new CharacterHit(currentCluster, trailingLength); + } + + /// + /// Calculates the bounds of the . + /// + /// + /// The calculated bounds. + /// + private Rect CalculateBounds() + { + var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; + + var width = 0.0; + + if (GlyphAdvances.IsEmpty) + { + foreach (var glyph in GlyphIndices) + { + width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale; + } + } + else + { + foreach (var advance in GlyphAdvances) + { + width += advance; + } + } + + return new Rect(0, 0, width, height); + } + + private void Set(ref T field, T value) + { + if (_glyphRunImpl != null) + { + throw new InvalidOperationException("GlyphRun can't be changed after it has been initialized.'"); + } + + field = value; + } + + /// + /// Initializes the . + /// + private void Initialize() + { + if (GlyphIndices.Length == 0) + { + throw new InvalidOperationException(); + } + + var glyphCount = GlyphIndices.Length; + + if (GlyphAdvances.Length > 0 && GlyphAdvances.Length != glyphCount) + { + throw new InvalidOperationException(); + } + + if (GlyphOffsets.Length > 0 && GlyphOffsets.Length != glyphCount) + { + throw new InvalidOperationException(); + } + + var platformRenderInterface = AvaloniaLocator.Current.GetService(); + + _glyphRunImpl = platformRenderInterface.CreateGlyphRun(this, out var width); + + var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; + + _bounds = new Rect(0, 0, width, height); + } + + void IDisposable.Dispose() + { + _glyphRunImpl?.Dispose(); + } + } +} diff --git a/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs b/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs new file mode 100644 index 0000000000..d0ea113a6f --- /dev/null +++ b/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs @@ -0,0 +1,47 @@ +namespace Avalonia.Media +{ + public class GlyphRunDrawing : Drawing + { + public static readonly StyledProperty ForegroundProperty = + AvaloniaProperty.Register(nameof(Foreground)); + + public static readonly StyledProperty GlyphRunProperty = + AvaloniaProperty.Register(nameof(GlyphRun)); + + public static readonly StyledProperty BaselineOriginProperty = + AvaloniaProperty.Register(nameof(BaselineOrigin)); + + public IBrush Foreground + { + get => GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + public GlyphRun GlyphRun + { + get => GetValue(GlyphRunProperty); + set => SetValue(GlyphRunProperty, value); + } + + public Point BaselineOrigin + { + get => GetValue(BaselineOriginProperty); + set => SetValue(BaselineOriginProperty, value); + } + + public override void Draw(DrawingContext context) + { + if (GlyphRun == null) + { + return; + } + + context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin); + } + + public override Rect GetBounds() + { + return GlyphRun?.Bounds ?? default; + } + } +} diff --git a/src/Avalonia.Visuals/Media/GlyphTypeface.cs b/src/Avalonia.Visuals/Media/GlyphTypeface.cs new file mode 100644 index 0000000000..35aa62aa65 --- /dev/null +++ b/src/Avalonia.Visuals/Media/GlyphTypeface.cs @@ -0,0 +1,125 @@ +using System; +using Avalonia.Platform; + +namespace Avalonia.Media +{ + public sealed class GlyphTypeface : IDisposable + { + public GlyphTypeface(Typeface typeface) + : this(FontManager.Current?.PlatformImpl.CreateGlyphTypeface(typeface)) + { + } + + public GlyphTypeface(IGlyphTypefaceImpl platformImpl) + { + PlatformImpl = platformImpl; + } + + public IGlyphTypefaceImpl PlatformImpl { get; } + + /// + /// Gets the font design units per em. + /// + public short DesignEmHeight => PlatformImpl.DesignEmHeight; + + /// + /// Gets the recommended distance above the baseline in design em size. + /// + public int Ascent => PlatformImpl.Ascent; + + /// + /// Gets the recommended distance under the baseline in design em size. + /// + public int Descent => PlatformImpl.Descent; + + /// + /// Gets the recommended additional space between two lines of text in design em size. + /// + public int LineGap => PlatformImpl.LineGap; + + /// + /// Gets the recommended line height. + /// + public int LineHeight => Descent - Ascent + LineGap; + + /// + /// Gets a value that indicates the distance of the underline from the baseline in design em size. + /// + public int UnderlinePosition => PlatformImpl.UnderlinePosition; + + /// + /// Gets a value that indicates the thickness of the underline in design em size. + /// + public int UnderlineThickness => PlatformImpl.UnderlineThickness; + + /// + /// Gets a value that indicates the distance of the strikethrough from the baseline in design em size. + /// + public int StrikethroughPosition => PlatformImpl.StrikethroughPosition; + + /// + /// Gets a value that indicates the thickness of the underline in design em size. + /// + public int StrikethroughThickness => PlatformImpl.StrikethroughThickness; + + /// + /// A value indicating whether all glyphs in the font have the same advancement. + /// + public bool IsFixedPitch => PlatformImpl.IsFixedPitch; + + /// + /// Returns an glyph index for the specified codepoint. + /// + /// + /// Returns a replacement glyph if a glyph isn't found. + /// + /// The codepoint. + /// + /// A glyph index. + /// + public ushort GetGlyph(uint codepoint) => PlatformImpl.GetGlyph(codepoint); + + /// + /// Tries to get an glyph index for specified codepoint. + /// + /// The codepoint. + /// A glyph index. + /// + /// true if an glyph index was found, false otherwise. + /// + public bool TryGetGlyph(uint codepoint, out ushort glyph) + { + glyph = PlatformImpl.GetGlyph(codepoint); + + return glyph != 0; + } + + /// + /// Returns an array of glyph indices. Codepoints that are not represented by the font are returned as 0. + /// + /// The codepoints to map. + /// + public ushort[] GetGlyphs(ReadOnlySpan codepoints) => PlatformImpl.GetGlyphs(codepoints); + + /// + /// Returns the glyph advance for the specified glyph. + /// + /// The glyph. + /// + /// The advance. + /// + public int GetGlyphAdvance(ushort glyph) => PlatformImpl.GetGlyphAdvance(glyph); + + /// + /// Returns an array of glyph advances in design em size. + /// + /// The glyph indices. + /// + public int[] GetGlyphAdvances(ReadOnlySpan glyphs) => PlatformImpl.GetGlyphAdvances(glyphs); + + void IDisposable.Dispose() + { + PlatformImpl?.Dispose(); + } + } +} diff --git a/src/Avalonia.Visuals/Media/GradientBrush.cs b/src/Avalonia.Visuals/Media/GradientBrush.cs index 8fd2dcf27f..99923b8e06 100644 --- a/src/Avalonia.Visuals/Media/GradientBrush.cs +++ b/src/Avalonia.Visuals/Media/GradientBrush.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Collections.Specialized; diff --git a/src/Avalonia.Visuals/Media/GradientSpreadMethod.cs b/src/Avalonia.Visuals/Media/GradientSpreadMethod.cs index f9506d78a0..327f16d994 100644 --- a/src/Avalonia.Visuals/Media/GradientSpreadMethod.cs +++ b/src/Avalonia.Visuals/Media/GradientSpreadMethod.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Media { public enum GradientSpreadMethod diff --git a/src/Avalonia.Visuals/Media/GradientStop.cs b/src/Avalonia.Visuals/Media/GradientStop.cs index 25a6eb10dc..de8c7fda2a 100644 --- a/src/Avalonia.Visuals/Media/GradientStop.cs +++ b/src/Avalonia.Visuals/Media/GradientStop.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Media { /// diff --git a/src/Avalonia.Visuals/Media/IBrush.cs b/src/Avalonia.Visuals/Media/IBrush.cs index bf096076fd..7756e94598 100644 --- a/src/Avalonia.Visuals/Media/IBrush.cs +++ b/src/Avalonia.Visuals/Media/IBrush.cs @@ -1,6 +1,3 @@ -// 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 System.ComponentModel; namespace Avalonia.Media diff --git a/src/Avalonia.Visuals/Media/IImage.cs b/src/Avalonia.Visuals/Media/IImage.cs new file mode 100644 index 0000000000..aff2a9ddf9 --- /dev/null +++ b/src/Avalonia.Visuals/Media/IImage.cs @@ -0,0 +1,29 @@ +using Avalonia.Platform; +using Avalonia.Visuals.Media.Imaging; + +namespace Avalonia.Media +{ + /// + /// Represents a raster or vector image. + /// + public interface IImage + { + /// + /// Gets the size of the image, in device independent pixels. + /// + Size Size { get; } + + /// + /// Draws the image to a . + /// + /// The drawing context. + /// The rect in the image to draw. + /// The rect in the output to draw to. + /// The bitmap interpolation mode. + void Draw( + DrawingContext context, + Rect sourceRect, + Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode); + } +} 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/ISolidColorBrush.cs b/src/Avalonia.Visuals/Media/ISolidColorBrush.cs index 73a9fb6795..d0ab00599b 100644 --- a/src/Avalonia.Visuals/Media/ISolidColorBrush.cs +++ b/src/Avalonia.Visuals/Media/ISolidColorBrush.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Media { /// diff --git a/src/Avalonia.Visuals/Media/ITileBrush.cs b/src/Avalonia.Visuals/Media/ITileBrush.cs index 5cffe02193..12fb221a89 100644 --- a/src/Avalonia.Visuals/Media/ITileBrush.cs +++ b/src/Avalonia.Visuals/Media/ITileBrush.cs @@ -1,7 +1,4 @@ -// 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.Visuals.Media.Imaging; +using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Media { 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/ImageBrush.cs b/src/Avalonia.Visuals/Media/ImageBrush.cs index 8b42a51d9f..19dcf00901 100644 --- a/src/Avalonia.Visuals/Media/ImageBrush.cs +++ b/src/Avalonia.Visuals/Media/ImageBrush.cs @@ -1,6 +1,3 @@ -// 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.Media.Imaging; using Avalonia.Media.Immutable; diff --git a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs index 8dd75d2374..86e2700c04 100644 --- a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs @@ -1,10 +1,8 @@ -// 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 System; using System.IO; using Avalonia.Platform; using Avalonia.Utilities; +using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Media.Imaging { @@ -13,6 +11,46 @@ namespace Avalonia.Media.Imaging /// public class Bitmap : IBitmap { + /// + /// Loads a Bitmap from a stream and decodes at the desired width. Aspect ratio is maintained. + /// This is more efficient than loading and then resizing. + /// + /// The stream to read the bitmap from. This can be any supported image format. + /// The desired width of the resulting bitmap. + /// The to use should any scaling be required. + /// An instance of the class. + public static Bitmap DecodeToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); + return new Bitmap(factory.LoadBitmapToWidth(stream, width, interpolationMode)); + } + + /// + /// Loads a Bitmap from a stream and decodes at the desired height. Aspect ratio is maintained. + /// This is more efficient than loading and then resizing. + /// + /// The stream to read the bitmap from. This can be any supported image format. + /// The desired height of the resulting bitmap. + /// The to use should any scaling be required. + /// An instance of the class. + public static Bitmap DecodeToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); + return new Bitmap(factory.LoadBitmapToHeight(stream, height, interpolationMode)); + } + + /// + /// Creates a Bitmap scaled to a specified size from the current bitmap. + /// + /// The destination size. + /// The to use should any scaling be required. + /// An instance of the class. + public Bitmap CreateScaledBitmap(PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); + return new Bitmap(factory.ResizeBitmap(PlatformImpl.Item, destinationSize, interpolationMode)); + } + /// /// Initializes a new instance of the class. /// @@ -41,7 +79,7 @@ namespace Avalonia.Media.Imaging { PlatformImpl = impl.Clone(); } - + /// /// Initializes a new instance of the class. /// @@ -50,7 +88,7 @@ namespace Avalonia.Media.Imaging { PlatformImpl = RefCountable.Create(impl); } - + /// public virtual void Dispose() { @@ -94,9 +132,28 @@ namespace Avalonia.Media.Imaging PlatformImpl.Item.Save(fileName); } + /// + /// Saves the bitmap to a stream. + /// + /// The stream. public void Save(Stream stream) { PlatformImpl.Item.Save(stream); } + + /// + void IImage.Draw( + DrawingContext context, + Rect sourceRect, + Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode) + { + context.PlatformImpl.DrawBitmap( + PlatformImpl, + 1, + sourceRect, + destRect, + bitmapInterpolationMode); + } } } diff --git a/src/Avalonia.Visuals/Media/Imaging/BitmapInterpolationMode.cs b/src/Avalonia.Visuals/Media/Imaging/BitmapInterpolationMode.cs index 7e6d330618..5fa48a5ec8 100644 --- a/src/Avalonia.Visuals/Media/Imaging/BitmapInterpolationMode.cs +++ b/src/Avalonia.Visuals/Media/Imaging/BitmapInterpolationMode.cs @@ -1,7 +1,4 @@ -// 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. - -namespace Avalonia.Visuals.Media.Imaging +namespace Avalonia.Visuals.Media.Imaging { /// /// Controls the performance and quality of bitmap scaling. diff --git a/src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs b/src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs new file mode 100644 index 0000000000..70823e114c --- /dev/null +++ b/src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs @@ -0,0 +1,96 @@ +using System; +using Avalonia.Visuals.Media.Imaging; + +namespace Avalonia.Media.Imaging +{ + /// + /// Crops a Bitmap. + /// + public class CroppedBitmap : AvaloniaObject, IImage, IAffectsRender, IDisposable + { + /// + /// Defines the property. + /// + public static readonly StyledProperty SourceProperty = + AvaloniaProperty.Register(nameof(Source)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty SourceRectProperty = + AvaloniaProperty.Register(nameof(SourceRect)); + + public event EventHandler Invalidated; + + static CroppedBitmap() + { + SourceRectProperty.Changed.AddClassHandler((x, e) => x.SourceRectChanged(e)); + SourceProperty.Changed.AddClassHandler((x, e) => x.SourceChanged(e)); + } + + /// + /// Gets or sets the source for the bitmap. + /// + public IImage Source + { + get => GetValue(SourceProperty); + set => SetValue(SourceProperty, value); + } + + /// + /// Gets or sets the rectangular area that the bitmap is cropped to. + /// + public PixelRect SourceRect + { + get => GetValue(SourceRectProperty); + set => SetValue(SourceRectProperty, value); + } + + public CroppedBitmap() + { + Source = null; + SourceRect = default; + } + + public CroppedBitmap(IImage source, PixelRect sourceRect) + { + Source = source; + SourceRect = sourceRect; + } + + private void SourceChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.NewValue == null) + return; + if (!(e.NewValue is IBitmap)) + throw new ArgumentException("Only IBitmap supported as source"); + Invalidated?.Invoke(this, e); + } + + private void SourceRectChanged(AvaloniaPropertyChangedEventArgs e) => Invalidated?.Invoke(this, e); + + public virtual void Dispose() + { + (Source as IBitmap)?.Dispose(); + } + + public Size Size { + get + { + if (Source == null) + return Size.Empty; + if (SourceRect.IsEmpty) + return Source.Size; + return SourceRect.Size.ToSizeWithDpi((Source as IBitmap).Dpi); + } + } + + public void Draw(DrawingContext context, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + { + if (Source == null) + return; + var topLeft = SourceRect.TopLeft.ToPointWithDpi((Source as IBitmap).Dpi); + Source.Draw(context, sourceRect.Translate(new Vector(topLeft.X, topLeft.Y)), destRect, bitmapInterpolationMode); + } + } +} diff --git a/src/Avalonia.Visuals/Media/Imaging/IBitmap.cs b/src/Avalonia.Visuals/Media/Imaging/IBitmap.cs index 90b13088e1..134bebc002 100644 --- a/src/Avalonia.Visuals/Media/Imaging/IBitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/IBitmap.cs @@ -1,6 +1,3 @@ -// 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 System; using System.IO; using Avalonia.Platform; @@ -11,7 +8,7 @@ namespace Avalonia.Media.Imaging /// /// Represents a bitmap image. /// - public interface IBitmap : IDisposable + public interface IBitmap : IImage, IDisposable { /// /// Gets the dots per inch (DPI) of the image. @@ -32,15 +29,6 @@ namespace Avalonia.Media.Imaging /// IRef PlatformImpl { get; } - /// - /// Gets the size of the image, in device independent pixels. - /// - /// - /// Note that Skia does not currently support reading the DPI of an image so this value - /// will equal on Skia. - /// - Size Size { get; } - /// /// Saves the bitmap to a file. /// diff --git a/src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs index d0b85ef14e..928b188f8c 100644 --- a/src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Platform; using Avalonia.Rendering; diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs index 4b3bd640cb..e586eaf3a9 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs index b55ca251a6..010184ad3b 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs @@ -1,12 +1,11 @@ -// 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 System; namespace Avalonia.Media.Immutable { /// /// Fills an area with a solid color. /// - public readonly struct ImmutableSolidColorBrush : ISolidColorBrush + public readonly struct ImmutableSolidColorBrush : ISolidColorBrush, IEquatable { /// /// Initializes a new instance of the class. @@ -47,6 +46,35 @@ namespace Avalonia.Media.Immutable /// public double Opacity { get; } + public bool Equals(ImmutableSolidColorBrush other) + { + // ReSharper disable once CompareOfFloatsByEqualityOperator + return Color == other.Color && Opacity == other.Opacity; + } + + public override bool Equals(object obj) + { + return obj is ImmutableSolidColorBrush other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return (Color.GetHashCode() * 397) ^ Opacity.GetHashCode(); + } + } + + public static bool operator ==(ImmutableSolidColorBrush left, ImmutableSolidColorBrush right) + { + return left.Equals(right); + } + + public static bool operator !=(ImmutableSolidColorBrush left, ImmutableSolidColorBrush right) + { + return !left.Equals(right); + } + /// /// Returns a string representation of the brush. /// diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableTextDecoration.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableTextDecoration.cs new file mode 100644 index 0000000000..cb83cfec64 --- /dev/null +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableTextDecoration.cs @@ -0,0 +1,53 @@ +namespace Avalonia.Media.Immutable +{ + /// + /// An immutable representation of a . + /// + public class ImmutableTextDecoration + { + public ImmutableTextDecoration(TextDecorationLocation location, ImmutablePen pen, + TextDecorationUnit penThicknessUnit, + double penOffset, TextDecorationUnit penOffsetUnit) + { + Location = location; + Pen = pen; + PenThicknessUnit = penThicknessUnit; + PenOffset = penOffset; + PenOffsetUnit = penOffsetUnit; + } + + /// + /// Gets or sets the location. + /// + /// + /// The location. + /// + public TextDecorationLocation Location { get; } + + /// + /// Gets or sets the pen. + /// + /// + /// The pen. + /// + public ImmutablePen Pen { get; } + + /// + /// Gets the units in which the Thickness of the text decoration's is expressed. + /// + public TextDecorationUnit PenThicknessUnit { get; } + + /// + /// Gets or sets the pen offset. + /// + /// + /// The pen offset. + /// + public double PenOffset { get; } + + /// + /// Gets the units in which the value is expressed. + /// + public TextDecorationUnit PenOffsetUnit { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs index c3dd159d04..fd4d921516 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs @@ -1,7 +1,4 @@ -// 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.Visuals.Media.Imaging; +using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Media.Immutable { diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs index f1f61a6e65..4c6aae08ab 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs @@ -1,7 +1,4 @@ -// 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.Visuals.Media.Imaging; +using Avalonia.Visuals.Media.Imaging; using Avalonia.VisualTree; namespace Avalonia.Media.Immutable diff --git a/src/Avalonia.Visuals/Media/LineGeometry.cs b/src/Avalonia.Visuals/Media/LineGeometry.cs index 90577dabd8..94b638114b 100644 --- a/src/Avalonia.Visuals/Media/LineGeometry.cs +++ b/src/Avalonia.Visuals/Media/LineGeometry.cs @@ -1,6 +1,3 @@ -// 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.Platform; namespace Avalonia.Media diff --git a/src/Avalonia.Visuals/Media/LineSegment.cs b/src/Avalonia.Visuals/Media/LineSegment.cs index d81f67d99f..49d9ebeb72 100644 --- a/src/Avalonia.Visuals/Media/LineSegment.cs +++ b/src/Avalonia.Visuals/Media/LineSegment.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Media { public sealed class LineSegment : PathSegment @@ -27,5 +24,8 @@ namespace Avalonia.Media { ctx.LineTo(Point); } + + public override string ToString() + => $"L {Point}"; } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/LinearGradientBrush.cs b/src/Avalonia.Visuals/Media/LinearGradientBrush.cs index 14adc0e0cd..a51adf0949 100644 --- a/src/Avalonia.Visuals/Media/LinearGradientBrush.cs +++ b/src/Avalonia.Visuals/Media/LinearGradientBrush.cs @@ -1,6 +1,3 @@ -// 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.Media.Immutable; namespace Avalonia.Media diff --git a/src/Avalonia.Visuals/Media/MatrixTransform.cs b/src/Avalonia.Visuals/Media/MatrixTransform.cs index 247a26dac1..4e60e1e290 100644 --- a/src/Avalonia.Visuals/Media/MatrixTransform.cs +++ b/src/Avalonia.Visuals/Media/MatrixTransform.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.VisualTree; diff --git a/src/Avalonia.Visuals/Media/MediaExtensions.cs b/src/Avalonia.Visuals/Media/MediaExtensions.cs index 95d17b454e..d5995ff683 100644 --- a/src/Avalonia.Visuals/Media/MediaExtensions.cs +++ b/src/Avalonia.Visuals/Media/MediaExtensions.cs @@ -1,7 +1,5 @@ -// 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 System; +using Avalonia.Utilities; namespace Avalonia.Media { @@ -16,24 +14,82 @@ namespace Avalonia.Media /// The stretch mode. /// The size of the destination viewport. /// The size of the source. + /// The stretch direction. /// A vector with the X and Y scaling factors. - public static Vector CalculateScaling(this Stretch stretch, Size destinationSize, Size sourceSize) + public static Vector CalculateScaling( + this Stretch stretch, + Size destinationSize, + Size sourceSize, + StretchDirection stretchDirection = StretchDirection.Both) { - double scaleX = 1; - double scaleY = 1; + var scaleX = 1.0; + var scaleY = 1.0; + + bool isConstrainedWidth = !double.IsPositiveInfinity(destinationSize.Width); + bool isConstrainedHeight = !double.IsPositiveInfinity(destinationSize.Height); - if (stretch != Stretch.None) + if ((stretch == Stretch.Uniform || stretch == Stretch.UniformToFill || stretch == Stretch.Fill) + && (isConstrainedWidth || isConstrainedHeight)) { - scaleX = destinationSize.Width / sourceSize.Width; - scaleY = destinationSize.Height / sourceSize.Height; + // Compute scaling factors for both axes + scaleX = MathUtilities.IsZero(sourceSize.Width) ? 0.0 : destinationSize.Width / sourceSize.Width; + scaleY = MathUtilities.IsZero(sourceSize.Height) ? 0.0 : destinationSize.Height / sourceSize.Height; + + if (!isConstrainedWidth) + { + scaleX = scaleY; + } + else if (!isConstrainedHeight) + { + scaleY = scaleX; + } + else + { + // If not preserving aspect ratio, then just apply transform to fit + switch (stretch) + { + case Stretch.Uniform: + // Find minimum scale that we use for both axes + double minscale = scaleX < scaleY ? scaleX : scaleY; + scaleX = scaleY = minscale; + break; - switch (stretch) + case Stretch.UniformToFill: + // Find maximum scale that we use for both axes + double maxscale = scaleX > scaleY ? scaleX : scaleY; + scaleX = scaleY = maxscale; + break; + + case Stretch.Fill: + // We already computed the fill scale factors above, so just use them + break; + } + } + + // Apply stretch direction by bounding scales. + // In the uniform case, scaleX=scaleY, so this sort of clamping will maintain aspect ratio + // In the uniform fill case, we have the same result too. + // In the fill case, note that we change aspect ratio, but that is okay + switch (stretchDirection) { - case Stretch.Uniform: - scaleX = scaleY = Math.Min(scaleX, scaleY); + case StretchDirection.UpOnly: + if (scaleX < 1.0) + scaleX = 1.0; + if (scaleY < 1.0) + scaleY = 1.0; + break; + + case StretchDirection.DownOnly: + if (scaleX > 1.0) + scaleX = 1.0; + if (scaleY > 1.0) + scaleY = 1.0; + break; + + case StretchDirection.Both: break; - case Stretch.UniformToFill: - scaleX = scaleY = Math.Max(scaleX, scaleY); + + default: break; } } @@ -47,10 +103,15 @@ namespace Avalonia.Media /// The stretch mode. /// The size of the destination viewport. /// The size of the source. + /// The stretch direction. /// The size of the stretched source. - public static Size CalculateSize(this Stretch stretch, Size destinationSize, Size sourceSize) + public static Size CalculateSize( + this Stretch stretch, + Size destinationSize, + Size sourceSize, + StretchDirection stretchDirection = StretchDirection.Both) { - return sourceSize * stretch.CalculateScaling(destinationSize, sourceSize); + return sourceSize * stretch.CalculateScaling(destinationSize, sourceSize, stretchDirection); } } } diff --git a/src/Avalonia.Visuals/Media/PathFigure.cs b/src/Avalonia.Visuals/Media/PathFigure.cs index 6a438a85ee..d0eb67ba39 100644 --- a/src/Avalonia.Visuals/Media/PathFigure.cs +++ b/src/Avalonia.Visuals/Media/PathFigure.cs @@ -1,6 +1,3 @@ -// 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.Metadata; namespace Avalonia.Media @@ -98,5 +95,8 @@ namespace Avalonia.Media } private PathSegments _segments; + + public override string ToString() + => $"M {StartPoint} {string.Join(" ", _segments)}{(IsClosed ? "Z" : "")}"; } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/PathGeometry.cs b/src/Avalonia.Visuals/Media/PathGeometry.cs index 3a91c924d2..fbc29aedc8 100644 --- a/src/Avalonia.Visuals/Media/PathGeometry.cs +++ b/src/Avalonia.Visuals/Media/PathGeometry.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Collections; using Avalonia.Metadata; @@ -112,5 +109,9 @@ namespace Avalonia.Media () => InvalidateGeometry()); _figuresPropertiesObserver = figures?.TrackItemPropertyChanged(_ => InvalidateGeometry()); } + + + public override string ToString() + => $"{(FillRule != FillRule.EvenOdd ? "F1 " : "")}{(string.Join(" ", Figures))}"; } } diff --git a/src/Avalonia.Visuals/Media/PathGeometryCollections.cs b/src/Avalonia.Visuals/Media/PathGeometryCollections.cs index e161cfc32d..61099a0956 100644 --- a/src/Avalonia.Visuals/Media/PathGeometryCollections.cs +++ b/src/Avalonia.Visuals/Media/PathGeometryCollections.cs @@ -1,6 +1,3 @@ -// 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.Collections; namespace Avalonia.Media diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index e8f149df11..9fefcb6645 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Globalization; diff --git a/src/Avalonia.Visuals/Media/PathSegment.cs b/src/Avalonia.Visuals/Media/PathSegment.cs index 4ee2211763..89a33815ae 100644 --- a/src/Avalonia.Visuals/Media/PathSegment.cs +++ b/src/Avalonia.Visuals/Media/PathSegment.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Media { public abstract class PathSegment : AvaloniaObject diff --git a/src/Avalonia.Visuals/Media/Pen.cs b/src/Avalonia.Visuals/Media/Pen.cs index b88fae28ff..eb44b3f21e 100644 --- a/src/Avalonia.Visuals/Media/Pen.cs +++ b/src/Avalonia.Visuals/Media/Pen.cs @@ -1,8 +1,4 @@ -// 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 System; -using System.Collections.Generic; using Avalonia.Media.Immutable; using Avalonia.Utilities; @@ -197,7 +193,15 @@ namespace Avalonia.Media protected static void AffectsRender(params AvaloniaProperty[] properties) where T : Pen { - void Invalidate(AvaloniaPropertyChangedEventArgs e) + static void Invalidate(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is T sender) + { + sender.RaiseInvalidated(EventArgs.Empty); + } + } + + static void InvalidateAndSubscribe(AvaloniaPropertyChangedEventArgs e) { if (e.Sender is T sender) { @@ -223,7 +227,14 @@ namespace Avalonia.Media foreach (var property in properties) { - property.Changed.Subscribe(Invalidate); + if (property.CanValueAffectRender()) + { + property.Changed.Subscribe(e => InvalidateAndSubscribe(e)); + } + else + { + property.Changed.Subscribe(e => Invalidate(e)); + } } } diff --git a/src/Avalonia.Visuals/Media/PixelPoint.cs b/src/Avalonia.Visuals/Media/PixelPoint.cs index 5384a40be9..7d62d7bc23 100644 --- a/src/Avalonia.Visuals/Media/PixelPoint.cs +++ b/src/Avalonia.Visuals/Media/PixelPoint.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Globalization; using Avalonia.Utilities; diff --git a/src/Avalonia.Visuals/Media/PixelRect.cs b/src/Avalonia.Visuals/Media/PixelRect.cs index a024e1af11..32a728475f 100644 --- a/src/Avalonia.Visuals/Media/PixelRect.cs +++ b/src/Avalonia.Visuals/Media/PixelRect.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Globalization; using Avalonia.Utilities; @@ -380,7 +377,7 @@ namespace Avalonia /// The device-independent rect. public static PixelRect FromRect(Rect rect, double scale) => new PixelRect( PixelPoint.FromPoint(rect.Position, scale), - PixelSize.FromSize(rect.Size, scale)); + FromPointCeiling(rect.BottomRight, new Vector(scale, scale))); /// /// Converts a to device pixels using the specified scaling factor. @@ -390,7 +387,7 @@ namespace Avalonia /// The device-independent point. public static PixelRect FromRect(Rect rect, Vector scale) => new PixelRect( PixelPoint.FromPoint(rect.Position, scale), - PixelSize.FromSize(rect.Size, scale)); + FromPointCeiling(rect.BottomRight, scale)); /// /// Converts a to device pixels using the specified dots per inch (DPI). @@ -400,7 +397,7 @@ namespace Avalonia /// The device-independent point. public static PixelRect FromRectWithDpi(Rect rect, double dpi) => new PixelRect( PixelPoint.FromPointWithDpi(rect.Position, dpi), - PixelSize.FromSizeWithDpi(rect.Size, dpi)); + FromPointCeiling(rect.BottomRight, new Vector(dpi / 96, dpi / 96))); /// /// Converts a to device pixels using the specified dots per inch (DPI). @@ -410,7 +407,7 @@ namespace Avalonia /// The device-independent point. public static PixelRect FromRectWithDpi(Rect rect, Vector dpi) => new PixelRect( PixelPoint.FromPointWithDpi(rect.Position, dpi), - PixelSize.FromSizeWithDpi(rect.Size, dpi)); + FromPointCeiling(rect.BottomRight, dpi / 96)); /// /// Returns the string representation of the rectangle. @@ -444,5 +441,12 @@ namespace Avalonia ); } } + + private static PixelPoint FromPointCeiling(Point point, Vector scale) + { + return new PixelPoint( + (int)Math.Ceiling(point.X * scale.X), + (int)Math.Ceiling(point.Y * scale.Y)); + } } } diff --git a/src/Avalonia.Visuals/Media/PixelSize.cs b/src/Avalonia.Visuals/Media/PixelSize.cs index a49aa82c0d..530b01ed90 100644 --- a/src/Avalonia.Visuals/Media/PixelSize.cs +++ b/src/Avalonia.Visuals/Media/PixelSize.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Globalization; using Avalonia.Utilities; diff --git a/src/Avalonia.Visuals/Media/PixelVector.cs b/src/Avalonia.Visuals/Media/PixelVector.cs index 4a623e3bc2..f32e9c0178 100644 --- a/src/Avalonia.Visuals/Media/PixelVector.cs +++ b/src/Avalonia.Visuals/Media/PixelVector.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Globalization; using Avalonia.Animation.Animators; diff --git a/src/Avalonia.Visuals/Media/PolylineGeometry.cs b/src/Avalonia.Visuals/Media/PolylineGeometry.cs index 5ed16ca957..94c809d432 100644 --- a/src/Avalonia.Visuals/Media/PolylineGeometry.cs +++ b/src/Avalonia.Visuals/Media/PolylineGeometry.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using Avalonia.Collections; @@ -23,7 +20,7 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly AvaloniaProperty IsFilledProperty = + public static readonly StyledProperty IsFilledProperty = AvaloniaProperty.Register(nameof(IsFilled)); private Points _points; diff --git a/src/Avalonia.Visuals/Media/QuadraticBezierSegment .cs b/src/Avalonia.Visuals/Media/QuadraticBezierSegment .cs index 31c364edf4..f1a0ccaaa0 100644 --- a/src/Avalonia.Visuals/Media/QuadraticBezierSegment .cs +++ b/src/Avalonia.Visuals/Media/QuadraticBezierSegment .cs @@ -42,5 +42,8 @@ namespace Avalonia.Media { ctx.QuadraticBezierTo(Point1, Point2); } + + public override string ToString() + => $"Q {Point1} {Point2}"; } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/RadialGradientBrush.cs b/src/Avalonia.Visuals/Media/RadialGradientBrush.cs index 589cd83ca1..ee42c3d7e4 100644 --- a/src/Avalonia.Visuals/Media/RadialGradientBrush.cs +++ b/src/Avalonia.Visuals/Media/RadialGradientBrush.cs @@ -1,6 +1,3 @@ -// 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.Media.Immutable; namespace Avalonia.Media diff --git a/src/Avalonia.Visuals/Media/RectangleGeometry.cs b/src/Avalonia.Visuals/Media/RectangleGeometry.cs index 9250500644..743e103a88 100644 --- a/src/Avalonia.Visuals/Media/RectangleGeometry.cs +++ b/src/Avalonia.Visuals/Media/RectangleGeometry.cs @@ -1,6 +1,3 @@ -// 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.Platform; namespace Avalonia.Media diff --git a/src/Avalonia.Visuals/Media/RenderOptions.cs b/src/Avalonia.Visuals/Media/RenderOptions.cs index 180863f9e8..7abbc8a656 100644 --- a/src/Avalonia.Visuals/Media/RenderOptions.cs +++ b/src/Avalonia.Visuals/Media/RenderOptions.cs @@ -1,7 +1,4 @@ -// 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.Visuals.Media.Imaging; +using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Media { diff --git a/src/Avalonia.Visuals/Media/RotateTransform.cs b/src/Avalonia.Visuals/Media/RotateTransform.cs index c3babcfbab..653d38eb45 100644 --- a/src/Avalonia.Visuals/Media/RotateTransform.cs +++ b/src/Avalonia.Visuals/Media/RotateTransform.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.VisualTree; diff --git a/src/Avalonia.Visuals/Media/ScaleTransform.cs b/src/Avalonia.Visuals/Media/ScaleTransform.cs index fd301802e0..259b23cbd2 100644 --- a/src/Avalonia.Visuals/Media/ScaleTransform.cs +++ b/src/Avalonia.Visuals/Media/ScaleTransform.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.VisualTree; diff --git a/src/Avalonia.Visuals/Media/SkewTransform.cs b/src/Avalonia.Visuals/Media/SkewTransform.cs index 405e3947e8..a96710e331 100644 --- a/src/Avalonia.Visuals/Media/SkewTransform.cs +++ b/src/Avalonia.Visuals/Media/SkewTransform.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using Avalonia.VisualTree; namespace Avalonia.Media diff --git a/src/Avalonia.Visuals/Media/SolidColorBrush.cs b/src/Avalonia.Visuals/Media/SolidColorBrush.cs index bacb9e4ad7..8e30880489 100644 --- a/src/Avalonia.Visuals/Media/SolidColorBrush.cs +++ b/src/Avalonia.Visuals/Media/SolidColorBrush.cs @@ -1,6 +1,3 @@ -// 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.Animation; using Avalonia.Animation.Animators; using Avalonia.Media.Immutable; diff --git a/src/Avalonia.Visuals/Media/StreamGeometry.cs b/src/Avalonia.Visuals/Media/StreamGeometry.cs index d3cb788486..be914cc7b2 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometry.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometry.cs @@ -1,6 +1,3 @@ -// 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.Platform; namespace Avalonia.Media diff --git a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs index 9df67c226e..0bfd774c79 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs @@ -1,6 +1,3 @@ -// 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.Platform; namespace Avalonia.Media diff --git a/src/Avalonia.Visuals/Media/Stretch.cs b/src/Avalonia.Visuals/Media/Stretch.cs index 3265d6eaa2..60ec8d4c6e 100644 --- a/src/Avalonia.Visuals/Media/Stretch.cs +++ b/src/Avalonia.Visuals/Media/Stretch.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Media { /// diff --git a/src/Avalonia.Visuals/Media/StretchDirection.cs b/src/Avalonia.Visuals/Media/StretchDirection.cs new file mode 100644 index 0000000000..a4be26f6cd --- /dev/null +++ b/src/Avalonia.Visuals/Media/StretchDirection.cs @@ -0,0 +1,25 @@ +namespace Avalonia.Media +{ + /// + /// Describes the type of scaling that can be used when scaling content. + /// + public enum StretchDirection + { + /// + /// Only scales the content upwards when the content is smaller than the available space. + /// If the content is larger, no scaling downwards is done. + /// + UpOnly, + + /// + /// Only scales the content downwards when the content is larger than the available space. + /// If the content is smaller, no scaling upwards is done. + /// + DownOnly, + + /// + /// Always stretches to fit the available space according to the stretch mode. + /// + Both, + } +} diff --git a/src/Avalonia.Visuals/Media/SweepDirection.cs b/src/Avalonia.Visuals/Media/SweepDirection.cs index be1f74ee90..67b79f9dfd 100644 --- a/src/Avalonia.Visuals/Media/SweepDirection.cs +++ b/src/Avalonia.Visuals/Media/SweepDirection.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Media { /// diff --git a/src/Avalonia.Visuals/Media/TextAlignment.cs b/src/Avalonia.Visuals/Media/TextAlignment.cs index 5dd152c4a5..b1a394e157 100644 --- a/src/Avalonia.Visuals/Media/TextAlignment.cs +++ b/src/Avalonia.Visuals/Media/TextAlignment.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Media { /// diff --git a/src/Avalonia.Visuals/Media/TextDecoration.cs b/src/Avalonia.Visuals/Media/TextDecoration.cs new file mode 100644 index 0000000000..a83555946b --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextDecoration.cs @@ -0,0 +1,103 @@ +using Avalonia.Media.Immutable; + +namespace Avalonia.Media +{ + /// + /// Represents a text decoration, which is a visual ornamentation that is added to text (such as an underline). + /// + public class TextDecoration : AvaloniaObject + { + /// + /// Defines the property. + /// + public static readonly StyledProperty LocationProperty = + AvaloniaProperty.Register(nameof(Location)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PenProperty = + AvaloniaProperty.Register(nameof(Pen)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PenThicknessUnitProperty = + AvaloniaProperty.Register(nameof(PenThicknessUnit)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PenOffsetProperty = + AvaloniaProperty.Register(nameof(PenOffset)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PenOffsetUnitProperty = + AvaloniaProperty.Register(nameof(PenOffsetUnit)); + + /// + /// Gets or sets the location. + /// + /// + /// The location. + /// + public TextDecorationLocation Location + { + get => GetValue(LocationProperty); + set => SetValue(LocationProperty, value); + } + + /// + /// Gets or sets the pen. + /// + /// + /// The pen. + /// + public IPen Pen + { + get => GetValue(PenProperty); + set => SetValue(PenProperty, value); + } + + /// + /// Gets the units in which the Thickness of the text decoration's is expressed. + /// + public TextDecorationUnit PenThicknessUnit + { + get => GetValue(PenThicknessUnitProperty); + set => SetValue(PenThicknessUnitProperty, value); + } + + /// + /// Gets or sets the pen offset. + /// + /// + /// The pen offset. + /// + public double PenOffset + { + get => GetValue(PenOffsetProperty); + set => SetValue(PenOffsetProperty, value); + } + + /// + /// Gets the units in which the value is expressed. + /// + public TextDecorationUnit PenOffsetUnit + { + get => GetValue(PenOffsetUnitProperty); + set => SetValue(PenOffsetUnitProperty, value); + } + + /// + /// Creates an immutable clone of the . + /// + /// The immutable clone. + public ImmutableTextDecoration ToImmutable() + { + return new ImmutableTextDecoration(Location, Pen?.ToImmutable(), PenThicknessUnit, PenOffset, PenOffsetUnit); + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextDecorationCollection.cs b/src/Avalonia.Visuals/Media/TextDecorationCollection.cs new file mode 100644 index 0000000000..21e2e2484c --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextDecorationCollection.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using Avalonia.Collections; +using Avalonia.Media.Immutable; +using Avalonia.Utilities; + +namespace Avalonia.Media +{ + /// + /// A collection that holds objects. + /// + 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. + /// + /// The string. + /// The . + public static TextDecorationCollection Parse(string s) + { + var locations = new List(); + + using (var tokenizer = new StringTokenizer(s, ',', "Invalid text decoration.")) + { + while (tokenizer.TryReadString(out var name)) + { + var location = GetTextDecorationLocation(name); + + if (locations.Contains(location)) + { + throw new ArgumentException("Text decoration already specified.", nameof(s)); + } + + locations.Add(location); + } + } + + var textDecorations = new TextDecorationCollection(); + + foreach (var textDecorationLocation in locations) + { + textDecorations.Add(new TextDecoration { Location = textDecorationLocation }); + } + + return textDecorations; + } + + /// + /// Parses a string. + /// + /// The string. + /// The . + private static TextDecorationLocation GetTextDecorationLocation(string s) + { + if (Enum.TryParse(s,true, out var location)) + { + return location; + } + + throw new ArgumentException("Could not parse text decoration.", nameof(s)); + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextDecorationLocation.cs b/src/Avalonia.Visuals/Media/TextDecorationLocation.cs new file mode 100644 index 0000000000..0082ea8e62 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextDecorationLocation.cs @@ -0,0 +1,28 @@ +namespace Avalonia.Media +{ + /// + /// Specifies the vertical position of a object. + /// + public enum TextDecorationLocation + { + /// + /// The underline position. + /// + Underline = 0, + + /// + /// The over line position. + /// + Overline = 1, + + /// + /// The strikethrough position. + /// + Strikethrough = 2, + + /// + /// The baseline position. + /// + Baseline = 3, + } +} diff --git a/src/Avalonia.Visuals/Media/TextDecorationUnit.cs b/src/Avalonia.Visuals/Media/TextDecorationUnit.cs new file mode 100644 index 0000000000..dde425bb94 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextDecorationUnit.cs @@ -0,0 +1,26 @@ +namespace Avalonia.Media +{ + /// + /// Specifies the unit type of either a or a thickness value. + /// + public enum TextDecorationUnit + { + /// + /// A unit value that is relative to the font used for the . + /// If the decoration spans multiple fonts, an average recommended value is calculated. + /// This is the default value. + /// + FontRecommended, + + /// + /// A unit value that is relative to the em size of the font. + /// The value of the offset or thickness is equal to the offset or thickness value multiplied by the font em size. + /// + FontRenderingEmSize, + + /// + /// A unit value that is expressed in pixels. + /// + Pixel + } +} diff --git a/src/Avalonia.Visuals/Media/TextDecorations.cs b/src/Avalonia.Visuals/Media/TextDecorations.cs new file mode 100644 index 0000000000..006075dde1 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextDecorations.cs @@ -0,0 +1,63 @@ +namespace Avalonia.Media +{ + /// + /// Defines a set of commonly used text decorations. + /// + public static class TextDecorations + { + static TextDecorations() + { + Underline = new TextDecorationCollection + { + new TextDecoration + { + Location = TextDecorationLocation.Underline + } + }; + + Strikethrough = new TextDecorationCollection + { + new TextDecoration + { + Location = TextDecorationLocation.Strikethrough + } + }; + + Overline = new TextDecorationCollection + { + new TextDecoration + { + Location = TextDecorationLocation.Overline + } + }; + + Baseline = new TextDecorationCollection + { + new TextDecoration + { + Location = TextDecorationLocation.Baseline + } + }; + } + + /// + /// Gets a containing an underline. + /// + public static TextDecorationCollection Underline { get; } + + /// + /// Gets a containing a strikethrough. + /// + public static TextDecorationCollection Strikethrough { get; } + + /// + /// Gets a containing an overline. + /// + public static TextDecorationCollection Overline { get; } + + /// + /// Gets a containing a baseline. + /// + public static TextDecorationCollection Baseline { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs new file mode 100644 index 0000000000..4903342cea --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs @@ -0,0 +1,22 @@ +using Avalonia.Platform; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// A text run that supports drawing content. + /// + public abstract class DrawableTextRun : TextRun + { + /// + /// Gets the bounds. + /// + public abstract Rect Bounds { get; } + + /// + /// Draws the at the given origin. + /// + /// The drawing context. + /// The origin. + public abstract void Draw(IDrawingContextImpl drawingContext, Point origin); + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/FontMetrics.cs b/src/Avalonia.Visuals/Media/TextFormatting/FontMetrics.cs new file mode 100644 index 0000000000..dd91dc04bd --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/FontMetrics.cs @@ -0,0 +1,71 @@ +namespace Avalonia.Media.TextFormatting +{ + /// + /// A metric that holds information about font specific measurements. + /// + public readonly struct FontMetrics + { + public FontMetrics(Typeface typeface, double fontSize) + { + var glyphTypeface = typeface.GlyphTypeface; + + var scale = fontSize / glyphTypeface.DesignEmHeight; + + Ascent = glyphTypeface.Ascent * scale; + + Descent = glyphTypeface.Descent * scale; + + LineGap = glyphTypeface.LineGap * scale; + + LineHeight = Descent - Ascent + LineGap; + + UnderlineThickness = glyphTypeface.UnderlineThickness * scale; + + UnderlinePosition = glyphTypeface.UnderlinePosition * scale; + + StrikethroughThickness = glyphTypeface.StrikethroughThickness * scale; + + StrikethroughPosition = glyphTypeface.StrikethroughPosition * scale; + } + + /// + /// Gets the recommended distance above the baseline. + /// + public double Ascent { get; } + + /// + /// Gets the recommended distance under the baseline. + /// + public double Descent { get; } + + /// + /// Gets the recommended additional space between two lines of text. + /// + public double LineGap { get; } + + /// + /// Gets the estimated line height. + /// + public double LineHeight { get; } + + /// + /// Gets a value that indicates the thickness of the underline. + /// + public double UnderlineThickness { get; } + + /// + /// Gets a value that indicates the distance of the underline from the baseline. + /// + public double UnderlinePosition { get; } + + /// + /// Gets a value that indicates the thickness of the underline. + /// + public double StrikethroughThickness { get; } + + /// + /// Gets a value that indicates the distance of the strikethrough from the baseline. + /// + public double StrikethroughPosition { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ITextSource.cs b/src/Avalonia.Visuals/Media/TextFormatting/ITextSource.cs new file mode 100644 index 0000000000..0f9994bc65 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/ITextSource.cs @@ -0,0 +1,15 @@ +namespace Avalonia.Media.TextFormatting +{ + /// + /// Produces objects that are used by the . + /// + public interface ITextSource + { + /// + /// Gets a for specified text source index. + /// + /// The text source index. + /// The text run. + TextRun GetTextRun(int textSourceIndex); + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextRun.cs new file mode 100644 index 0000000000..00f9b918cb --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextRun.cs @@ -0,0 +1,212 @@ +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 new file mode 100644 index 0000000000..f84e45d4c6 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs @@ -0,0 +1,395 @@ +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 new file mode 100644 index 0000000000..11d241bc34 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextLine.cs @@ -0,0 +1,259 @@ +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 new file mode 100644 index 0000000000..d9b27958ab --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs @@ -0,0 +1,21 @@ +using Avalonia.Utility; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// A text run that holds text characters. + /// + public class TextCharacters : TextRun + { + protected TextCharacters() + { + + } + + public TextCharacters(ReadOnlySlice text, TextStyle style) + { + Text = text; + Style = style; + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextEndOfLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextEndOfLine.cs new file mode 100644 index 0000000000..fd71fb53e7 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextEndOfLine.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Media.TextFormatting +{ + /// + /// A text run that indicates the end of a line. + /// + public class TextEndOfLine : TextRun + { + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextEndOfParagraph.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextEndOfParagraph.cs new file mode 100644 index 0000000000..682fd930f6 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextEndOfParagraph.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Media.TextFormatting +{ + /// + /// A text run that indicates the end of a paragraph. + /// + public class TextEndOfParagraph : TextEndOfLine + { + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormat.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormat.cs new file mode 100644 index 0000000000..18dd6c7c10 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormat.cs @@ -0,0 +1,71 @@ +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 new file mode 100644 index 0000000000..7da39dc5dc --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs @@ -0,0 +1,186 @@ +using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.Utility; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Represents a base class for text formatting. + /// + public abstract class TextFormatter + { + /// + /// Gets the current that is used for non complex text formatting. + /// + public static TextFormatter Current + { + get + { + var current = AvaloniaLocator.Current.GetService(); + + if (current != null) + { + return current; + } + + current = new SimpleTextFormatter(); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(current); + + return current; + } + } + + /// + /// Formats a text line. + /// + /// The text source. + /// The first character index to start the text line from. + /// 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 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; + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs new file mode 100644 index 0000000000..0292398782 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -0,0 +1,394 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Media.Immutable; +using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.Platform; +using Avalonia.Utilities; +using Avalonia.Utility; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Represents a multi line text layout. + /// + public class TextLayout + { + private static readonly ReadOnlySlice s_empty = new ReadOnlySlice(new[] { '\u200B' }); + + private readonly ReadOnlySlice _text; + private readonly TextParagraphProperties _paragraphProperties; + private readonly IReadOnlyList _textStyleOverrides; + + /// + /// Initializes a new instance of the class. + /// + /// The text. + /// The typeface. + /// Size of the font. + /// The foreground. + /// The text alignment. + /// The text wrapping. + /// The text trimming. + /// The text decorations. + /// The maximum width. + /// The maximum height. + /// The maximum number of text lines. + /// The text style overrides. + public TextLayout( + string text, + Typeface typeface, + double fontSize, + IBrush foreground, + TextAlignment textAlignment = TextAlignment.Left, + TextWrapping textWrapping = TextWrapping.NoWrap, + TextTrimming textTrimming = TextTrimming.None, + TextDecorationCollection textDecorations = null, + double maxWidth = double.PositiveInfinity, + double maxHeight = double.PositiveInfinity, + 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()); + + _textStyleOverrides = textStyleOverrides; + + MaxWidth = maxWidth; + + MaxHeight = maxHeight; + + MaxLines = maxLines; + + UpdateLayout(); + } + + /// + /// Gets the maximum width. + /// + public double MaxWidth { get; } + + + /// + /// Gets the maximum height. + /// + public double MaxHeight { get; } + + + /// + /// Gets the maximum number of text lines. + /// + public double MaxLines { get; } + + /// + /// Gets the text lines. + /// + /// + /// The text lines. + /// + public IReadOnlyList TextLines { get; private set; } + + /// + /// Gets the bounds of the layout. + /// + /// + /// The bounds. + /// + public Rect Bounds { get; private set; } + + /// + /// Draws the text layout. + /// + /// The drawing context. + /// The origin. + public void Draw(IDrawingContextImpl context, Point origin) + { + if (!TextLines.Any()) + { + return; + } + + var currentY = origin.Y; + + foreach (var textLine in TextLines) + { + textLine.Draw(context, new Point(origin.X, currentY)); + + currentY += textLine.LineMetrics.Size.Height; + } + } + + /// + /// Creates the default that are used by the . + /// + /// The typeface. + /// The font size. + /// The foreground. + /// The text alignment. + /// The text wrapping. + /// The text trimming. + /// The text decorations. + /// + private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize, + IBrush foreground, TextAlignment textAlignment, TextWrapping textWrapping, TextTrimming textTrimming, + ImmutableTextDecoration[] textDecorations) + { + var textRunStyle = new TextStyle(typeface, fontSize, foreground, textDecorations); + + return new TextParagraphProperties(textRunStyle, textAlignment, textWrapping, textTrimming); + } + + /// + /// Updates the current bounds. + /// + /// The text line. + /// The left. + /// The right. + /// The bottom. + private static void UpdateBounds(TextLine textLine, ref double left, ref double right, ref double bottom) + { + if (right < textLine.LineMetrics.BaselineOrigin.X + textLine.LineMetrics.Size.Width) + { + right = textLine.LineMetrics.BaselineOrigin.X + textLine.LineMetrics.Size.Width; + } + + if (left < textLine.LineMetrics.BaselineOrigin.X) + { + left = textLine.LineMetrics.BaselineOrigin.X; + } + + bottom += textLine.LineMetrics.Size.Height; + } + + /// + /// Creates an empty text line. + /// + /// The empty text line. + private TextLine CreateEmptyTextLine(int startingIndex) + { + var textFormat = _paragraphProperties.DefaultTextStyle.TextFormat; + + var glyphRun = TextShaper.Current.ShapeText(s_empty, textFormat); + + var textRuns = new[] { new ShapedTextRun(glyphRun, _paragraphProperties.DefaultTextStyle) }; + + return new SimpleTextLine(new TextPointer(startingIndex, 0), textRuns, + TextLineMetrics.Create(textRuns, MaxWidth, _paragraphProperties.TextAlignment)); + } + + /// + /// Updates the layout and applies specified text style overrides. + /// + private void UpdateLayout() + { + if (_text.IsEmpty || MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) + { + var textLine = CreateEmptyTextLine(0); + + TextLines = new List { textLine }; + + Bounds = new Rect(textLine.LineMetrics.BaselineOrigin.X, 0, 0, textLine.LineMetrics.Size.Height); + } + else + { + var textLines = new List(); + + double left = 0.0, right = 0.0, bottom = 0.0; + + var lineBreaker = new LineBreakEnumerator(_text); + + var currentPosition = 0; + + while (currentPosition < _text.Length && (MaxLines == 0 || textLines.Count < MaxLines)) + { + int length; + + if (lineBreaker.MoveNext()) + { + if (!lineBreaker.Current.Required) + { + continue; + } + + length = lineBreaker.Current.PositionWrap - currentPosition; + + 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 + { + length = _text.Length - currentPosition; + } + + var remainingLength = length; + + while (remainingLength > 0 && (MaxLines == 0 || textLines.Count < MaxLines)) + { + 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; + } + } + + if (lineBreaker.Current.Required && currentPosition == _text.Length) + { + var emptyTextLine = CreateEmptyTextLine(currentPosition); + + UpdateBounds(emptyTextLine, ref left, ref right, ref bottom); + + textLines.Add(emptyTextLine); + } + + Bounds = new Rect(left, 0, right, bottom); + + TextLines = textLines; + } + } + + private struct FormattedTextSource : ITextSource + { + private readonly ReadOnlySlice _text; + private readonly TextStyle _defaultStyle; + private readonly IReadOnlyList _textStyleOverrides; + + public FormattedTextSource(ReadOnlySlice text, TextStyle defaultStyle, + IReadOnlyList textStyleOverrides) + { + _text = text; + _defaultStyle = defaultStyle; + _textStyleOverrides = textStyleOverrides; + } + + public TextRun GetTextRun(int textSourceIndex) + { + var runText = _text.Skip(textSourceIndex); + + if (runText.IsEmpty) + { + return new TextEndOfLine(); + } + + var textStyleRun = CreateTextStyleRunWithOverride(runText, _defaultStyle, _textStyleOverrides); + + return new TextCharacters(runText.Take(textStyleRun.TextPointer.Length), textStyleRun.Style); + } + + /// + /// Creates a text style run that has overrides applied. Only overrides with equal TextStyle. + /// If optimizeForShaping is true Foreground is ignored. + /// + /// The text to create the run for. + /// The default text style for segments that don't have an override. + /// The text style overrides. + /// + /// The created text style run. + /// + private static TextStyleRun CreateTextStyleRunWithOverride(ReadOnlySlice text, + TextStyle defaultTextStyle, IReadOnlyList textStyleOverrides) + { + if(textStyleOverrides == null || textStyleOverrides.Count == 0) + { + return new TextStyleRun(new TextPointer(text.Start, text.Length), defaultTextStyle); + } + + var currentTextStyle = defaultTextStyle; + + var hasOverride = false; + + var i = 0; + + var length = 0; + + for (; i < textStyleOverrides.Count; i++) + { + var styleOverride = textStyleOverrides[i]; + + var textPointer = styleOverride.TextPointer; + + if (textPointer.End < text.Start) + { + continue; + } + + if (textPointer.Start > text.End) + { + length = text.Length; + break; + } + + if (textPointer.Start > text.Start) + { + if (styleOverride.Style.TextFormat != currentTextStyle.TextFormat || + !currentTextStyle.Foreground.Equals(styleOverride.Style.Foreground)) + { + length = Math.Min(Math.Abs(textPointer.Start - text.Start), text.Length); + + break; + } + } + + length += Math.Min(text.Length - length, textPointer.Length); + + if (hasOverride) + { + continue; + } + + hasOverride = true; + + currentTextStyle = styleOverride.Style; + } + + if (length < text.Length && i == textStyleOverrides.Count) + { + if (currentTextStyle.Foreground.Equals(defaultTextStyle.Foreground) && + currentTextStyle.TextFormat == defaultTextStyle.TextFormat) + { + length = text.Length; + } + } + + if (length != text.Length) + { + text = text.Take(length); + } + + return new TextStyleRun(new TextPointer(text.Start, length), currentTextStyle); + } + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs new file mode 100644 index 0000000000..a0f7b44882 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; +using Avalonia.Platform; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Represents a line of text that is used for text rendering. + /// + public abstract class TextLine + { + /// + /// Gets the text. + /// + /// + /// The text pointer. + /// + public abstract TextPointer Text { get; } + + /// + /// Gets the text runs. + /// + /// + /// The text runs. + /// + public abstract IReadOnlyList TextRuns { get; } + + /// + /// Gets the line metrics. + /// + /// + /// The line metrics. + /// + public abstract TextLineMetrics LineMetrics { get; } + + /// + /// Draws the at the given origin. + /// + /// The drawing context. + /// The origin. + public abstract void Draw(IDrawingContextImpl drawingContext, Point origin); + + /// + /// Client to get the character hit corresponding to the specified + /// distance from the beginning of the line. + /// + /// distance in text flow direction from the beginning of the line + /// The + public abstract CharacterHit GetCharacterHitFromDistance(double distance); + + /// + /// Client to get the distance from the beginning of the line from the specified + /// . + /// + /// of the character to query the distance. + /// Distance in text flow direction from the beginning of the line. + public abstract double GetDistanceFromCharacterHit(CharacterHit characterHit); + + /// + /// Client to get the next for caret navigation. + /// + /// The current . + /// The next . + public abstract CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit); + + /// + /// Client to get the previous character hit for caret navigation + /// + /// the current character hit + /// The previous + public abstract CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit); + + /// + /// Client to get the previous character hit after backspacing + /// + /// the current character hit + /// The after backspacing + public abstract CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit); + + /// + /// Gets the text line offset x. + /// + /// The line width. + /// The paragraph width. + /// The text alignment. + /// The paragraph offset. + internal static double GetParagraphOffsetX(double lineWidth, double paragraphWidth, TextAlignment textAlignment) + { + if (double.IsPositiveInfinity(paragraphWidth)) + { + return 0; + } + + switch (textAlignment) + { + case TextAlignment.Center: + return (paragraphWidth - lineWidth) / 2; + + case TextAlignment.Right: + return paragraphWidth - lineWidth; + + default: + return 0.0f; + } + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs new file mode 100644 index 0000000000..096305c09c --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Represents a metric for a objects, + /// that holds information about ascent, descent, line gap, size and origin of the text line. + /// + public readonly struct TextLineMetrics + { + public TextLineMetrics(double width, double xOrigin, double ascent, double descent, double lineGap) + { + Ascent = ascent; + Descent = descent; + LineGap = lineGap; + Size = new Size(width, descent - ascent + lineGap); + BaselineOrigin = new Point(xOrigin, -ascent); + } + + /// + /// Gets the overall recommended distance above the baseline. + /// + /// + /// The ascent. + /// + 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; } + + /// + /// Gets the size of the text line. + /// + /// + /// The size. + /// + public Size Size { get; } + + /// + /// Gets the baseline origin. + /// + /// + /// The baseline origin. + /// + public Point BaselineOrigin { get; } + + /// + /// Creates the text line metrics. + /// + /// The text runs. + /// The paragraph width. + /// The text alignment. + /// + public static TextLineMetrics Create(IEnumerable textRuns, double paragraphWidth, TextAlignment textAlignment) + { + var lineWidth = 0.0; + var ascent = 0.0; + var descent = 0.0; + var lineGap = 0.0; + + foreach (var textRun in textRuns) + { + var shapedRun = (ShapedTextRun)textRun; + + lineWidth += shapedRun.Bounds.Width; + + var textFormat = textRun.Style.TextFormat; + + if (ascent > textRun.Style.TextFormat.FontMetrics.Ascent) + { + ascent = textFormat.FontMetrics.Ascent; + } + + if (descent < textFormat.FontMetrics.Descent) + { + descent = textFormat.FontMetrics.Descent; + } + + if (lineGap < textFormat.FontMetrics.LineGap) + { + lineGap = textFormat.FontMetrics.LineGap; + } + } + + var xOrigin = TextLine.GetParagraphOffsetX(lineWidth, paragraphWidth, textAlignment); + + return new TextLineMetrics(lineWidth, xOrigin, ascent, descent, lineGap); + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs new file mode 100644 index 0000000000..1368f1777a --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs @@ -0,0 +1,40 @@ +namespace Avalonia.Media.TextFormatting +{ + /// + /// Provides a set of properties that are used during the paragraph layout. + /// + public readonly struct 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 default text style. + /// + public TextStyle DefaultTextStyle { get; } + + /// + /// Gets the text alignment. + /// + public TextAlignment TextAlignment { get; } + + /// + /// Gets the text wrapping. + /// + public TextWrapping TextWrapping { get; } + + /// + /// Gets the text trimming. + /// + public TextTrimming TextTrimming { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextPointer.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextPointer.cs new file mode 100644 index 0000000000..65d5c04b4c --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextPointer.cs @@ -0,0 +1,70 @@ +using System; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// References a portion of a text buffer. + /// + public readonly struct TextPointer + { + public TextPointer(int start, int length) + { + Start = start; + Length = length; + } + + /// + /// Gets the start. + /// + /// + /// The start. + /// + public int Start { get; } + + /// + /// Gets the length. + /// + /// + /// The length. + /// + public int Length { get; } + + /// + /// Gets the end. + /// + /// + /// The end. + /// + public int End => Start + Length - 1; + + /// + /// 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) + { + if (length > Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return new TextPointer(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) + { + if (length > Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return new TextPointer(Start + length, Length - length); + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs new file mode 100644 index 0000000000..28b83333b9 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs @@ -0,0 +1,48 @@ +using System.Diagnostics; +using Avalonia.Utility; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Represents a portion of a object. + /// + [DebuggerTypeProxy(typeof(TextRunDebuggerProxy))] + public abstract class TextRun + { + /// + /// Gets the text run's text. + /// + public ReadOnlySlice Text { get; protected set; } + + /// + /// Gets the text run's style. + /// + public TextStyle Style { get; protected set; } + + private class TextRunDebuggerProxy + { + private readonly TextRun _textRun; + + public TextRunDebuggerProxy(TextRun textRun) + { + _textRun = textRun; + } + + public string Text + { + get + { + unsafe + { + fixed (char* charsPtr = _textRun.Text.Buffer.Span) + { + return new string(charsPtr, 0, _textRun.Text.Length); + } + } + } + } + + public TextStyle Style => _textRun.Style; + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs new file mode 100644 index 0000000000..eb3a4129bc --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs @@ -0,0 +1,52 @@ +using System; +using Avalonia.Platform; +using Avalonia.Utility; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// A class that is responsible for text shaping. + /// + public class TextShaper + { + private readonly ITextShaperImpl _platformImpl; + + public TextShaper(ITextShaperImpl platformImpl) + { + _platformImpl = platformImpl; + } + + /// + /// Gets the current text shaper. + /// + public static TextShaper Current + { + get + { + var current = AvaloniaLocator.Current.GetService(); + + if (current != null) + { + return current; + } + + var textShaperImpl = AvaloniaLocator.Current.GetService(); + + if (textShaperImpl == null) + throw new InvalidOperationException("No text shaper implementation was registered."); + + current = new TextShaper(textShaperImpl); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(current); + + return current; + } + } + + /// + public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat) + { + return _platformImpl.ShapeText(text, textFormat); + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextStyle.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextStyle.cs new file mode 100644 index 0000000000..cf52c3ca17 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextStyle.cs @@ -0,0 +1,39 @@ +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 new file mode 100644 index 0000000000..55f8999182 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextStyleRun.cs @@ -0,0 +1,24 @@ +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 new file mode 100644 index 0000000000..03576a4c40 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiClass.cs @@ -0,0 +1,29 @@ +namespace Avalonia.Media.TextFormatting.Unicode +{ + public enum BiDiClass + { + ArabicLetter, //AL + ArabicNumber, //AN + ParagraphSeparator, //B + BoundaryNeutral, //BN + CommonSeparator, //CS + EuropeanNumber, //EN + EuropeanSeparator, //ES + EuropeanTerminator, //ET + FirstStrongIsolate, //FSI + LeftToRight, //L + LeftToRightEmbedding, //LRE + LeftToRightIsolate, //LRI + LeftToRightOverride, //LRO + NonspacingMark, //NSM + OtherNeutral, //ON + PopDirectionalFormat, //PDF + PopDirectionalIsolate, //PDI + RightToLeft, //R + RightToLeftEmbedding, //RLE + RightToLeftIsolate, //RLI + RightToLeftOverride, //RLO + SegmentSeparator, //S + WhiteSpace, //WS + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BinaryReaderExtensions.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BinaryReaderExtensions.cs new file mode 100644 index 0000000000..412007c6e0 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BinaryReaderExtensions.cs @@ -0,0 +1,72 @@ +// RichTextKit +// Copyright © 2019 Topten Software. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this product except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// Copied from: https://github.com/toptensoftware/RichTextKit + +using System; +using System.IO; + +namespace Avalonia.Media.TextFormatting.Unicode +{ + internal static class BinaryReaderExtensions + { + public static int ReadInt32BE(this BinaryReader reader) + { + var bytes = reader.ReadBytes(4); + + if (BitConverter.IsLittleEndian) + { + Array.Reverse(bytes); + } + + return BitConverter.ToInt32(bytes, 0); + } + + public static uint ReadUInt32BE(this BinaryReader reader) + { + var bytes = reader.ReadBytes(4); + + if (BitConverter.IsLittleEndian) + { + Array.Reverse(bytes); + } + + return BitConverter.ToUInt32(bytes, 0); + } + + public static void WriteBE(this BinaryWriter writer, int value) + { + var bytes = BitConverter.GetBytes(value); + + if (BitConverter.IsLittleEndian) + { + Array.Reverse(bytes); + } + + writer.Write(bytes); + } + + public static void WriteBE(this BinaryWriter writer, uint value) + { + var bytes = BitConverter.GetBytes(value); + + if (BitConverter.IsLittleEndian) + { + Array.Reverse(bytes); + } + + writer.Write(bytes); + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BreakPairTable.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BreakPairTable.cs new file mode 100644 index 0000000000..c13074711e --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BreakPairTable.cs @@ -0,0 +1,55 @@ +namespace Avalonia.Media.TextFormatting.Unicode +{ + internal static class BreakPairTable + { + 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}, + }; + + public static PairBreakType Map(LineBreakClass first, LineBreakClass second) + { + return (PairBreakType)s_breakPairTable[(int)first][(int)second]; + } + } + + internal enum PairBreakType : byte + { + DI = 0, // Direct break opportunity + IN = 1, // Indirect break opportunity + CI = 2, // Indirect break opportunity for combining marks + CP = 3, // Prohibited break for combining marks + PR = 4 // Prohibited break + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs new file mode 100644 index 0000000000..94171b7324 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs @@ -0,0 +1,169 @@ +using Avalonia.Utility; + +namespace Avalonia.Media.TextFormatting.Unicode +{ + public readonly struct Codepoint + { + /// + /// The replacement codepoint that is used for non supported values. + /// + public static readonly Codepoint ReplacementCodepoint = new Codepoint('\uFFFD'); + + private readonly int _value; + + public Codepoint(int value) + { + _value = value; + } + + /// + /// Gets the . + /// + public GeneralCategory GeneralCategory => UnicodeData.GetGeneralCategory(_value); + + /// + /// Gets the . + /// + public Script Script => UnicodeData.GetScript(_value); + + /// + /// Gets the . + /// + public BiDiClass BiDiClass => UnicodeData.GetBiDiClass(_value); + + /// + /// Gets the . + /// + public LineBreakClass LineBreakClass => UnicodeData.GetLineBreakClass(_value); + + /// + /// Gets the . + /// + public GraphemeBreakClass GraphemeBreakClass => UnicodeData.GetGraphemeClusterBreak(_value); + + /// + /// Determines whether this is a break char. + /// + /// + /// true if [is break character]; otherwise, false. + /// + public bool IsBreakChar + { + get + { + switch (_value) + { + case '\u000A': + case '\u000B': + case '\u000C': + case '\u000D': + case '\u0085': + case '\u2028': + case '\u2029': + return true; + default: + return false; + } + } + } + + /// + /// Determines whether this is white space. + /// + /// + /// true if [is whitespace]; otherwise, false. + /// + public bool IsWhiteSpace + { + get + { + switch (GeneralCategory) + { + case GeneralCategory.Control: + case GeneralCategory.NonspacingMark: + case GeneralCategory.Format: + case GeneralCategory.SpaceSeparator: + case GeneralCategory.SpacingMark: + return true; + } + + return false; + } + } + + public static implicit operator int(Codepoint codepoint) + { + return codepoint._value; + } + + public static implicit operator uint(Codepoint codepoint) + { + return (uint)codepoint._value; + } + + /// + /// Reads the at specified position. + /// + /// The buffer to read from. + /// The index to read at. + /// The count of character that were read. + /// + public static Codepoint ReadAt(ReadOnlySlice text, int index, out int count) + { + count = 1; + + if (index > text.End) + { + return ReplacementCodepoint; + } + + var code = text[index]; + + ushort hi, low; + + //# High surrogate + if (0xD800 <= code && code <= 0xDBFF) + { + hi = code; + + if (index + 1 == text.Length) + { + return ReplacementCodepoint; + } + + low = text[index + 1]; + + if (0xDC00 <= low && low <= 0xDFFF) + { + count = 2; + return new Codepoint((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000); + } + + return ReplacementCodepoint; + } + + //# Low surrogate + if (0xDC00 <= code && code <= 0xDFFF) + { + if (index == 0) + { + return ReplacementCodepoint; + } + + hi = text[index - 1]; + + low = code; + + if (0xD800 <= hi && hi <= 0xDBFF) + { + count = 2; + return new Codepoint((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000); + } + + return ReplacementCodepoint; + } + + return new Codepoint(code); + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs new file mode 100644 index 0000000000..2ff4952cab --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs @@ -0,0 +1,40 @@ +using Avalonia.Utility; + +namespace Avalonia.Media.TextFormatting.Unicode +{ + public ref struct CodepointEnumerator + { + private ReadOnlySlice _text; + + public CodepointEnumerator(ReadOnlySlice text) + { + _text = text; + Current = Codepoint.ReplacementCodepoint; + } + + /// + /// Gets the current . + /// + public Codepoint Current { get; private set; } + + /// + /// Moves to the next . + /// + /// + public bool MoveNext() + { + if (_text.IsEmpty) + { + Current = Codepoint.ReplacementCodepoint; + + return false; + } + + Current = Codepoint.ReadAt(_text, 0, out var count); + + _text = _text.Skip(count); + + return true; + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GeneralCategory.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GeneralCategory.cs new file mode 100644 index 0000000000..3ca55e1336 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GeneralCategory.cs @@ -0,0 +1,44 @@ +namespace Avalonia.Media.TextFormatting.Unicode +{ + public enum GeneralCategory + { + Other, //C# Cc | Cf | Cn | Co | Cs + Control, //Cc + Format, //Cf + Unassigned, //Cn + PrivateUse, //Co + Surrogate, //Cs + Letter, //L# Ll | Lm | Lo | Lt | Lu + CasedLetter, //LC# Ll | Lt | Lu + LowercaseLetter, //Ll + ModifierLetter, //Lm + OtherLetter, //Lo + TitlecaseLetter, //Lt + UppercaseLetter, //Lu + Mark, //M + SpacingMark, //Mc + EnclosingMark, //Me + NonspacingMark, //Mn + Number, //N# Nd | Nl | No + DecimalNumber, //Nd + LetterNumber, //Nl + OtherNumber, //No + Punctuation, //P + ConnectorPunctuation, //Pc + DashPunctuation, //Pd + ClosePunctuation, //Pe + FinalPunctuation, //Pf + InitialPunctuation, //Pi + OtherPunctuation, //Po + OpenPunctuation, //Ps + Symbol, //S# Sc | Sk | Sm | So + CurrencySymbol, //Sc + ModifierSymbol, //Sk + MathSymbol, //Sm + OtherSymbol, //So + Separator, //Z# Zl | Zp | Zs + LineSeparator, //Zl + ParagraphSeparator, //Zp + SpaceSeparator, //Zs + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Grapheme.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Grapheme.cs new file mode 100644 index 0000000000..a6791b4a53 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Grapheme.cs @@ -0,0 +1,26 @@ +using Avalonia.Utility; + +namespace Avalonia.Media.TextFormatting.Unicode +{ + /// + /// Represents the smallest unit of a writing system of any given language. + /// + public readonly struct Grapheme + { + public Grapheme(Codepoint firstCodepoint, ReadOnlySlice text) + { + FirstCodepoint = firstCodepoint; + Text = text; + } + + /// + /// The first of the grapheme cluster. + /// + public Codepoint FirstCodepoint { get; } + + /// + /// The text that is representing the . + /// + public ReadOnlySlice Text { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeBreakClass.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeBreakClass.cs new file mode 100644 index 0000000000..684baae51f --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeBreakClass.cs @@ -0,0 +1,25 @@ +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 + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeEnumerator.cs new file mode 100644 index 0000000000..fd7831dfe6 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeEnumerator.cs @@ -0,0 +1,263 @@ +// This source file is adapted from the .NET cross-platform runtime project. +// (https://github.com/dotnet/runtime/) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System.Runtime.InteropServices; +using Avalonia.Utility; + +namespace Avalonia.Media.TextFormatting.Unicode +{ + public ref struct GraphemeEnumerator + { + private ReadOnlySlice _text; + + public GraphemeEnumerator(ReadOnlySlice text) + { + _text = text; + Current = default; + } + + /// + /// Gets the current . + /// + public Grapheme Current { get; private set; } + + /// + /// Moves to the next . + /// + /// + public bool MoveNext() + { + if (_text.IsEmpty) + { + return false; + } + + // Algorithm given at https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules. + + var processor = new Processor(_text); + + processor.MoveNext(); + + var firstCodepoint = processor.CurrentCodepoint; + + // First, consume as many Prepend scalars as we can (rule GB9b). + while (processor.CurrentType == GraphemeBreakClass.Prepend) + { + processor.MoveNext(); + } + + // Next, make sure we're not about to violate control character restrictions. + // Essentially, if we saw Prepend data, we can't have Control | CR | LF data afterward (rule GB5). + if (processor.CurrentCodeUnitOffset > 0) + { + if (processor.CurrentType == GraphemeBreakClass.Control + || processor.CurrentType == GraphemeBreakClass.CR + || processor.CurrentType == GraphemeBreakClass.LF) + { + goto Return; + } + } + + // Now begin the main state machine. + + var previousClusterBreakType = processor.CurrentType; + + processor.MoveNext(); + + switch (previousClusterBreakType) + { + case GraphemeBreakClass.CR: + if (processor.CurrentType != GraphemeBreakClass.LF) + { + goto Return; // rules GB3 & GB4 (only can follow ) + } + + processor.MoveNext(); + goto case GraphemeBreakClass.LF; + + case GraphemeBreakClass.Control: + case GraphemeBreakClass.LF: + goto Return; // rule GB4 (no data after Control | LF) + + case GraphemeBreakClass.L: + if (processor.CurrentType == GraphemeBreakClass.L) + { + processor.MoveNext(); // rule GB6 (L x L) + goto case GraphemeBreakClass.L; + } + else if (processor.CurrentType == GraphemeBreakClass.V) + { + processor.MoveNext(); // rule GB6 (L x V) + goto case GraphemeBreakClass.V; + } + else if (processor.CurrentType == GraphemeBreakClass.LV) + { + processor.MoveNext(); // rule GB6 (L x LV) + goto case GraphemeBreakClass.LV; + } + else if (processor.CurrentType == GraphemeBreakClass.LVT) + { + processor.MoveNext(); // rule GB6 (L x LVT) + goto case GraphemeBreakClass.LVT; + } + else + { + break; + } + + case GraphemeBreakClass.LV: + case GraphemeBreakClass.V: + if (processor.CurrentType == GraphemeBreakClass.V) + { + processor.MoveNext(); // rule GB7 (LV | V x V) + goto case GraphemeBreakClass.V; + } + else if (processor.CurrentType == GraphemeBreakClass.T) + { + processor.MoveNext(); // rule GB7 (LV | V x T) + goto case GraphemeBreakClass.T; + } + else + { + break; + } + + case GraphemeBreakClass.LVT: + case GraphemeBreakClass.T: + if (processor.CurrentType == GraphemeBreakClass.T) + { + processor.MoveNext(); // rule GB8 (LVT | T x T) + goto case GraphemeBreakClass.T; + } + else + { + break; + } + + case GraphemeBreakClass.ExtendedPictographic: + // Attempt processing extended pictographic (rules GB11, GB9). + // First, drain any Extend scalars that might exist + while (processor.CurrentType == GraphemeBreakClass.Extend) + { + processor.MoveNext(); + } + + // Now see if there's a ZWJ + extended pictograph again. + if (processor.CurrentType != GraphemeBreakClass.ZWJ) + { + break; + } + + processor.MoveNext(); + if (processor.CurrentType != GraphemeBreakClass.ExtendedPictographic) + { + break; + } + + processor.MoveNext(); + goto case GraphemeBreakClass.ExtendedPictographic; + + case GraphemeBreakClass.RegionalIndicator: + // We've consumed a single RI scalar. Try to consume another (to make it a pair). + + if (processor.CurrentType == GraphemeBreakClass.RegionalIndicator) + { + processor.MoveNext(); + } + + // Standlone RI scalars (or a single pair of RI scalars) can only be followed by trailers. + + break; // nothing but trailers after the final RI + + default: + break; + } + + // rules GB9, GB9a + while (processor.CurrentType == GraphemeBreakClass.Extend + || processor.CurrentType == GraphemeBreakClass.ZWJ + || processor.CurrentType == GraphemeBreakClass.SpacingMark) + { + processor.MoveNext(); + } + + Return: + + var text = _text.Take(processor.CurrentCodeUnitOffset); + + Current = new Grapheme(firstCodepoint, text); + + _text = _text.Skip(processor.CurrentCodeUnitOffset); + + return true; // rules GB2, GB999 + } + + [StructLayout(LayoutKind.Auto)] + private ref struct Processor + { + private readonly ReadOnlySlice _buffer; + private int _codeUnitLengthOfCurrentScalar; + + internal Processor(ReadOnlySlice buffer) + { + _buffer = buffer; + _codeUnitLengthOfCurrentScalar = 0; + CurrentCodepoint = Codepoint.ReplacementCodepoint; + CurrentType = GraphemeBreakClass.Other; + CurrentCodeUnitOffset = 0; + } + + public int CurrentCodeUnitOffset { get; private set; } + + /// + /// Will be if invalid data or EOF reached. + /// Caller shouldn't need to special-case this since the normal rules will halt on this condition. + /// + public GraphemeBreakClass CurrentType { get; private set; } + + /// + /// Get the currently processed . + /// + public Codepoint CurrentCodepoint { get; private set; } + + public void MoveNext() + { + // For ill-formed subsequences (like unpaired UTF-16 surrogate code points), we rely on + // the decoder's default behavior of interpreting these ill-formed subsequences as + // equivalent to U+FFFD REPLACEMENT CHARACTER. This code point has a boundary property + // of Other (XX), which matches the modifications made to UAX#29, Rev. 35. + // See: https://www.unicode.org/reports/tr29/tr29-35.html#Modifications + // This change is also reflected in the UCD files. For example, Unicode 11.0's UCD file + // https://www.unicode.org/Public/11.0.0/ucd/auxiliary/GraphemeBreakProperty.txt + // has the line "D800..DFFF ; Control # Cs [2048] ..", + // but starting with Unicode 12.0 that line has been removed. + // + // If a later version of the Unicode Standard further modifies this guidance we should reflect + // that here. + + if (CurrentCodeUnitOffset == _buffer.Length) + { + CurrentCodepoint = Codepoint.ReplacementCodepoint; + } + else + { + CurrentCodeUnitOffset += _codeUnitLengthOfCurrentScalar; + + if (CurrentCodeUnitOffset < _buffer.Length) + { + CurrentCodepoint = Codepoint.ReadAt(_buffer, CurrentCodeUnitOffset, + out _codeUnitLengthOfCurrentScalar); + } + else + { + CurrentCodepoint = Codepoint.ReplacementCodepoint; + } + } + + CurrentType = CurrentCodepoint.GraphemeBreakClass; + } + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreak.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreak.cs new file mode 100644 index 0000000000..34b14f008f --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreak.cs @@ -0,0 +1,63 @@ +// RichTextKit +// Copyright © 2019 Topten Software. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this product except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// +// Ported from: https://github.com/foliojs/linebreak +// Copied from: https://github.com/toptensoftware/RichTextKit + +using System.Diagnostics; + +namespace Avalonia.Media.TextFormatting.Unicode +{ + /// + /// Information about a potential line break position + /// + [DebuggerDisplay("{PositionMeasure}/{PositionWrap} @ {Required}")] + public readonly struct LineBreak + { + /// + /// Constructor + /// + /// The code point index to measure to + /// The code point index to actually break the line at + /// True if this is a required line break; otherwise false + public LineBreak(int positionMeasure, int positionWrap, bool required = false) + { + PositionMeasure = positionMeasure; + PositionWrap = positionWrap; + Required = required; + } + + /// + /// The break position, before any trailing whitespace + /// + /// + /// This doesn't include trailing whitespace + /// + public int PositionMeasure { get; } + + /// + /// The break position, after any trailing whitespace + /// + /// + /// This includes trailing whitespace + /// + public int PositionWrap { get; } + + /// + /// True if there should be a forced line break here + /// + public bool Required { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakClass.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakClass.cs new file mode 100644 index 0000000000..925706dd4f --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakClass.cs @@ -0,0 +1,50 @@ +namespace Avalonia.Media.TextFormatting.Unicode +{ + public enum LineBreakClass + { + OpenPunctuation, //OP + ClosePunctuation, //CL + CloseParenthesis, //CP + Quotation, //QU + Glue, //GL + Nonstarter, //NS + Exclamation, //EX + BreakSymbols, //SY + InfixNumeric, //IS + PrefixNumeric, //PR + PostfixNumeric, //PO + Numeric, //NU + Alphabetic, //AL + HebrewLetter, //HL + Ideographic, //ID + Inseparable, //IN + Hyphen, //HY + BreakAfter, //BA + BreakBefore, //BB + BreakBoth, //B2 + ZWSpace, //ZW + CombiningMark, //CM + WordJoiner, //WJ + H2, //H2 + H3, //H3 + JL, //JL + JV, //JV + JT, //JT + RegionalIndicator, //RI + EBase, //EB + EModifier, //EM + ZWJ, //ZWJ + + Ambiguous, //AI + MandatoryBreak, //BK + ContingentBreak, //CB + ConditionalJapaneseStarter, //CJ + CarriageReturn, //CR + LineFeed, //LF + NextLine, //NL + 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 new file mode 100644 index 0000000000..a11c008409 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs @@ -0,0 +1,243 @@ +// RichTextKit +// Copyright © 2019 Topten Software. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this product except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// +// Ported from: https://github.com/foliojs/linebreak +// Copied from: https://github.com/toptensoftware/RichTextKit + +using Avalonia.Utility; + +namespace Avalonia.Media.TextFormatting.Unicode +{ + /// + /// Implementation of the Unicode Line Break Algorithm + /// + public ref struct LineBreakEnumerator + { + // State + private readonly ReadOnlySlice _text; + private int _pos; + private int _lastPos; + private LineBreakClass? _curClass; + private LineBreakClass? _nextClass; + + public LineBreakEnumerator(ReadOnlySlice text) + { + _text = text; + _pos = 0; + _lastPos = 0; + _curClass = null; + _nextClass = null; + Current = default; + } + + public LineBreak Current { get; private set; } + + public bool MoveNext() + { + // get the first char if we're at the beginning of the string + if (!_curClass.HasValue) + { + _curClass = PeekCharClass() == LineBreakClass.Space ? LineBreakClass.WordJoiner : MapFirst(ReadCharClass()); + } + + while (_pos < _text.Length) + { + _lastPos = _pos; + var lastClass = _nextClass; + _nextClass = ReadCharClass(); + + // explicit newline + if (_curClass.HasValue && (_curClass == LineBreakClass.MandatoryBreak || _curClass == LineBreakClass.CarriageReturn && _nextClass != LineBreakClass.LineFeed)) + { + _curClass = MapFirst(MapClass(_nextClass.Value)); + Current = new LineBreak(FindPriorNonWhitespace(_lastPos), _lastPos, true); + return true; + } + + // handle classes not handled by the pair table + LineBreakClass? cur = null; + switch (_nextClass.Value) + { + case LineBreakClass.Space: + cur = _curClass; + break; + + case LineBreakClass.MandatoryBreak: + case LineBreakClass.LineFeed: + case LineBreakClass.NextLine: + cur = LineBreakClass.MandatoryBreak; + break; + + case LineBreakClass.CarriageReturn: + cur = LineBreakClass.CarriageReturn; + break; + + case LineBreakClass.ContingentBreak: + cur = LineBreakClass.BreakAfter; + break; + } + + if (cur != null) + { + _curClass = cur; + + if (_nextClass.Value == LineBreakClass.MandatoryBreak) + { + Current = new LineBreak(FindPriorNonWhitespace(_lastPos), _lastPos); + return true; + } + + continue; + } + + // if not handled already, use the pair table + var shouldBreak = false; + switch (BreakPairTable.Map(_curClass.Value,_nextClass.Value)) + { + case PairBreakType.DI: // Direct break + shouldBreak = true; + break; + + case PairBreakType.IN: // possible indirect break + shouldBreak = lastClass.HasValue && lastClass.Value == LineBreakClass.Space; + break; + + case PairBreakType.CI: + shouldBreak = lastClass.HasValue && lastClass.Value == LineBreakClass.Space; + if (!shouldBreak) + { + continue; + } + break; + + case PairBreakType.CP: // prohibited for combining marks + if (!lastClass.HasValue || lastClass.Value != LineBreakClass.Space) + { + continue; + } + break; + } + + _curClass = _nextClass; + + if (shouldBreak) + { + Current = new LineBreak(FindPriorNonWhitespace(_lastPos), _lastPos); + return true; + } + } + + if (_pos >= _text.Length) + { + if (_lastPos < _text.Length) + { + _lastPos = _text.Length; + var cls = Codepoint.ReadAt(_text, _text.Length - 1, out _).LineBreakClass; + bool required = cls == LineBreakClass.MandatoryBreak || cls == LineBreakClass.LineFeed || cls == LineBreakClass.CarriageReturn; + Current = new LineBreak(FindPriorNonWhitespace(_text.Length), _text.Length, required); + return true; + } + } + + return false; + } + + private int FindPriorNonWhitespace(int from) + { + if (from > 0) + { + var cp = Codepoint.ReadAt(_text, from - 1, out var count); + + var cls = cp.LineBreakClass; + + if (cls == LineBreakClass.MandatoryBreak || cls == LineBreakClass.LineFeed || cls == LineBreakClass.CarriageReturn) + { + from -= count; + } + } + + while (from > 0) + { + var cp = Codepoint.ReadAt(_text, from - 1, out var count); + + var cls = cp.LineBreakClass; + + if (cls == LineBreakClass.Space) + { + from -= count; + } + else + { + break; + } + } + return from; + } + + // Get the next character class + private LineBreakClass ReadCharClass() + { + var cp = Codepoint.ReadAt(_text, _pos, out var count); + + _pos += count; + + return MapClass(cp.LineBreakClass); + } + + private LineBreakClass PeekCharClass() + { + return MapClass(Codepoint.ReadAt(_text, _pos, out _).LineBreakClass); + } + + private static LineBreakClass MapClass(LineBreakClass c) + { + switch (c) + { + case LineBreakClass.Ambiguous: + return LineBreakClass.Alphabetic; + + case LineBreakClass.ComplexContext: + case LineBreakClass.Surrogate: + case LineBreakClass.Unknown: + return LineBreakClass.Alphabetic; + + case LineBreakClass.ConditionalJapaneseStarter: + return LineBreakClass.Nonstarter; + + default: + return c; + } + } + + private static LineBreakClass MapFirst(LineBreakClass c) + { + switch (c) + { + case LineBreakClass.LineFeed: + case LineBreakClass.NextLine: + return LineBreakClass.MandatoryBreak; + + case LineBreakClass.ContingentBreak: + return LineBreakClass.BreakAfter; + + case LineBreakClass.Space: + return LineBreakClass.WordJoiner; + + default: + return c; + } + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Script.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Script.cs new file mode 100644 index 0000000000..e9681d4c24 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Script.cs @@ -0,0 +1,160 @@ +namespace Avalonia.Media.TextFormatting.Unicode +{ + public enum Script + { + Adlam, //Adlm + CaucasianAlbanian, //Aghb + Ahom, //Ahom + Arabic, //Arab + ImperialAramaic, //Armi + Armenian, //Armn + Avestan, //Avst + Balinese, //Bali + Bamum, //Bamu + BassaVah, //Bass + Batak, //Batk + Bengali, //Beng + Bhaiksuki, //Bhks + Bopomofo, //Bopo + Brahmi, //Brah + Braille, //Brai + Buginese, //Bugi + Buhid, //Buhd + Chakma, //Cakm + CanadianAboriginal, //Cans + Carian, //Cari + Cham, //Cham + Cherokee, //Cher + Coptic, //Copt + Cypriot, //Cprt + Cyrillic, //Cyrl + Devanagari, //Deva + Dogra, //Dogr + Deseret, //Dsrt + Duployan, //Dupl + EgyptianHieroglyphs, //Egyp + Elbasan, //Elba + Elymaic, //Elym + Ethiopic, //Ethi + Georgian, //Geor + Glagolitic, //Glag + GunjalaGondi, //Gong + MasaramGondi, //Gonm + Gothic, //Goth + Grantha, //Gran + Greek, //Grek + Gujarati, //Gujr + Gurmukhi, //Guru + Hangul, //Hang + Han, //Hani + Hanunoo, //Hano + Hatran, //Hatr + Hebrew, //Hebr + Hiragana, //Hira + AnatolianHieroglyphs, //Hluw + PahawhHmong, //Hmng + NyiakengPuachueHmong, //Hmnp + KatakanaOrHiragana, //Hrkt + OldHungarian, //Hung + OldItalic, //Ital + Javanese, //Java + KayahLi, //Kali + Katakana, //Kana + Kharoshthi, //Khar + Khmer, //Khmr + Khojki, //Khoj + Kannada, //Knda + Kaithi, //Kthi + TaiTham, //Lana + Lao, //Laoo + Latin, //Latn + Lepcha, //Lepc + Limbu, //Limb + LinearA, //Lina + LinearB, //Linb + Lisu, //Lisu + Lycian, //Lyci + Lydian, //Lydi + Mahajani, //Mahj + Makasar, //Maka + Mandaic, //Mand + Manichaean, //Mani + Marchen, //Marc + Medefaidrin, //Medf + MendeKikakui, //Mend + MeroiticCursive, //Merc + MeroiticHieroglyphs, //Mero + Malayalam, //Mlym + Modi, //Modi + Mongolian, //Mong + Mro, //Mroo + MeeteiMayek, //Mtei + Multani, //Mult + Myanmar, //Mymr + Nandinagari, //Nand + OldNorthArabian, //Narb + Nabataean, //Nbat + Newa, //Newa + Nko, //Nkoo + Nushu, //Nshu + Ogham, //Ogam + OlChiki, //Olck + OldTurkic, //Orkh + Oriya, //Orya + Osage, //Osge + Osmanya, //Osma + Palmyrene, //Palm + PauCinHau, //Pauc + OldPermic, //Perm + PhagsPa, //Phag + InscriptionalPahlavi, //Phli + PsalterPahlavi, //Phlp + Phoenician, //Phnx + Miao, //Plrd + InscriptionalParthian, //Prti + Rejang, //Rjng + HanifiRohingya, //Rohg + Runic, //Runr + Samaritan, //Samr + OldSouthArabian, //Sarb + Saurashtra, //Saur + SignWriting, //Sgnw + Shavian, //Shaw + Sharada, //Shrd + Siddham, //Sidd + Khudawadi, //Sind + Sinhala, //Sinh + Sogdian, //Sogd + OldSogdian, //Sogo + SoraSompeng, //Sora + Soyombo, //Soyo + Sundanese, //Sund + SylotiNagri, //Sylo + Syriac, //Syrc + Tagbanwa, //Tagb + Takri, //Takr + TaiLe, //Tale + NewTaiLue, //Talu + Tamil, //Taml + Tangut, //Tang + TaiViet, //Tavt + Telugu, //Telu + Tifinagh, //Tfng + Tagalog, //Tglg + Thaana, //Thaa + Thai, //Thai + Tibetan, //Tibt + Tirhuta, //Tirh + Ugaritic, //Ugar + Vai, //Vaii + WarangCiti, //Wara + Wancho, //Wcho + OldPersian, //Xpeo + Cuneiform, //Xsux + Yi, //Yiii + ZanabazarSquare, //Zanb + Inherited, //Zinh + Common, //Zyyy + Unknown, //Zzzz + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeData.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeData.cs new file mode 100644 index 0000000000..3c00c49707 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeData.cs @@ -0,0 +1,89 @@ +namespace Avalonia.Media.TextFormatting.Unicode +{ + /// + /// Helper for looking up unicode character class information + /// + internal static class UnicodeData + { + internal const int CATEGORY_BITS = 6; + internal const int SCRIPT_BITS = 8; + internal const int BIDI_BITS = 5; + internal const int LINEBREAK_BITS = 6; + + internal const int SCRIPT_SHIFT = CATEGORY_BITS; + internal const int BIDI_SHIFT = CATEGORY_BITS + SCRIPT_BITS; + internal const int LINEBREAK_SHIFT = CATEGORY_BITS + SCRIPT_BITS + BIDI_BITS; + + internal const int CATEGORY_MASK = (1 << CATEGORY_BITS) - 1; + internal const int SCRIPT_MASK = (1 << SCRIPT_BITS) - 1; + internal const int BIDI_MASK = (1 << BIDI_BITS) - 1; + internal const int LINEBREAK_MASK = (1 << LINEBREAK_BITS) - 1; + + private static readonly UnicodeTrie s_unicodeDataTrie; + private static readonly UnicodeTrie s_graphemeBreakTrie; + + static UnicodeData() + { + s_unicodeDataTrie = new UnicodeTrie(typeof(UnicodeData).Assembly.GetManifestResourceStream("Avalonia.Assets.UnicodeData.trie")); + s_graphemeBreakTrie = new UnicodeTrie(typeof(UnicodeData).Assembly.GetManifestResourceStream("Avalonia.Assets.GraphemeBreak.trie")); + } + + /// + /// Gets the for a Unicode codepoint. + /// + /// The codepoint in question. + /// The code point's general category. + public static GeneralCategory GetGeneralCategory(int codepoint) + { + var value = s_unicodeDataTrie.Get(codepoint); + + return (GeneralCategory)(value & CATEGORY_MASK); + } + + /// + /// Gets the for a Unicode codepoint. + /// + /// The codepoint in question. + /// The code point's script. + public static Script GetScript(int codepoint) + { + var value = s_unicodeDataTrie.Get(codepoint); + + return (Script)((value >> SCRIPT_SHIFT) & SCRIPT_MASK); + } + + /// + /// Gets the for a Unicode codepoint. + /// + /// The codepoint in question. + /// The code point's biDi class. + public static BiDiClass GetBiDiClass(int codepoint) + { + var value = s_unicodeDataTrie.Get(codepoint); + + return (BiDiClass)((value >> BIDI_SHIFT) & BIDI_MASK); + } + + /// + /// Gets the line break class for a Unicode codepoint. + /// + /// The codepoint in question. + /// The code point's line break class. + public static LineBreakClass GetLineBreakClass(int codepoint) + { + var value = s_unicodeDataTrie.Get(codepoint); + + return (LineBreakClass)((value >> LINEBREAK_SHIFT) & LINEBREAK_MASK); + } + + /// + /// Gets the grapheme break type for the Unicode codepoint. + /// + /// The codepoint in question. + /// The code point's grapheme break type. + public static GraphemeBreakClass GetGraphemeClusterBreak(int codepoint) + { + return (GraphemeBreakClass)s_graphemeBreakTrie.Get(codepoint); + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeTrie.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeTrie.cs new file mode 100644 index 0000000000..08b019ed33 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeTrie.cs @@ -0,0 +1,128 @@ +// RichTextKit +// Copyright © 2019 Topten Software. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this product except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// Ported from: https://github.com/foliojs/unicode-trie +// Copied from: https://github.com/toptensoftware/RichTextKit + +using System.IO; +using System.IO.Compression; +using System.Text; + +namespace Avalonia.Media.TextFormatting.Unicode +{ + internal class UnicodeTrie + { + private readonly int[] _data; + private readonly int _highStart; + private readonly uint _errorValue; + + public UnicodeTrie(Stream stream) + { + int dataLength; + using (var bw = new BinaryReader(stream, Encoding.UTF8, true)) + { + _highStart = bw.ReadInt32BE(); + _errorValue = bw.ReadUInt32BE(); + dataLength = bw.ReadInt32BE() / 4; + } + + using (var infl1 = new DeflateStream(stream, CompressionMode.Decompress, true)) + using (var infl2 = new DeflateStream(infl1, CompressionMode.Decompress, true)) + using (var bw = new BinaryReader(infl2, Encoding.UTF8, true)) + { + _data = new int[dataLength]; + for (int i = 0; i < _data.Length; i++) + { + _data[i] = bw.ReadInt32(); + } + } + } + + public UnicodeTrie(byte[] buf) : this(new MemoryStream(buf)) + { + + } + + internal UnicodeTrie(int[] data, int highStart, uint errorValue) + { + _data = data; + _highStart = highStart; + _errorValue = errorValue; + } + + internal void Save(Stream stream) + { + // Write the header info + using (var bw = new BinaryWriter(stream, Encoding.UTF8, true)) + { + bw.WriteBE(_highStart); + bw.WriteBE(_errorValue); + bw.WriteBE(_data.Length * 4); + } + + // Double compress the data + using (var def1 = new DeflateStream(stream, CompressionLevel.Optimal, true)) + using (var def2 = new DeflateStream(def1, CompressionLevel.Optimal, true)) + using (var bw = new BinaryWriter(def2, Encoding.UTF8, true)) + { + foreach (var v in _data) + { + bw.Write(v); + } + bw.Flush(); + def2.Flush(); + def1.Flush(); + } + } + + public uint Get(int codePoint) + { + int index; + if ((codePoint < 0) || (codePoint > 0x10ffff)) + { + return _errorValue; + } + + if ((codePoint < 0xd800) || ((codePoint > 0xdbff) && (codePoint <= 0xffff))) + { + // Ordinary BMP code point, excluding leading surrogates. + // BMP uses a single level lookup. BMP index starts at offset 0 in the index. + // data is stored in the index array itself. + index = (_data[codePoint >> UnicodeTrieBuilder.SHIFT_2] << UnicodeTrieBuilder.INDEX_SHIFT) + (codePoint & UnicodeTrieBuilder.DATA_MASK); + return (uint)_data[index]; + } + + if (codePoint <= 0xffff) + { + // Lead Surrogate Code Point. A Separate index section is stored for + // lead surrogate code units and code points. + // The main index has the code unit data. + // For this function, we need the code point data. + index = (_data[UnicodeTrieBuilder.LSCP_INDEX_2_OFFSET + ((codePoint - 0xd800) >> UnicodeTrieBuilder.SHIFT_2)] << UnicodeTrieBuilder.INDEX_SHIFT) + (codePoint & UnicodeTrieBuilder.DATA_MASK); + return (uint)_data[index]; + } + + if (codePoint < _highStart) + { + // Supplemental code point, use two-level lookup. + index = _data[(UnicodeTrieBuilder.INDEX_1_OFFSET - UnicodeTrieBuilder.OMITTED_BMP_INDEX_1_LENGTH) + (codePoint >> UnicodeTrieBuilder.SHIFT_1)]; + index = _data[index + ((codePoint >> UnicodeTrieBuilder.SHIFT_2) & UnicodeTrieBuilder.INDEX_2_MASK)]; + index = (index << UnicodeTrieBuilder.INDEX_SHIFT) + (codePoint & UnicodeTrieBuilder.DATA_MASK); + return (uint)_data[index]; + } + + return (uint)_data[_data.Length - UnicodeTrieBuilder.DATA_GRANULARITY]; + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeTrieBuilder.Constants.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeTrieBuilder.Constants.cs new file mode 100644 index 0000000000..29ee45acc2 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeTrieBuilder.Constants.cs @@ -0,0 +1,159 @@ +// RichTextKit +// Copyright © 2019 Topten Software. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this product except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// Ported from: https://github.com/foliojs/unicode-trie +// Copied from: https://github.com/toptensoftware/RichTextKit + +namespace Avalonia.Media.TextFormatting.Unicode +{ + internal partial class UnicodeTrieBuilder + { + // Shift size for getting the index-1 table offset. + internal const int SHIFT_1 = 6 + 5; + + // Shift size for getting the index-2 table offset. + internal const int SHIFT_2 = 5; + + // Difference between the two shift sizes, + // for getting an index-1 offset from an index-2 offset. 6=11-5 + const int SHIFT_1_2 = SHIFT_1 - SHIFT_2; + + // Number of index-1 entries for the BMP. 32=0x20 + // This part of the index-1 table is omitted from the serialized form. + internal const int OMITTED_BMP_INDEX_1_LENGTH = 0x10000 >> SHIFT_1; + + // Number of code points per index-1 table entry. 2048=0x800 + const int CP_PER_INDEX_1_ENTRY = 1 << SHIFT_1; + + // Number of entries in an index-2 block. 64=0x40 + const int INDEX_2_BLOCK_LENGTH = 1 << SHIFT_1_2; + + // Mask for getting the lower bits for the in-index-2-block offset. */ + internal const int INDEX_2_MASK = INDEX_2_BLOCK_LENGTH - 1; + + // Number of entries in a data block. 32=0x20 + const int DATA_BLOCK_LENGTH = 1 << SHIFT_2; + + // Mask for getting the lower bits for the in-data-block offset. + internal const int DATA_MASK = DATA_BLOCK_LENGTH - 1; + + // Shift size for shifting left the index array values. + // Increases possible data size with 16-bit index values at the cost + // of compactability. + // This requires data blocks to be aligned by DATA_GRANULARITY. + internal const int INDEX_SHIFT = 2; + + // The alignment size of a data block. Also the granularity for compaction. + internal const int DATA_GRANULARITY = 1 << INDEX_SHIFT; + + // The BMP part of the index-2 table is fixed and linear and starts at offset 0. + // Length=2048=0x800=0x10000>>SHIFT_2. + const int INDEX_2_OFFSET = 0; + + // The part of the index-2 table for U+D800..U+DBFF stores values for + // lead surrogate code _units_ not code _points_. + // Values for lead surrogate code _points_ are indexed with this portion of the table. + // Length=32=0x20=0x400>>SHIFT_2. (There are 1024=0x400 lead surrogates.) + internal const int LSCP_INDEX_2_OFFSET = 0x10000 >> SHIFT_2; + const int LSCP_INDEX_2_LENGTH = 0x400 >> SHIFT_2; + + // Count the lengths of both BMP pieces. 2080=0x820 + const int INDEX_2_BMP_LENGTH = LSCP_INDEX_2_OFFSET + LSCP_INDEX_2_LENGTH; + + // The 2-byte UTF-8 version of the index-2 table follows at offset 2080=0x820. + // Length 32=0x20 for lead bytes C0..DF, regardless of SHIFT_2. + const int UTF8_2B_INDEX_2_OFFSET = INDEX_2_BMP_LENGTH; + const int UTF8_2B_INDEX_2_LENGTH = 0x800 >> 6; // U+0800 is the first code point after 2-byte UTF-8 + + // The index-1 table, only used for supplementary code points, at offset 2112=0x840. + // Variable length, for code points up to highStart, where the last single-value range starts. + // Maximum length 512=0x200=0x100000>>SHIFT_1. + // (For 0x100000 supplementary code points U+10000..U+10ffff.) + // + // The part of the index-2 table for supplementary code points starts + // after this index-1 table. + // + // Both the index-1 table and the following part of the index-2 table + // are omitted completely if there is only BMP data. + internal const int INDEX_1_OFFSET = UTF8_2B_INDEX_2_OFFSET + UTF8_2B_INDEX_2_LENGTH; + const int MAX_INDEX_1_LENGTH = 0x100000 >> SHIFT_1; + + // The illegal-UTF-8 data block follows the ASCII block, at offset 128=0x80. + // Used with linear access for single bytes 0..0xbf for simple error handling. + // Length 64=0x40, not DATA_BLOCK_LENGTH. + const int BAD_UTF8_DATA_OFFSET = 0x80; + + // The start of non-linear-ASCII data blocks, at offset 192=0xc0. + // !!!! + const int DATA_START_OFFSET = 0xc0; + + // The null data block. + // Length 64=0x40 even if DATA_BLOCK_LENGTH is smaller, + // to work with 6-bit trail bytes from 2-byte UTF-8. + const int DATA_NULL_OFFSET = DATA_START_OFFSET; + + // The start of allocated data blocks. + const int NEW_DATA_START_OFFSET = DATA_NULL_OFFSET + 0x40; + + // The start of data blocks for U+0800 and above. + // Below, compaction uses a block length of 64 for 2-byte UTF-8. + // From here on, compaction uses DATA_BLOCK_LENGTH. + // Data values for 0x780 code points beyond ASCII. + const int DATA_0800_OFFSET = NEW_DATA_START_OFFSET + 0x780; + + // Start with allocation of 16k data entries. */ + const int INITIAL_DATA_LENGTH = 1 << 14; + + // Grow about 8x each time. + const int MEDIUM_DATA_LENGTH = 1 << 17; + + // Maximum length of the runtime data array. + // Limited by 16-bit index values that are left-shifted by INDEX_SHIFT, + // and by uint16_t UTrie2Header.shiftedDataLength. + const int MAX_DATA_LENGTH_RUNTIME = 0xffff << INDEX_SHIFT; + + const int INDEX_1_LENGTH = 0x110000 >> SHIFT_1; + + // Maximum length of the build-time data array. + // One entry per 0x110000 code points, plus the illegal-UTF-8 block and the null block, + // plus values for the 0x400 surrogate code units. + const int MAX_DATA_LENGTH_BUILDTIME = 0x110000 + 0x40 + 0x40 + 0x400; + + // At build time, leave a gap in the index-2 table, + // at least as long as the maximum lengths of the 2-byte UTF-8 index-2 table + // and the supplementary index-1 table. + // Round up to INDEX_2_BLOCK_LENGTH for proper compacting. + const int INDEX_GAP_OFFSET = INDEX_2_BMP_LENGTH; + const int INDEX_GAP_LENGTH = ((UTF8_2B_INDEX_2_LENGTH + MAX_INDEX_1_LENGTH) + INDEX_2_MASK) & ~INDEX_2_MASK; + + // Maximum length of the build-time index-2 array. + // Maximum number of Unicode code points (0x110000) shifted right by SHIFT_2, + // plus the part of the index-2 table for lead surrogate code points, + // plus the build-time index gap, + // plus the null index-2 block.) + const int MAX_INDEX_2_LENGTH = (0x110000 >> SHIFT_2) + LSCP_INDEX_2_LENGTH + INDEX_GAP_LENGTH + INDEX_2_BLOCK_LENGTH; + + // The null index-2 block, following the gap in the index-2 table. + const int INDEX_2_NULL_OFFSET = INDEX_GAP_OFFSET + INDEX_GAP_LENGTH; + + // The start of allocated index-2 blocks. + const int INDEX_2_START_OFFSET = INDEX_2_NULL_OFFSET + INDEX_2_BLOCK_LENGTH; + + // Maximum length of the runtime index array. + // Limited by its own 16-bit index values, and by uint16_t UTrie2Header.indexLength. + // (The actual maximum length is lower, + // (0x110000>>SHIFT_2)+UTF8_2B_INDEX_2_LENGTH+MAX_INDEX_1_LENGTH.) + const int MAX_INDEX_LENGTH = 0xffff; + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeTrieBuilder.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeTrieBuilder.cs new file mode 100644 index 0000000000..a60bac4ce4 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeTrieBuilder.cs @@ -0,0 +1,984 @@ +// RichTextKit +// Copyright © 2019 Topten Software. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this product except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// Ported from: https://github.com/foliojs/unicode-trie +// Copied from: https://github.com/toptensoftware/RichTextKit + +using System; +using System.Collections.Generic; +using System.IO; + +namespace Avalonia.Media.TextFormatting.Unicode +{ + internal partial class UnicodeTrieBuilder + { + private readonly uint _initialValue; + private readonly uint _errorValue; + private readonly int[] _index1; + private readonly int[] _index2; + private int _highStart; + private uint[] _data; + private int _dataCapacity; + private int _firstFreeBlock; + private bool _isCompacted; + private readonly int[] _map; + private int _dataNullOffset; + private int _dataLength; + private int _index2NullOffset; + private int _index2Length; + + public UnicodeTrieBuilder(uint initialValue = 0, uint errorValue = 0) + { + _initialValue = initialValue; + _errorValue = errorValue; + _index1 = new int[INDEX_1_LENGTH]; + _index2 = new int[MAX_INDEX_2_LENGTH]; + _highStart = 0x110000; + + _data = new uint[INITIAL_DATA_LENGTH]; + _dataCapacity = INITIAL_DATA_LENGTH; + + _firstFreeBlock = 0; + _isCompacted = false; + + // Multi-purpose per-data-block table. + // + // Before compacting: + // + // Per-data-block reference counters/free-block list. + // 0: unused + // >0: reference counter (number of index-2 entries pointing here) + // <0: next free data block in free-block list + // + // While compacting: + // + // Map of adjusted indexes, used in compactData() and compactIndex2(). + // Maps from original indexes to new ones. + _map = new int[MAX_DATA_LENGTH_BUILDTIME >> SHIFT_2]; + + int i; + for (i = 0; i < 0x80; i++) + { + _data[i] = _initialValue; + } + + for (; i < 0xc0; i++) + { + _data[i] = _errorValue; + } + + for (i = DATA_NULL_OFFSET; i < NEW_DATA_START_OFFSET; i++) + { + _data[i] = _initialValue; + } + + _dataNullOffset = DATA_NULL_OFFSET; + _dataLength = NEW_DATA_START_OFFSET; + + // set the index-2 indexes for the 2=0x80>>SHIFT_2 ASCII data blocks + int j; + i = 0; + for (j = 0; j < 0x80; j += DATA_BLOCK_LENGTH) { + _index2[i] = j; + _map[i++] = 1; + } + + // reference counts for the bad-UTF-8-data block + for (; j < 0xc0; j += DATA_BLOCK_LENGTH) { + _map[i++] = 0; + } + + // Reference counts for the null data block: all blocks except for the ASCII blocks. + // Plus 1 so that we don't drop this block during compaction. + // Plus as many as needed for lead surrogate code points. + // i==newTrie->dataNullOffset + _map[i++] = ((0x110000 >> SHIFT_2) - (0x80 >> SHIFT_2)) + 1 + LSCP_INDEX_2_LENGTH; + j += DATA_BLOCK_LENGTH; + for (; j < NEW_DATA_START_OFFSET; j += DATA_BLOCK_LENGTH) { + _map[i++] = 0; + } + + // set the remaining indexes in the BMP index-2 block + // to the null data block + for (i = 0x80 >> SHIFT_2; i < INDEX_2_BMP_LENGTH; i++) { + _index2[i] = DATA_NULL_OFFSET; + } + + // Fill the index gap with impossible values so that compaction + // does not overlap other index-2 blocks with the gap. + for (i = 0; i < INDEX_GAP_LENGTH; i++) { + _index2[INDEX_GAP_OFFSET + i] = -1; + } + + // set the indexes in the null index-2 block + for (i = 0; i < INDEX_2_BLOCK_LENGTH; i++) { + _index2[INDEX_2_NULL_OFFSET + i] = DATA_NULL_OFFSET; + } + + _index2NullOffset = INDEX_2_NULL_OFFSET; + _index2Length = INDEX_2_START_OFFSET; + + // set the index-1 indexes for the linear index-2 block + j = 0; + for (i = 0; i < OMITTED_BMP_INDEX_1_LENGTH; i++) { + _index1[i] = j; + j += INDEX_2_BLOCK_LENGTH; + } + + // set the remaining index-1 indexes to the null index-2 block + for (; i < INDEX_1_LENGTH; i++) { + _index1[i] = INDEX_2_NULL_OFFSET; + } + + // Preallocate and reset data for U+0080..U+07ff, + // for 2-byte UTF-8 which will be compacted in 64-blocks + // even if DATA_BLOCK_LENGTH is smaller. + for (i = 0x80; i < 0x800; i += DATA_BLOCK_LENGTH) { + Set(i, _initialValue); + } + + } + + public UnicodeTrieBuilder Set(int codePoint, uint value) + { + if ((codePoint < 0) || (codePoint > 0x10ffff)) + { + throw new InvalidOperationException("Invalid code point"); + } + + if (_isCompacted) + { + throw new InvalidOperationException("Already compacted"); + } + + var block = GetDataBlock(codePoint, true); + _data[block + (codePoint & DATA_MASK)] = value; + return this; + } + + public UnicodeTrieBuilder SetRange(int start, int end, uint value, bool overwrite = true) + { + + if ((start > 0x10ffff) || (end > 0x10ffff) || (start > end)) + { + throw new InvalidOperationException("Invalid code point"); + } + + if (_isCompacted) + { + throw new InvalidOperationException("Already compacted"); + } + + if (!overwrite && (value == _initialValue)) + { + return this; // nothing to do + } + + var limit = end + 1; + if ((start & DATA_MASK) != 0) + { + // set partial block at [start..following block boundary + var block = GetDataBlock(start, true); + + var nextStart = (start + DATA_BLOCK_LENGTH) & ~DATA_MASK; + if (nextStart <= limit) + { + FillBlock(block, start & DATA_MASK, DATA_BLOCK_LENGTH, value, _initialValue, overwrite); + start = nextStart; + } + else + { + FillBlock(block, start & DATA_MASK, limit & DATA_MASK, value, _initialValue, overwrite); + return this; + } + } + + // number of positions in the last, partial block + var rest = limit & DATA_MASK; + + // round down limit to a block boundary + limit &= ~DATA_MASK; + + // iterate over all-value blocks + int repeatBlock; + if (value == _initialValue) + { + repeatBlock = _dataNullOffset; + } + else + { + repeatBlock = -1; + } + + while (start < limit) + { + var setRepeatBlock = false; + + if ((value == _initialValue) && IsInNullBlock(start, true)) + { + start += DATA_BLOCK_LENGTH; // nothing to do + continue; + } + + // get index value + var i2 = GetIndex2Block(start, true); + i2 += (start >> SHIFT_2) & INDEX_2_MASK; + + var block = _index2[i2]; + if (IsWritableBlock(block)) + { + // already allocated + if (overwrite && (block >= DATA_0800_OFFSET)) + { + // We overwrite all values, and it's not a + // protected (ASCII-linear or 2-byte UTF-8) block: + // replace with the repeatBlock. + setRepeatBlock = true; + } + else + { + // protected block: just write the values into this block + FillBlock(block, 0, DATA_BLOCK_LENGTH, value, _initialValue, overwrite); + } + + } + else if ((_data[block] != value) && (overwrite || (block == _dataNullOffset))) + { + // Set the repeatBlock instead of the null block or previous repeat block: + // + // If !isWritableBlock() then all entries in the block have the same value + // because it's the null block or a range block (the repeatBlock from a previous + // call to utrie2_setRange32()). + // No other blocks are used multiple times before compacting. + // + // The null block is the only non-writable block with the initialValue because + // of the repeatBlock initialization above. (If value==initialValue, then + // the repeatBlock will be the null data block.) + // + // We set our repeatBlock if the desired value differs from the block's value, + // and if we overwrite any data or if the data is all initial values + // (which is the same as the block being the null block, see above). + setRepeatBlock = true; + } + + if (setRepeatBlock) + { + if (repeatBlock >= 0) + { + SetIndex2Entry(i2, repeatBlock); + } + else + { + // create and set and fill the repeatBlock + repeatBlock = GetDataBlock(start, true); + WriteBlock(repeatBlock, value); + } + } + + start += DATA_BLOCK_LENGTH; + } + + if (rest > 0) + { + // set partial block at [last block boundary..limit + var block = GetDataBlock(start, true); + FillBlock(block, 0, rest, value, _initialValue, overwrite); + } + + return this; + } + + public uint Get(int c, bool fromLSCP = true) + { + if ((c < 0) || (c > 0x10ffff)) + { + return _errorValue; + } + + if ((c >= _highStart) && (!((c >= 0xd800) && (c < 0xdc00)) || fromLSCP)) + { + return _data[_dataLength - DATA_GRANULARITY]; + } + + int i2; + if (((c >= 0xd800) && (c < 0xdc00)) && fromLSCP) + { + i2 = (LSCP_INDEX_2_OFFSET - (0xd800 >> SHIFT_2)) + (c >> SHIFT_2); + } + else + { + i2 = _index1[c >> SHIFT_1] + ((c >> SHIFT_2) & INDEX_2_MASK); + } + + var block = _index2[i2]; + return _data[block + (c & DATA_MASK)]; + } + + public byte[] ToBuffer() + { + var mem = new MemoryStream(); + Save(mem); + return mem.GetBuffer(); + } + + public void Save(Stream stream) + { + var trie = this.Freeze(); + trie.Save(stream); + } + + public UnicodeTrie Freeze() + { + int allIndexesLength, i; + if (!_isCompacted) + { + Compact(); + } + + if (_highStart <= 0x10000) + { + allIndexesLength = INDEX_1_OFFSET; + } + else + { + allIndexesLength = _index2Length; + } + + var dataMove = allIndexesLength; + + // are indexLength and dataLength within limits? + if ((allIndexesLength > MAX_INDEX_LENGTH) || // for unshifted indexLength + ((dataMove + _dataNullOffset) > 0xffff) || // for unshifted dataNullOffset + ((dataMove + DATA_0800_OFFSET) > 0xffff) || // for unshifted 2-byte UTF-8 index-2 values + ((dataMove + _dataLength) > MAX_DATA_LENGTH_RUNTIME)) + { // for shiftedDataLength + throw new InvalidOperationException("Trie data is too large."); + } + + // calculate the sizes of, and allocate, the index and data arrays + var indexLength = allIndexesLength + _dataLength; + var data = new int[indexLength]; + + // write the index-2 array values shifted right by INDEX_SHIFT, after adding dataMove + var destIdx = 0; + for (i = 0; i < INDEX_2_BMP_LENGTH; i++) + { + data[destIdx++] = ((_index2[i] + dataMove) >> INDEX_SHIFT); + } + + // write UTF-8 2-byte index-2 values, not right-shifted + for (i = 0; i < 0xc2 - 0xc0; i++) + { // C0..C1 + data[destIdx++] = (dataMove + BAD_UTF8_DATA_OFFSET); + } + + for (; i < 0xe0 - 0xc0; i++) + { // C2..DF + data[destIdx++] = (dataMove + _index2[i << (6 - SHIFT_2)]); + } + + if (_highStart > 0x10000) + { + var index1Length = (_highStart - 0x10000) >> SHIFT_1; + var index2Offset = INDEX_2_BMP_LENGTH + UTF8_2B_INDEX_2_LENGTH + index1Length; + + // write 16-bit index-1 values for supplementary code points + for (i = 0; i < index1Length; i++) + { + data[destIdx++] = (INDEX_2_OFFSET + _index1[i + OMITTED_BMP_INDEX_1_LENGTH]); + } + + // write the index-2 array values for supplementary code points, + // shifted right by INDEX_SHIFT, after adding dataMove + for (i = 0; i < _index2Length - index2Offset; i++) + { + data[destIdx++] = ((dataMove + _index2[index2Offset + i]) >> INDEX_SHIFT); + } + } + + // write 16-bit data values + for (i = 0; i < _dataLength; i++) + { + data[destIdx++] = (int)_data[i]; + } + + return new UnicodeTrie(data, _highStart, _errorValue); + } + + private bool IsInNullBlock(int c, bool forLSCP) + { + int i2; + if (((c & 0xfffffc00) == 0xd800) && forLSCP) + { + i2 = (LSCP_INDEX_2_OFFSET - (0xd800 >> SHIFT_2)) + (c >> SHIFT_2); + } + else + { + i2 = _index1[c >> SHIFT_1] + ((c >> SHIFT_2) & INDEX_2_MASK); + } + + var block = _index2[i2]; + return block == _dataNullOffset; + } + + private int AllocIndex2Block() + { + var newBlock = _index2Length; + var newTop = newBlock + INDEX_2_BLOCK_LENGTH; + if (newTop > _index2.Length) + { + // Should never occur. + // Either MAX_BUILD_TIME_INDEX_LENGTH is incorrect, + // or the code writes more values than should be possible. + throw new InvalidOperationException("Internal error in Trie2 creation."); + } + + _index2Length = newTop; + Array.Copy(_index2, _index2NullOffset, _index2, newBlock, INDEX_2_BLOCK_LENGTH); + + return newBlock; + } + + private int GetIndex2Block(int c, bool forLSCP) + { + if ((c >= 0xd800) && (c < 0xdc00) && forLSCP) + { + return LSCP_INDEX_2_OFFSET; + } + + var i1 = c >> SHIFT_1; + var i2 = _index1[i1]; + if (i2 == _index2NullOffset) + { + i2 = AllocIndex2Block(); + _index1[i1] = i2; + } + + return i2; + } + + private bool IsWritableBlock(int block) + { + return (block != _dataNullOffset) && (_map[block >> SHIFT_2] == 1); + } + + private int AllocDataBlock(int copyBlock) + { + int newBlock; + if (_firstFreeBlock != 0) + { + // get the first free block + newBlock = _firstFreeBlock; + _firstFreeBlock = -_map[newBlock >> SHIFT_2]; + } + else + { + // get a new block from the high end + newBlock = _dataLength; + var newTop = newBlock + DATA_BLOCK_LENGTH; + if (newTop > _dataCapacity) + { + // out of memory in the data array + int capacity; + if (_dataCapacity < MEDIUM_DATA_LENGTH) + { + capacity = MEDIUM_DATA_LENGTH; + } + else if (_dataCapacity < MAX_DATA_LENGTH_BUILDTIME) + { + capacity = MAX_DATA_LENGTH_BUILDTIME; + } + else + { + // Should never occur. + // Either MAX_DATA_LENGTH_BUILDTIME is incorrect, + // or the code writes more values than should be possible. + throw new InvalidOperationException("Internal error in Trie2 creation."); + } + + var newData = new UInt32[capacity]; + Array.Copy(_data, newData, _dataLength); + _data = newData; + _dataCapacity = capacity; + } + + _dataLength = newTop; + } + + Array.Copy(_data, copyBlock, _data, newBlock, DATA_BLOCK_LENGTH); + //_data.set(_data.subarray(copyBlock, copyBlock + DATA_BLOCK_LENGTH), newBlock); + _map[newBlock >> SHIFT_2] = 0; + return newBlock; + } + + private void ReleaseDataBlock(int block) + { + // put this block at the front of the free-block chain + _map[block >> SHIFT_2] = -_firstFreeBlock; + _firstFreeBlock = block; + } + + private void SetIndex2Entry(int i2, int block) + { + ++_map[block >> SHIFT_2]; // increment first, in case block == oldBlock! + var oldBlock = _index2[i2]; + if (--_map[oldBlock >> SHIFT_2] == 0) + { + ReleaseDataBlock(oldBlock); + } + + _index2[i2] = block; + } + + private int GetDataBlock(int c, bool forLSCP) + { + var i2 = GetIndex2Block(c, forLSCP); + i2 += (c >> SHIFT_2) & INDEX_2_MASK; + + var oldBlock = _index2[i2]; + if (IsWritableBlock(oldBlock)) + { + return oldBlock; + } + + // allocate a new data block + var newBlock = AllocDataBlock(oldBlock); + SetIndex2Entry(i2, newBlock); + return newBlock; + } + + private void FillBlock(int block, int start, int limit, uint value, uint initialValue, bool overwrite) + { + int i; + if (overwrite) + { + for (i = block + start; i < block + limit; i++) + { + _data[i] = value; + } + } + else + { + for (i = block + start; i < block + limit; i++) + { + if (_data[i] == initialValue) + { + _data[i] = value; + } + } + } + } + + private void WriteBlock(int block, uint value) + { + var limit = block + DATA_BLOCK_LENGTH; + while (block < limit) + { + _data[block++] = value; + } + } + + private int FindHighStart(uint highValue) + { + int prevBlock, prevI2Block; + + // set variables for previous range + if (highValue == _initialValue) + { + prevI2Block = _index2NullOffset; + prevBlock = _dataNullOffset; + } + else + { + prevI2Block = -1; + prevBlock = -1; + } + + int prev = 0x110000; + + // enumerate index-2 blocks + var i1 = INDEX_1_LENGTH; + var c = prev; + while (c > 0) + { + var i2Block = _index1[--i1]; + if (i2Block == prevI2Block) + { + // the index-2 block is the same as the previous one, and filled with highValue + c -= CP_PER_INDEX_1_ENTRY; + continue; + } + + prevI2Block = i2Block; + if (i2Block == _index2NullOffset) + { + // this is the null index-2 block + if (highValue != _initialValue) + { + return c; + } + c -= CP_PER_INDEX_1_ENTRY; + } + else + { + // enumerate data blocks for one index-2 block + var i2 = INDEX_2_BLOCK_LENGTH; + while (i2 > 0) + { + var block = _index2[i2Block + --i2]; + if (block == prevBlock) + { + // the block is the same as the previous one, and filled with highValue + c -= DATA_BLOCK_LENGTH; + continue; + } + + prevBlock = block; + if (block == _dataNullOffset) + { + // this is the null data block + if (highValue != _initialValue) + { + return c; + } + c -= DATA_BLOCK_LENGTH; + } + else + { + var j = DATA_BLOCK_LENGTH; + while (j > 0) + { + var value = _data[block + --j]; + if (value != highValue) + { + return c; + } + --c; + } + } + } + } + } + + // deliver last range + return 0; + } + + private int FindSameDataBlock(int dataLength, int otherBlock, int blockLength) + { + // ensure that we do not even partially get past dataLength + dataLength -= blockLength; + var block = 0; + while (block <= dataLength) + { + if (EqualSequence(_data, block, otherBlock, blockLength)) + { + return block; + } + block += DATA_GRANULARITY; + } + + return -1; + } + + private int FindSameIndex2Block(int index2Length, int otherBlock) { + // ensure that we do not even partially get past index2Length + index2Length -= INDEX_2_BLOCK_LENGTH; + for (var block = 0; block <= index2Length; block++) + { + if (EqualSequence(_index2, block, otherBlock, INDEX_2_BLOCK_LENGTH)) + { + return block; + } + } + + return -1; + } + + private void CompactData() + { + // do not compact linear-ASCII data + var newStart = DATA_START_OFFSET; + var start = 0; + var i = 0; + + while (start < newStart) + { + _map[i++] = start; + start += DATA_BLOCK_LENGTH; + } + + // Start with a block length of 64 for 2-byte UTF-8, + // then switch to DATA_BLOCK_LENGTH. + var blockLength = 64; + var blockCount = blockLength >> SHIFT_2; + start = newStart; + while (start < _dataLength) + { + // start: index of first entry of current block + // newStart: index where the current block is to be moved + // (right after current end of already-compacted data) + int mapIndex, movedStart; + if (start == DATA_0800_OFFSET) + { + blockLength = DATA_BLOCK_LENGTH; + blockCount = 1; + } + + // skip blocks that are not used + if (_map[start >> SHIFT_2] <= 0) + { + // advance start to the next block + start += blockLength; + + // leave newStart with the previous block! + continue; + } + + // search for an identical block + if ((movedStart = FindSameDataBlock(newStart, start, blockLength)) >= 0) + { + // found an identical block, set the other block's index value for the current block + mapIndex = start >> SHIFT_2; + for (i = blockCount; i > 0; i--) + { + _map[mapIndex++] = movedStart; + movedStart += DATA_BLOCK_LENGTH; + } + + // advance start to the next block + start += blockLength; + + // leave newStart with the previous block! + continue; + } + + // see if the beginning of this block can be overlapped with the end of the previous block + // look for maximum overlap (modulo granularity) with the previous, adjacent block + var overlap = blockLength - DATA_GRANULARITY; + while ((overlap > 0) && !EqualSequence(_data, (newStart - overlap), start, overlap)) + { + overlap -= DATA_GRANULARITY; + } + + if ((overlap > 0) || (newStart < start)) + { + // some overlap, or just move the whole block + movedStart = newStart - overlap; + mapIndex = start >> SHIFT_2; + + for (i = blockCount; i > 0; i--) + { + _map[mapIndex++] = movedStart; + movedStart += DATA_BLOCK_LENGTH; + } + + // move the non-overlapping indexes to their new positions + start += overlap; + for (i = blockLength - overlap; i > 0; i--) + { + _data[newStart++] = _data[start++]; + } + + } + else + { // no overlap && newStart==start + mapIndex = start >> SHIFT_2; + for (i = blockCount; i > 0; i--) + { + _map[mapIndex++] = start; + start += DATA_BLOCK_LENGTH; + } + + newStart = start; + } + } + + // now adjust the index-2 table + i = 0; + while (i < _index2Length) + { + // Gap indexes are invalid (-1). Skip over the gap. + if (i == INDEX_GAP_OFFSET) + { + i += INDEX_GAP_LENGTH; + } + _index2[i] = _map[_index2[i] >> SHIFT_2]; + ++i; + } + + _dataNullOffset = _map[_dataNullOffset >> SHIFT_2]; + + // ensure dataLength alignment + while ((newStart & (DATA_GRANULARITY - 1)) != 0) + { + _data[newStart++] = _initialValue; + } + _dataLength = newStart; + } + + private void CompactIndex2() + { + // do not compact linear-BMP index-2 blocks + var newStart = INDEX_2_BMP_LENGTH; + var start = 0; + var i = 0; + + while (start < newStart) + { + _map[i++] = start; + start += INDEX_2_BLOCK_LENGTH; + } + + // Reduce the index table gap to what will be needed at runtime. + newStart += UTF8_2B_INDEX_2_LENGTH + ((_highStart - 0x10000) >> SHIFT_1); + start = INDEX_2_NULL_OFFSET; + while (start < _index2Length) + { + // start: index of first entry of current block + // newStart: index where the current block is to be moved + // (right after current end of already-compacted data) + + // search for an identical block + int movedStart; + if ((movedStart = FindSameIndex2Block(newStart, start)) >= 0) + { + // found an identical block, set the other block's index value for the current block + _map[start >> SHIFT_1_2] = movedStart; + + // advance start to the next block + start += INDEX_2_BLOCK_LENGTH; + + // leave newStart with the previous block! + continue; + } + + // see if the beginning of this block can be overlapped with the end of the previous block + // look for maximum overlap with the previous, adjacent block + var overlap = INDEX_2_BLOCK_LENGTH - 1; + while ((overlap > 0) && !EqualSequence(_index2, (newStart - overlap), start, overlap)) + { + --overlap; + } + + if ((overlap > 0) || (newStart < start)) + { + // some overlap, or just move the whole block + _map[start >> SHIFT_1_2] = newStart - overlap; + + // move the non-overlapping indexes to their new positions + start += overlap; + for (i = INDEX_2_BLOCK_LENGTH - overlap; i > 0; i--) + { + _index2[newStart++] = _index2[start++]; + } + + } + else + { // no overlap && newStart==start + _map[start >> SHIFT_1_2] = start; + start += INDEX_2_BLOCK_LENGTH; + newStart = start; + } + } + + // now adjust the index-1 table + for (i = 0; i < INDEX_1_LENGTH; i++) + { + _index1[i] = _map[_index1[i] >> SHIFT_1_2]; + } + + _index2NullOffset = _map[_index2NullOffset >> SHIFT_1_2]; + + // Ensure data table alignment: + // Needs to be granularity-aligned for 16-bit trie + // (so that dataMove will be down-shiftable), + // and 2-aligned for uint32_t data. + + // Arbitrary value: 0x3fffc not possible for real data. + while ((newStart & ((DATA_GRANULARITY - 1) | 1)) != 0) + { + _index2[newStart++] = 0x0000ffff << INDEX_SHIFT; + } + + _index2Length = newStart; + } + + private void Compact() + { + // find highStart and round it up + var highValue = Get(0x10ffff); + var highStart = FindHighStart(highValue); + highStart = (highStart + (CP_PER_INDEX_1_ENTRY - 1)) & ~(CP_PER_INDEX_1_ENTRY - 1); + if (highStart == 0x110000) + { + highValue = _errorValue; + } + + // Set trie->highStart only after utrie2_get32(trie, highStart). + // Otherwise utrie2_get32(trie, highStart) would try to read the highValue. + _highStart = highStart; + if (_highStart < 0x110000) + { + // Blank out [highStart..10ffff] to release associated data blocks. + var suppHighStart = _highStart <= 0x10000 ? 0x10000 : _highStart; + SetRange(suppHighStart, 0x10ffff, _initialValue); + } + + CompactData(); + + if (_highStart > 0x10000) + { + CompactIndex2(); + } + + // Store the highValue in the data array and round up the dataLength. + // Must be done after compactData() because that assumes that dataLength + // is a multiple of DATA_BLOCK_LENGTH. + _data[_dataLength++] = highValue; + while ((_dataLength & (DATA_GRANULARITY - 1)) != 0) + { + _data[_dataLength++] = _initialValue; + } + + _isCompacted = true; + } + + private static bool EqualSequence(IReadOnlyList a, int s, int t, int length) + { + for (var i = 0; i < length; i++) + { + if (a[s + i] != a[t + i]) + { + return false; + } + } + + return true; + } + + private static bool EqualSequence(IReadOnlyList a, int s, int t, int length) + { + for (var i = 0; i < length; i++) + { + if (a[s + i] != a[t + i]) + { + return false; + } + } + + return true; + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextHitTestResult.cs b/src/Avalonia.Visuals/Media/TextHitTestResult.cs index bf6b2e53db..537dfed49d 100644 --- a/src/Avalonia.Visuals/Media/TextHitTestResult.cs +++ b/src/Avalonia.Visuals/Media/TextHitTestResult.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Media { /// diff --git a/src/Avalonia.Visuals/Media/TextTrimming.cs b/src/Avalonia.Visuals/Media/TextTrimming.cs new file mode 100644 index 0000000000..f8a63dfede --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextTrimming.cs @@ -0,0 +1,23 @@ +namespace Avalonia.Media +{ + /// + /// Describes how text is trimmed when it overflows. + /// + public enum TextTrimming + { + /// + /// Text is not trimmed. + /// + None, + + /// + /// Text is trimmed at a character boundary. An ellipsis (...) is drawn in place of remaining text. + /// + CharacterEllipsis, + + /// + /// Text is trimmed at a word boundary. An ellipsis (...) is drawn in place of remaining text. + /// + WordEllipsis + } +} diff --git a/src/Avalonia.Visuals/Media/TextWrapping.cs b/src/Avalonia.Visuals/Media/TextWrapping.cs index 7d50e10799..56df3670bd 100644 --- a/src/Avalonia.Visuals/Media/TextWrapping.cs +++ b/src/Avalonia.Visuals/Media/TextWrapping.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Media { /// diff --git a/src/Avalonia.Visuals/Media/TileBrush.cs b/src/Avalonia.Visuals/Media/TileBrush.cs index 47f20fa285..0a4bc0e56d 100644 --- a/src/Avalonia.Visuals/Media/TileBrush.cs +++ b/src/Avalonia.Visuals/Media/TileBrush.cs @@ -1,6 +1,3 @@ -// 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.Visuals.Media.Imaging; namespace Avalonia.Media diff --git a/src/Avalonia.Visuals/Media/Transform.cs b/src/Avalonia.Visuals/Media/Transform.cs index 7a70657ce0..7cf1b35ada 100644 --- a/src/Avalonia.Visuals/Media/Transform.cs +++ b/src/Avalonia.Visuals/Media/Transform.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Animation; using Avalonia.Animation.Animators; @@ -11,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)); } /// @@ -28,6 +26,16 @@ namespace Avalonia.Media /// public abstract Matrix Value { get; } + /// + /// Parses a string. + /// + /// Six comma-delimited double values that describe the new . For details check + /// The . + public static Transform Parse(string s) + { + return new MatrixTransform(Matrix.Parse(s)); + } + /// /// Raises the event. /// @@ -35,5 +43,14 @@ namespace Avalonia.Media { Changed?.Invoke(this, EventArgs.Empty); } + + /// + /// Returns a String representing this transform matrix instance. + /// + /// The string representation. + public override string ToString() + { + return Value.ToString(); + } } } 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/TransformGroup.cs b/src/Avalonia.Visuals/Media/TransformGroup.cs index 3a47f40045..8e3cfec274 100644 --- a/src/Avalonia.Visuals/Media/TransformGroup.cs +++ b/src/Avalonia.Visuals/Media/TransformGroup.cs @@ -1,6 +1,3 @@ -// 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.Collections; using Avalonia.Metadata; @@ -11,7 +8,7 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly AvaloniaProperty ChildrenProperty = + public static readonly StyledProperty ChildrenProperty = AvaloniaProperty.Register(nameof(Children)); public TransformGroup() 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/TranslateTransform.cs b/src/Avalonia.Visuals/Media/TranslateTransform.cs index 6ffbdb9623..d6d6809f3d 100644 --- a/src/Avalonia.Visuals/Media/TranslateTransform.cs +++ b/src/Avalonia.Visuals/Media/TranslateTransform.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.VisualTree; diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs index 37ac0953bf..7618598a3f 100644 --- a/src/Avalonia.Visuals/Media/Typeface.cs +++ b/src/Avalonia.Visuals/Media/Typeface.cs @@ -1,39 +1,33 @@ using System; +using System.Diagnostics; +using JetBrains.Annotations; namespace Avalonia.Media { /// /// Represents a typeface. /// - public class Typeface + [DebuggerDisplay("Name = {FontFamily.Name}, Weight = {Weight}, Style = {Style}")] + public class Typeface : IEquatable { - public static readonly Typeface Default = new Typeface(FontFamily.Default); + private GlyphTypeface _glyphTypeface; /// /// Initializes a new instance of the class. /// /// The font family. - /// The font size, in DIPs. - /// The font style. /// The font weight. - public Typeface( - FontFamily fontFamily, - double fontSize = 12, - FontStyle style = FontStyle.Normal, - FontWeight weight = FontWeight.Normal) + /// The font style. + public Typeface([NotNull]FontFamily fontFamily, + FontWeight weight = FontWeight.Normal, + FontStyle style = FontStyle.Normal) { - if (fontSize <= 0) - { - throw new ArgumentException("Font size must be > 0."); - } - if (weight <= 0) { throw new ArgumentException("Font weight must be > 0."); } FontFamily = fontFamily; - FontSize = fontSize; Style = style; Weight = weight; } @@ -42,28 +36,22 @@ namespace Avalonia.Media /// Initializes a new instance of the class. /// /// The name of the font family. - /// The font size, in DIPs. /// The font style. /// The font weight. - public Typeface( - string fontFamilyName, - double fontSize = 12, - FontStyle style = FontStyle.Normal, - FontWeight weight = FontWeight.Normal) - : this(new FontFamily(fontFamilyName), fontSize, style, weight) + public Typeface(string fontFamilyName, + FontWeight weight = FontWeight.Normal, + FontStyle style = FontStyle.Normal) + : this(new FontFamily(fontFamilyName), weight, style) { } + public static Typeface Default => FontManager.Current?.GetOrAddTypeface(FontFamily.Default); + /// /// Gets the font family. /// public FontFamily FontFamily { get; } - /// - /// Gets the size of the font in DIPs. - /// - public double FontSize { get; } - /// /// Gets the font style. /// @@ -73,5 +61,59 @@ namespace Avalonia.Media /// Gets the font weight. /// public FontWeight Weight { get; } + + /// + /// Gets the glyph typeface. + /// + /// + /// The glyph typeface. + /// + public GlyphTypeface GlyphTypeface => _glyphTypeface ?? (_glyphTypeface = new GlyphTypeface(this)); + + public static bool operator !=(Typeface a, Typeface b) + { + return !(a == b); + } + + public static bool operator ==(Typeface a, Typeface b) + { + if (ReferenceEquals(a, b)) + { + return true; + } + + return !(a is null) && a.Equals(b); + } + + public override bool Equals(object obj) + { + if (obj is Typeface typeface) + { + return Equals(typeface); + } + + return false; + } + + public bool Equals(Typeface other) + { + if (other is null) + { + return false; + } + + return FontFamily.Equals(other.FontFamily) && Style == other.Style && Weight == other.Weight; + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = (FontFamily != null ? FontFamily.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (int)Style; + hashCode = (hashCode * 397) ^ (int)Weight; + return hashCode; + } + } } } diff --git a/src/Avalonia.Visuals/Media/VisualBrush.cs b/src/Avalonia.Visuals/Media/VisualBrush.cs index 963ba8f4a1..c665507652 100644 --- a/src/Avalonia.Visuals/Media/VisualBrush.cs +++ b/src/Avalonia.Visuals/Media/VisualBrush.cs @@ -1,6 +1,3 @@ -// 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.Media.Immutable; using Avalonia.VisualTree; diff --git a/src/Avalonia.Visuals/Platform/IBitmapImpl.cs b/src/Avalonia.Visuals/Platform/IBitmapImpl.cs index 1c8ac31955..1e68bc477d 100644 --- a/src/Avalonia.Visuals/Platform/IBitmapImpl.cs +++ b/src/Avalonia.Visuals/Platform/IBitmapImpl.cs @@ -1,6 +1,3 @@ -// 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 System; using System.IO; diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index f74c551fe0..c87946b3ea 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Media; using Avalonia.Rendering.SceneGraph; @@ -33,7 +30,7 @@ namespace Avalonia.Platform /// The rect in the image to draw. /// The rect in the output to draw to. /// The bitmap interpolation mode. - void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default); + void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default); /// /// Draws a bitmap image. @@ -42,7 +39,7 @@ namespace Avalonia.Platform /// The opacity mask to draw with. /// The destination rect for the opacity mask. /// The rect in the output to draw to. - void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect); + void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect); /// /// Draws a line. @@ -61,12 +58,18 @@ namespace Avalonia.Platform void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry); /// - /// Draws the outline of a rectangle. + /// Draws a rectangle with the specified Brush and Pen. /// - /// The pen. + /// The brush used to fill the rectangle, or null for no fill. + /// The pen used to stroke the rectangle, or null for no stroke. /// The rectangle bounds. - /// The corner radius. - void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f); + /// Box shadow effect parameters + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, + BoxShadows boxShadow = default); /// /// Draws text. @@ -77,12 +80,12 @@ namespace Avalonia.Platform void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text); /// - /// Draws a filled rectangle. + /// Draws a glyph run. /// - /// The brush. - /// The rectangle bounds. - /// The corner radius. - void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f); + /// The foreground. + /// The glyph run. + /// The baseline origin of the glyph run. + void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin); /// /// Creates a new that can be used as a render layer @@ -104,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/IFontManagerImpl.cs b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs new file mode 100644 index 0000000000..3d0bea8c80 --- /dev/null +++ b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Media; +using Avalonia.Media.Fonts; + +namespace Avalonia.Platform +{ + public interface IFontManagerImpl + { + /// + /// Gets the system's default font family's name. + /// + string GetDefaultFontFamilyName(); + + /// + /// Get all installed fonts in the system. + /// If true the font collection is updated. + /// + IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); + + /// + /// Tries to match a specified character to a typeface that supports specified font properties. + /// + /// The codepoint to match against. + /// The font weight. + /// The font style. + /// The font family. This is optional and used for fallback lookup. + /// The culture. + /// The matching font key. + /// + /// True, if the could match the character to specified parameters, False otherwise. + /// + bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + FontFamily fontFamily, CultureInfo culture, out FontKey fontKey); + + /// + /// Creates a glyph typeface. + /// + /// The typeface. + /// 0 + /// The created glyph typeface. Can be Null if it was not possible to create a glyph typeface. + /// + IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); + } +} diff --git a/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs b/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs index 2543e4f363..330fcac50c 100644 --- a/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using Avalonia.Media; diff --git a/src/Avalonia.Visuals/Platform/IGeometryContext.cs b/src/Avalonia.Visuals/Platform/IGeometryContext.cs index ac63837428..3f7273b53b 100644 --- a/src/Avalonia.Visuals/Platform/IGeometryContext.cs +++ b/src/Avalonia.Visuals/Platform/IGeometryContext.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Media; diff --git a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs index b762859d1d..7490ad912a 100644 --- a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs +++ b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs @@ -1,6 +1,3 @@ -// 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.Media; namespace Avalonia.Platform diff --git a/src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs b/src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs new file mode 100644 index 0000000000..08786d9689 --- /dev/null +++ b/src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs @@ -0,0 +1,9 @@ +using System; + +namespace Avalonia.Platform +{ + /// + /// Actual implementation of a glyph run that stores platform dependent resources. + /// + public interface IGlyphRunImpl : IDisposable { } +} diff --git a/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs b/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs new file mode 100644 index 0000000000..6afd79d29c --- /dev/null +++ b/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs @@ -0,0 +1,91 @@ +using System; + +namespace Avalonia.Platform +{ + public interface IGlyphTypefaceImpl : IDisposable + { + /// + /// Gets the font design units per em. + /// + short DesignEmHeight { get; } + + /// + /// Gets the recommended distance above the baseline in design em size. + /// + int Ascent { get; } + + /// + /// Gets the recommended distance under the baseline in design em size. + /// + int Descent { get; } + + /// + /// Gets the recommended additional space between two lines of text in design em size. + /// + int LineGap { get; } + + /// + /// Gets a value that indicates the distance of the underline from the baseline in design em size. + /// + int UnderlinePosition { get; } + + /// + /// Gets a value that indicates the thickness of the underline in design em size. + /// + int UnderlineThickness { get; } + + /// + /// Gets a value that indicates the distance of the strikethrough from the baseline in design em size. + /// + int StrikethroughPosition { get; } + + /// + /// Gets a value that indicates the thickness of the underline in design em size. + /// + int StrikethroughThickness { get; } + + /// + /// A value indicating whether all glyphs in the font have the same advancement. + /// + bool IsFixedPitch { get; } + + /// + /// Returns an glyph index for the specified codepoint. + /// + /// + /// Returns 0 if a glyph isn't found. + /// + /// The codepoint. + /// + /// A glyph index. + /// + ushort GetGlyph(uint codepoint); + + /// + /// Returns an array of glyph indices. Codepoints that are not represented by the font are returned as 0. + /// + /// The codepoints to map. + /// + /// An array of glyph indices. + /// + ushort[] GetGlyphs(ReadOnlySpan codepoints); + + /// + /// Returns the glyph advance for the specified glyph. + /// + /// The glyph. + /// + /// The advance. + /// + int GetGlyphAdvance(ushort glyph); + + /// + /// Returns an array of glyph advances in design em size. + /// + /// The glyph indices. + /// + /// An array of glyph advances. + /// + int[] GetGlyphAdvances(ReadOnlySpan glyphs); + } +} diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index 87db9251e1..ba30272b7b 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -1,10 +1,9 @@ -// 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 System; using System.Collections.Generic; using System.IO; using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Platform { @@ -13,16 +12,12 @@ namespace Avalonia.Platform /// public interface IPlatformRenderInterface { - /// - /// Get all installed fonts in the system - /// - IEnumerable InstalledFontNames { get; } - /// /// Creates a formatted text implementation. /// /// The text. /// The base typeface. + /// The font size. /// The text alignment. /// The text wrapping mode. /// The text layout constraints. @@ -31,6 +26,7 @@ namespace Avalonia.Platform IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, @@ -93,17 +89,37 @@ namespace Avalonia.Platform /// /// Loads a bitmap implementation from a file.. /// - /// The filename of the bitmap. + /// The filename of the bitmap. /// An . IBitmapImpl LoadBitmap(string fileName); /// /// Loads a bitmap implementation from a file.. /// - /// The stream to read the bitmap from. + /// The stream to read the bitmap from. /// An . IBitmapImpl LoadBitmap(Stream stream); + /// + /// Loads a bitmap implementation from a stream to a specified width maintaining aspect ratio. + /// + /// The stream to read the bitmap from. + /// The desired width of the resulting bitmap. + /// The to use should resizing be required. + /// An . + IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); + + /// + /// Loads a bitmap implementation from a stream to a specified height maintaining aspect ratio. + /// + /// The stream to read the bitmap from. + /// The desired height of the resulting bitmap. + /// The to use should resizing be required. + /// An . + IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); + + IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); + /// /// Loads a bitmap implementation from a pixels in memory. /// @@ -114,5 +130,15 @@ namespace Avalonia.Platform /// The number of bytes per row. /// An . IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride); + + /// + /// Creates a platform implementation of a glyph run. + /// + /// The glyph run. + /// The glyph run's width. + /// + IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width); + + bool SupportsIndividualRoundRects { get; } } } diff --git a/src/Avalonia.Visuals/Platform/IPlatformSettings.cs b/src/Avalonia.Visuals/Platform/IPlatformSettings.cs index e52135433b..bcb00df5d6 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformSettings.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformSettings.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Platform diff --git a/src/Avalonia.Visuals/Platform/IRenderTarget.cs b/src/Avalonia.Visuals/Platform/IRenderTarget.cs index 516bea782e..68b3b4c17d 100644 --- a/src/Avalonia.Visuals/Platform/IRenderTarget.cs +++ b/src/Avalonia.Visuals/Platform/IRenderTarget.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Rendering; diff --git a/src/Avalonia.Visuals/Platform/IRenderTargetBitmapImpl.cs b/src/Avalonia.Visuals/Platform/IRenderTargetBitmapImpl.cs index 6464fdd41b..9add07afe3 100644 --- a/src/Avalonia.Visuals/Platform/IRenderTargetBitmapImpl.cs +++ b/src/Avalonia.Visuals/Platform/IRenderTargetBitmapImpl.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Platform { diff --git a/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs b/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs index da9505cd2d..4587979308 100644 --- a/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Platform { /// diff --git a/src/Avalonia.Visuals/Platform/IStreamGeometryImpl.cs b/src/Avalonia.Visuals/Platform/IStreamGeometryImpl.cs index 956864fc73..5b070fde02 100644 --- a/src/Avalonia.Visuals/Platform/IStreamGeometryImpl.cs +++ b/src/Avalonia.Visuals/Platform/IStreamGeometryImpl.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Platform { /// diff --git a/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs b/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs new file mode 100644 index 0000000000..4d770a6c6e --- /dev/null +++ b/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs @@ -0,0 +1,20 @@ +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Utility; + +namespace Avalonia.Platform +{ + /// + /// An abstraction that is used produce shaped text. + /// + public interface ITextShaperImpl + { + /// + /// Shapes the specified region within the text and returns a resulting glyph run. + /// + /// The text. + /// The text format. + /// A shaped glyph run. + GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat); + } +} diff --git a/src/Avalonia.Visuals/Platform/PathGeometryContext.cs b/src/Avalonia.Visuals/Platform/PathGeometryContext.cs index 19322b9620..391a43d1cf 100644 --- a/src/Avalonia.Visuals/Platform/PathGeometryContext.cs +++ b/src/Avalonia.Visuals/Platform/PathGeometryContext.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using Avalonia.Media; using Avalonia.Platform; diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs index d92f8b0fc4..4cce2c925b 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Globalization; using Avalonia.Animation.Animators; @@ -175,7 +172,7 @@ namespace Avalonia /// Parses a string. /// /// The string. - /// The . + /// The . public static Point Parse(string s) { using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Point.")) diff --git a/src/Avalonia.Visuals/Points.cs b/src/Avalonia.Visuals/Points.cs index 867d3d4d24..b655dbcb38 100644 --- a/src/Avalonia.Visuals/Points.cs +++ b/src/Avalonia.Visuals/Points.cs @@ -1,6 +1,3 @@ -// 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.Collections; namespace Avalonia diff --git a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs index 05c3d7e62a..5d802c27b9 100644 --- a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs @@ -1,6 +1,3 @@ -// 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 System.Reflection; using System.Runtime.CompilerServices; using Avalonia.Metadata; @@ -8,7 +5,10 @@ using Avalonia.Metadata; [assembly: InternalsVisibleTo("Avalonia.Visuals.UnitTests")] [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")] -[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")] \ No newline at end of file +[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")] +[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests")] diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs index 4dfd641525..d1110e0613 100644 --- a/src/Avalonia.Visuals/Rect.cs +++ b/src/Avalonia.Visuals/Rect.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Globalization; using Avalonia.Animation.Animators; @@ -135,6 +132,16 @@ namespace Avalonia /// Gets the bottom position of the rectangle. /// public double Bottom => _y + _height; + + /// + /// Gets the left position. + /// + public double Left => _x; + + /// + /// Gets the top position. + /// + public double Top => _y; /// /// Gets the top left point of the rectangle. @@ -414,11 +421,50 @@ namespace Avalonia } /// - /// Gets the union of two rectangles. - /// - /// The other rectangle. - /// The union. - public Rect Union(Rect rect) + /// Normalizes the rectangle so both the and are positive, without changing the location of the rectangle + /// + /// Normalized Rect + /// + /// Empty rect will be return when Rect contains invalid values. Like NaN. + /// + public Rect Normalize() + { + Rect rect = this; + + if(double.IsNaN(rect.Right) || double.IsNaN(rect.Bottom) || + double.IsNaN(rect.X) || double.IsNaN(rect.Y) || + double.IsNaN(Height) || double.IsNaN(Width)) + { + return Rect.Empty; + } + + if (rect.Width < 0) + { + var x = X + Width; + var width = X - x; + + rect = rect.WithX(x).WithWidth(width); + } + + if (rect.Height < 0) + { + var y = Y + Height; + var height = Y - y; + + rect = rect.WithY(y).WithHeight(height); + } + + return rect; + } + + + /// + /// Gets the union of two rectangles. + /// + /// The other rectangle. + /// The union. + public Rect Union(Rect rect) { if (IsEmpty) { diff --git a/src/Avalonia.Visuals/RelativePoint.cs b/src/Avalonia.Visuals/RelativePoint.cs index 2e8fb16bc1..097ea69be4 100644 --- a/src/Avalonia.Visuals/RelativePoint.cs +++ b/src/Avalonia.Visuals/RelativePoint.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Globalization; using Avalonia.Utilities; @@ -177,5 +174,16 @@ namespace Avalonia unit); } } + + /// + /// Returns a String representing this RelativePoint instance. + /// + /// The string representation. + public override string ToString() + { + return _unit == RelativeUnit.Absolute ? + _point.ToString() : + string.Format(CultureInfo.InvariantCulture, "{0}%, {1}%", _point.X * 100, _point.Y * 100); + } } } diff --git a/src/Avalonia.Visuals/RelativeRect.cs b/src/Avalonia.Visuals/RelativeRect.cs index d2e4b2dc26..1d1f53e299 100644 --- a/src/Avalonia.Visuals/RelativeRect.cs +++ b/src/Avalonia.Visuals/RelativeRect.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Globalization; using Avalonia.Utilities; diff --git a/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs b/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs index d0eb181c65..8d6c3e67c1 100644 --- a/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs +++ b/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Diagnostics; using System.Threading.Tasks; using Avalonia.Platform; diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index d9a68b236a..8c020fc073 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -38,6 +35,7 @@ namespace Avalonia.Rendering private IRef _currentDraw; private readonly IDeferredRendererLock _lock; private readonly object _sceneLock = new object(); + private readonly Action _updateSceneIfNeededDelegate; /// /// Initializes a new instance of the class. @@ -52,7 +50,7 @@ namespace Avalonia.Rendering IRenderLoop renderLoop, ISceneBuilder sceneBuilder = null, IDispatcher dispatcher = null, - IDeferredRendererLock rendererLock = null) + IDeferredRendererLock rendererLock = null) : base(true) { Contract.Requires(root != null); @@ -62,6 +60,7 @@ namespace Avalonia.Rendering Layers = new RenderLayers(); _renderLoop = renderLoop; _lock = rendererLock ?? new ManagedDeferredRendererLock(); + _updateSceneIfNeededDelegate = UpdateSceneIfNeeded; } /// @@ -76,7 +75,7 @@ namespace Avalonia.Rendering public DeferredRenderer( IVisual root, IRenderTarget renderTarget, - ISceneBuilder sceneBuilder = null) + ISceneBuilder sceneBuilder = null) : base(true) { Contract.Requires(root != null); Contract.Requires(renderTarget != null); @@ -86,6 +85,7 @@ namespace Avalonia.Rendering _sceneBuilder = sceneBuilder ?? new SceneBuilder(); Layers = new RenderLayers(); _lock = new ManagedDeferredRendererLock(); + _updateSceneIfNeededDelegate = UpdateSceneIfNeeded; } /// @@ -160,16 +160,23 @@ namespace Avalonia.Rendering /// public IEnumerable HitTest(Point p, IVisual root, Func filter) { - if (_renderLoop == null && (_dirty == null || _dirty.Count > 0)) - { - // When unit testing the renderLoop may be null, so update the scene manually. - UpdateScene(); - } + EnsureCanHitTest(); + //It's safe to access _scene here without a lock since //it's only changed from UI thread which we are currently on return _scene?.Item.HitTest(p, root, filter) ?? Enumerable.Empty(); } + /// + public IVisual HitTestFirst(Point p, IVisual root, Func filter) + { + EnsureCanHitTest(); + + //It's safe to access _scene here without a lock since + //it's only changed from UI thread which we are currently on + return _scene?.Item.HitTestFirst(p, root, filter); + } + /// public void Paint(Rect rect) { @@ -235,6 +242,15 @@ namespace Avalonia.Rendering internal Scene UnitTestScene() => _scene.Item; + private void EnsureCanHitTest() + { + if (_renderLoop == null && (_dirty == null || _dirty.Count > 0)) + { + // When unit testing the renderLoop may be null, so update the scene manually. + UpdateScene(); + } + } + private void Render(bool forceComposite) { using (var l = _lock.TryLock()) @@ -248,7 +264,8 @@ namespace Avalonia.Rendering try { var (scene, updated) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context); - + if (updated) + FpsTick(); using (scene) { if (scene?.Item != null) @@ -270,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; } @@ -305,17 +322,25 @@ namespace Avalonia.Rendering _lastSceneId = scene.Generation; + var isUiThread = Dispatcher.UIThread.CheckAccess(); // We have consumed the previously available scene, but there might be some dirty // rects since the last update. *If* we are on UI thread, we can force immediate scene // rebuild before rendering anything on-screen // We are calling the same method recursively here - if (!recursiveCall && Dispatcher.UIThread.CheckAccess() && NeedsUpdate) + if (!recursiveCall && isUiThread && NeedsUpdate) { UpdateScene(); var (rs, _) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context, true); return (rs, true); } + // We are rendering a new scene version, so it's highly likely + // that there is already a pending update for animations + // So we are scheduling an update call so UI thread could prepare a scene before + // the next render timer tick + if (!recursiveCall && !isUiThread) + Dispatcher.UIThread.Post(_updateSceneIfNeededDelegate, DispatcherPriority.Render); + // Indicate that we have updated the layers return (sceneRef.Clone(), true); } @@ -339,15 +364,21 @@ namespace Avalonia.Rendering node.BeginRender(context, isLayerRoot); - foreach (var operation in node.DrawOperations) + var drawOperations = node.DrawOperations; + var drawOperationsCount = drawOperations.Count; + for (int i = 0; i < drawOperationsCount; i++) { + var operation = drawOperations[i]; _currentDraw = operation; operation.Item.Render(context); _currentDraw = null; } - foreach (var child in node.Children) + var children = node.Children; + var childrenCount = children.Count; + for (int i = 0; i < childrenCount; i++) { + var child = children[i]; Render(context, (VisualNode)child, layer, clipBounds); } @@ -412,11 +443,12 @@ namespace Avalonia.Rendering private static Rect SnapToDevicePixels(Rect rect, double scale) { return new Rect( - Math.Floor(rect.X * scale) / scale, - Math.Floor(rect.Y * scale) / scale, - Math.Ceiling(rect.Width * scale) / scale, - Math.Ceiling(rect.Height * scale) / scale); - + new Point( + Math.Floor(rect.X * scale) / scale, + Math.Floor(rect.Y * scale) / scale), + new Point( + Math.Ceiling(rect.Right * scale) / scale, + Math.Ceiling(rect.Bottom * scale) / scale)); } private void RenderOverlay(Scene scene, ref IDrawingContextImpl parentContent) @@ -445,7 +477,7 @@ namespace Avalonia.Rendering foreach (var r in _dirtyRectsDisplay) { var brush = new ImmutableSolidColorBrush(Colors.Magenta, r.Opacity); - context.FillRectangle(brush, r.Rect); + context.DrawRectangle(brush,null, r.Rect); } } @@ -469,11 +501,11 @@ namespace Avalonia.Rendering if (layer.OpacityMask == null) { - context.DrawImage(bitmap, layer.Opacity, sourceRect, clientRect); + context.DrawBitmap(bitmap, layer.Opacity, sourceRect, clientRect); } else { - context.DrawImage(bitmap, layer.OpacityMask, layer.OpacityMaskRect, sourceRect); + context.DrawBitmap(bitmap, layer.OpacityMask, layer.OpacityMaskRect, sourceRect); } if (layer.GeometryClip != null) @@ -485,7 +517,7 @@ namespace Avalonia.Rendering if (_overlay != null) { var sourceRect = new Rect(0, 0, _overlay.Item.PixelSize.Width, _overlay.Item.PixelSize.Height); - context.DrawImage(_overlay, 0.5, sourceRect, clientRect); + context.DrawBitmap(_overlay, 0.5, sourceRect, clientRect); } if (DrawFps) @@ -515,6 +547,12 @@ namespace Avalonia.Rendering context = RenderTarget.CreateDrawingContext(this); } + private void UpdateSceneIfNeeded() + { + if(NeedsUpdate) + UpdateScene(); + } + private void UpdateScene() { Dispatcher.UIThread.VerifyAccess(); diff --git a/src/Avalonia.Visuals/Rendering/DirtyRects.cs b/src/Avalonia.Visuals/Rendering/DirtyRects.cs index d206d23a24..f4b6a6b6ce 100644 --- a/src/Avalonia.Visuals/Rendering/DirtyRects.cs +++ b/src/Avalonia.Visuals/Rendering/DirtyRects.cs @@ -1,7 +1,4 @@ -// 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 System.Collections; +using System.Collections; using System.Collections.Generic; namespace Avalonia.Rendering diff --git a/src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs b/src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs index 7199053b08..4c15de0312 100644 --- a/src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs +++ b/src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs @@ -1,3 +1,7 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.VisualTree; + namespace Avalonia.Rendering { /// @@ -9,4 +13,13 @@ namespace Avalonia.Rendering { bool HitTest(Point point); } + + public static class CustomSimpleHitTestExtensions + { + public static bool HitTestCustom(this IVisual visual, Point point) + => (visual as ICustomSimpleHitTest)?.HitTest(point) ?? visual.TransformedBounds?.Contains(point) == true; + + public static bool HitTestCustom(this IEnumerable children, Point point) + => children.Any(ctrl => ctrl.HitTestCustom(point)); + } } diff --git a/src/Avalonia.Visuals/Rendering/IRenderRoot.cs b/src/Avalonia.Visuals/Rendering/IRenderRoot.cs index 044911ca95..54e58bf39c 100644 --- a/src/Avalonia.Visuals/Rendering/IRenderRoot.cs +++ b/src/Avalonia.Visuals/Rendering/IRenderRoot.cs @@ -1,6 +1,3 @@ -// 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.Platform; using Avalonia.VisualTree; diff --git a/src/Avalonia.Visuals/Rendering/IRenderer.cs b/src/Avalonia.Visuals/Rendering/IRenderer.cs index 9ad7186dca..0c7440d159 100644 --- a/src/Avalonia.Visuals/Rendering/IRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/IRenderer.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using Avalonia.VisualTree; using System.Collections.Generic; @@ -50,6 +47,18 @@ namespace Avalonia.Rendering /// The visuals at the specified point, topmost first. IEnumerable HitTest(Point p, IVisual root, Func filter); + /// + /// Hit tests a location to find first visual at the specified point. + /// + /// The point, in client coordinates. + /// The root of the subtree to search. + /// + /// A filter predicate. If the predicate returns false then the visual and all its + /// children will be excluded from the results. + /// + /// The visual at the specified point, topmost first. + IVisual HitTestFirst(Point p, IVisual root, Func filter); + /// /// Informs the renderer that the z-ordering of a visual's children has changed. /// diff --git a/src/Avalonia.Visuals/Rendering/IRendererFactory.cs b/src/Avalonia.Visuals/Rendering/IRendererFactory.cs index 27af39e40d..11d89ef94c 100644 --- a/src/Avalonia.Visuals/Rendering/IRendererFactory.cs +++ b/src/Avalonia.Visuals/Rendering/IRendererFactory.cs @@ -1,7 +1,4 @@ -// 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. - - + namespace Avalonia.Rendering { /// diff --git a/src/Avalonia.Visuals/Rendering/IVisualBrushInitialize.cs b/src/Avalonia.Visuals/Rendering/IVisualBrushInitialize.cs index 2949c9c051..00449c5344 100644 --- a/src/Avalonia.Visuals/Rendering/IVisualBrushInitialize.cs +++ b/src/Avalonia.Visuals/Rendering/IVisualBrushInitialize.cs @@ -1,7 +1,4 @@ -// 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.Media; +using Avalonia.Media; namespace Avalonia.Rendering { diff --git a/src/Avalonia.Visuals/Rendering/IVisualBrushRenderer.cs b/src/Avalonia.Visuals/Rendering/IVisualBrushRenderer.cs index 84f540ca8e..1cd6515635 100644 --- a/src/Avalonia.Visuals/Rendering/IVisualBrushRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/IVisualBrushRenderer.cs @@ -1,7 +1,4 @@ -// 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.Media; +using Avalonia.Media; using Avalonia.Platform; namespace Avalonia.Rendering diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index 68d56eeedd..9ea1b84311 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Collections.Generic; using System.Linq; using Avalonia.Logging; @@ -81,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; } @@ -164,6 +161,11 @@ namespace Avalonia.Rendering return HitTest(root, p, filter); } + public IVisual HitTestFirst(Point p, IVisual root, Func filter) + { + return HitTest(root, p, filter).FirstOrDefault(); + } + /// public void RecalculateChildren(IVisual visual) => AddDirty(visual); @@ -287,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()) @@ -307,7 +313,9 @@ namespace Avalonia.Rendering if (!child.ClipToBounds || clipRect.Intersects(childBounds)) { - var childClipRect = clipRect.Translate(-childBounds.Position); + var childClipRect = child.RenderTransform == null + ? clipRect.Translate(-childBounds.Position) + : clipRect; Render(context, child, childClipRect); } else diff --git a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs index 259874423e..158fecbfc6 100644 --- a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs +++ b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs @@ -1,53 +1,11 @@ -// 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 System; using System.Threading; +using Avalonia.Utilities; namespace Avalonia.Rendering { - public class ManagedDeferredRendererLock : IDeferredRendererLock + public class ManagedDeferredRendererLock : DisposableLock, IDeferredRendererLock { - private readonly object _lock = new object(); - - /// - /// Tries to lock the target surface or window - /// - /// IDisposable if succeeded to obtain the lock - public IDisposable TryLock() - { - if (Monitor.TryEnter(_lock)) - return new UnlockDisposable(_lock); - return null; - } - - /// - /// Enters a waiting lock, only use from platform code, not from the renderer - /// - public IDisposable Lock() - { - Monitor.Enter(_lock); - return new UnlockDisposable(_lock); - } - - private sealed class UnlockDisposable : IDisposable - { - private object _lock; - - public UnlockDisposable(object @lock) - { - _lock = @lock; - } - - public void Dispose() - { - object @lock = Interlocked.Exchange(ref _lock, null); - - if (@lock != null) - { - Monitor.Exit(@lock); - } - } - } + } } 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/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs index 7b10fc1212..b37d5d660b 100644 --- a/src/Avalonia.Visuals/Rendering/RendererBase.cs +++ b/src/Avalonia.Visuals/Rendering/RendererBase.cs @@ -7,27 +7,33 @@ namespace Avalonia.Rendering { public class RendererBase { - private static readonly Typeface s_fpsTypeface = new Typeface("Arial", 18); + private readonly bool _useManualFpsCounting; + private static int s_fontSize = 18; private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); private int _framesThisSecond; private int _fps; private FormattedText _fpsText; private TimeSpan _lastFpsUpdate; - public RendererBase() + public RendererBase(bool useManualFpsCounting = false) { + _useManualFpsCounting = useManualFpsCounting; _fpsText = new FormattedText { - Typeface = s_fpsTypeface + Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily.Default), + FontSize = s_fontSize }; } + protected void FpsTick() => _framesThisSecond++; + protected void RenderFps(IDrawingContextImpl context, Rect clientRect, int? layerCount) { var now = _stopwatch.Elapsed; var elapsed = now - _lastFpsUpdate; - ++_framesThisSecond; + if (!_useManualFpsCounting) + ++_framesThisSecond; if (elapsed.TotalSeconds > 1) { @@ -49,7 +55,7 @@ namespace Avalonia.Rendering var rect = new Rect(clientRect.Right - size.Width, 0, size.Width, size.Height); context.Transform = Matrix.Identity; - context.FillRectangle(Brushes.Black, rect); + context.DrawRectangle(Brushes.Black,null, rect); context.DrawText(Brushes.White, rect.TopLeft, _fpsText.PlatformImpl); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs index b2c0581388..c559f05d70 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs @@ -1,7 +1,4 @@ -// 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 System.Collections.Generic; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.VisualTree; @@ -12,8 +9,8 @@ namespace Avalonia.Rendering.SceneGraph /// internal abstract class BrushDrawOperation : DrawOperation { - public BrushDrawOperation(Rect bounds, Matrix transform, IPen pen) - : base(bounds, transform, pen) + public BrushDrawOperation(Rect bounds, Matrix transform) + : base(bounds, transform) { } 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/CustomDrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs index 68e2237430..15e5660671 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs @@ -9,7 +9,7 @@ namespace Avalonia.Rendering.SceneGraph public Matrix Transform { get; } public ICustomDrawOperation Custom { get; } public CustomDrawOperation(ICustomDrawOperation custom, Matrix transform) - : base(custom.Bounds, transform, null) + : base(custom.Bounds, transform) { Transform = transform; Custom = custom; diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 3af56f5215..dfb21a0289 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Collections.Generic; using Avalonia.Media; using Avalonia.Platform; @@ -115,7 +112,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { var next = NextDrawAs(); @@ -130,7 +127,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) { // This method is currently only used to composite layers so shouldn't be called here. throw new NotSupportedException(); @@ -152,20 +149,21 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0) + public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, + BoxShadows boxShadows = default) { var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, null, pen, rect, cornerRadius)) + if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows)) { - Add(new RectangleNode(Transform, null, pen, rect, cornerRadius, CreateChildScene(pen.Brush))); + Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush))); } else { ++_drawOperationindex; } } - + public void Custom(ICustomDrawOperation custom) { var next = NextDrawAs(); @@ -191,20 +189,20 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0) + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) { - var next = NextDrawAs(); + var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, brush, null, rect, cornerRadius)) + if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) { - Add(new RectangleNode(Transform, brush, null, rect, cornerRadius, CreateChildScene(brush))); + Add(new GlyphRunNode(Transform, foreground, glyphRun, baselineOrigin, CreateChildScene(foreground))); } + else { ++_drawOperationindex; } } - public IRenderTargetBitmapImpl CreateLayer(Size size) { throw new NotSupportedException("Creating layers on a deferred drawing context not supported"); @@ -285,6 +283,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) { @@ -366,11 +379,11 @@ namespace Avalonia.Rendering.SceneGraph public int DrawOperationIndex { get; } } - private void Add(IDrawOperation node) + private void Add(T node) where T : class, IDrawOperation { using (var refCounted = RefCountable.Create(node)) { - Add(refCounted); + Add(refCounted); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs index d9dfd8bd55..c49e7705e0 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs @@ -9,9 +9,10 @@ namespace Avalonia.Rendering.SceneGraph /// internal abstract class DrawOperation : IDrawOperation { - public DrawOperation(Rect bounds, Matrix transform, IPen pen) + public DrawOperation(Rect bounds, Matrix transform) { - bounds = bounds.Inflate((pen?.Thickness ?? 0) / 2).TransformToAABB(transform); + bounds = bounds.Normalize().TransformToAABB(transform); + Bounds = new Rect( new Point(Math.Floor(bounds.X), Math.Floor(bounds.Y)), new Point(Math.Ceiling(bounds.Right), Math.Ceiling(bounds.Bottom))); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs index d5aa1251f3..8a19679c77 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs @@ -1,7 +1,4 @@ -// 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 System.Collections.Generic; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; @@ -22,13 +19,12 @@ namespace Avalonia.Rendering.SceneGraph /// The stroke pen. /// The geometry. /// Child scenes for drawing visual brushes. - public GeometryNode( - Matrix transform, + public GeometryNode(Matrix transform, IBrush brush, IPen pen, IGeometryImpl geometry, IDictionary childScenes = null) - : base(geometry.GetRenderBounds(pen), transform, null) + : base(geometry.GetRenderBounds(pen), transform) { Transform = transform; Brush = brush?.ToImmutable(); @@ -67,6 +63,7 @@ namespace Avalonia.Rendering.SceneGraph /// The fill of the other draw operation. /// The stroke of the other draw operation. /// The geometry of the other draw operation. + /// The box shadow parameters /// True if the draw operations are the same, otherwise false. /// /// The properties of the other draw operation are passed in as arguments to prevent @@ -75,9 +72,9 @@ namespace Avalonia.Rendering.SceneGraph public bool Equals(Matrix transform, IBrush brush, IPen pen, IGeometryImpl geometry) { return transform == Transform && - Equals(brush, Brush) && - Equals(Pen, pen) && - Equals(geometry, Geometry); + Equals(brush, Brush) && + Equals(Pen, pen) && + Equals(geometry, Geometry); } /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs new file mode 100644 index 0000000000..eaf4effdbe --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; + +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering.SceneGraph +{ + /// + /// A node in the scene graph which represents a glyph run draw. + /// + internal class GlyphRunNode : BrushDrawOperation + { + /// + /// Initializes a new instance of the class. + /// + /// The transform. + /// The foreground brush. + /// The glyph run to draw. + /// The baseline origin of the glyph run. + /// Child scenes for drawing visual brushes. + public GlyphRunNode( + Matrix transform, + IBrush foreground, + GlyphRun glyphRun, + Point baselineOrigin, + IDictionary childScenes = null) + : base(glyphRun.Bounds, transform) + { + Transform = transform; + Foreground = foreground?.ToImmutable(); + GlyphRun = glyphRun; + BaselineOrigin = baselineOrigin; + ChildScenes = childScenes; + } + + /// + /// Gets the transform with which the node will be drawn. + /// + public Matrix Transform { get; } + + /// + /// Gets the foreground brush. + /// + public IBrush Foreground { get; } + + /// + /// Gets the glyph run to draw. + /// + public GlyphRun GlyphRun { get; } + + /// + /// Gets the baseline origin. + /// + public Point BaselineOrigin { get; set; } + + /// + public override IDictionary ChildScenes { get; } + + /// + public override void Render(IDrawingContextImpl context) + { + context.Transform = Transform; + context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin); + } + + /// + /// Determines if this draw operation equals another. + /// + /// The transform of the other draw operation. + /// The foreground of the other draw operation. + /// The glyph run 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. + /// + internal bool Equals(Matrix transform, IBrush foreground, GlyphRun glyphRun) + { + return transform == Transform && + Equals(foreground, Foreground) && + Equals(glyphRun, GlyphRun); + } + + /// + public override bool HitTest(Point p) => Bounds.Contains(p); + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs index f8d5bf5b7a..6d30358119 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using Avalonia.Platform; namespace Avalonia.Rendering.SceneGraph diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs index 10c8d36dc8..6d12b5bca4 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Collections.Generic; using Avalonia.Platform; using Avalonia.Utilities; @@ -29,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/ImageNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs index e1bdcaab3b..c9052c6ef2 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs @@ -1,7 +1,4 @@ -// 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.Platform; +using Avalonia.Platform; using Avalonia.Utilities; using Avalonia.Visuals.Media.Imaging; @@ -22,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The destination rect. /// The bitmap interpolation mode. public ImageNode(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) - : base(destRect, transform, null) + : base(destRect, transform) { Transform = transform; Source = source.Clone(); @@ -100,7 +97,7 @@ namespace Avalonia.Rendering.SceneGraph public override void Render(IDrawingContextImpl context) { context.Transform = Transform; - context.DrawImage(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode); + context.DrawBitmap(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode); } /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineBoundsHelper.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineBoundsHelper.cs new file mode 100644 index 0000000000..56d218e398 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineBoundsHelper.cs @@ -0,0 +1,68 @@ +using System; +using Avalonia.Media; + +namespace Avalonia.Rendering.SceneGraph +{ + internal static class LineBoundsHelper + { + private static double CalculateAngle(Point p1, Point p2) + { + var xDiff = p2.X - p1.X; + var yDiff = p2.Y - p1.Y; + + return Math.Atan2(yDiff, xDiff); + } + + internal static double CalculateOppSide(double angle, double hyp) + { + return Math.Sin(angle) * hyp; + } + + internal static double CalculateAdjSide(double angle, double hyp) + { + return Math.Cos(angle) * hyp; + } + + private static (Point p1, Point p2) TranslatePointsAlongTangent(Point p1, Point p2, double angle, double distance) + { + var xDiff = CalculateOppSide(angle, distance); + var yDiff = CalculateAdjSide(angle, distance); + + var c1 = new Point(p1.X + xDiff, p1.Y - yDiff); + var c2 = new Point(p1.X - xDiff, p1.Y + yDiff); + + var c3 = new Point(p2.X + xDiff, p2.Y - yDiff); + var c4 = new Point(p2.X - xDiff, p2.Y + yDiff); + + var minX = Math.Min(c1.X, Math.Min(c2.X, Math.Min(c3.X, c4.X))); + var minY = Math.Min(c1.Y, Math.Min(c2.Y, Math.Min(c3.Y, c4.Y))); + var maxX = Math.Max(c1.X, Math.Max(c2.X, Math.Max(c3.X, c4.X))); + var maxY = Math.Max(c1.Y, Math.Max(c2.Y, Math.Max(c3.Y, c4.Y))); + + return (new Point(minX, minY), new Point(maxX, maxY)); + } + + private static Rect CalculateBounds(Point p1, Point p2, double thickness, double angleToCorner) + { + var pts = TranslatePointsAlongTangent(p1, p2, angleToCorner, thickness / 2); + + return new Rect(pts.p1, pts.p2); + } + + public static Rect CalculateBounds(Point p1, Point p2, IPen p) + { + var radians = CalculateAngle(p1, p2); + + if (p.LineCap != PenLineCap.Flat) + { + var pts = TranslatePointsAlongTangent(p1, p2, radians - Math.PI / 2, p.Thickness / 2); + + return CalculateBounds(pts.p1, pts.p2, p.Thickness, radians); + } + else + { + return CalculateBounds(p1, p2, p.Thickness, radians); + } + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs index 9a65fac078..54a9ff733d 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs @@ -1,7 +1,4 @@ -// 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 System.Collections.Generic; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; @@ -28,7 +25,7 @@ namespace Avalonia.Rendering.SceneGraph Point p1, Point p2, IDictionary childScenes = null) - : base(new Rect(p1, p2), transform, pen) + : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform) { Transform = transform; Pen = pen?.ToImmutable(); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs index d6dbc1a8cb..b8e7b150ac 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs @@ -18,7 +18,7 @@ namespace Avalonia.Rendering.SceneGraph /// The bounds of the mask. /// Child scenes for drawing visual brushes. public OpacityMaskNode(IBrush mask, Rect bounds, IDictionary childScenes = null) - : base(Rect.Empty, Matrix.Identity, null) + : base(Rect.Empty, Matrix.Identity) { Mask = mask?.ToImmutable(); MaskBounds = bounds; @@ -30,7 +30,7 @@ namespace Avalonia.Rendering.SceneGraph /// opacity mask pop. /// public OpacityMaskNode() - : base(Rect.Empty, Matrix.Identity, null) + : base(Rect.Empty, Matrix.Identity) { } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index 0f3581b84c..5059a6d042 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -1,6 +1,4 @@ -// 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 System; using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; @@ -21,23 +19,23 @@ namespace Avalonia.Rendering.SceneGraph /// The fill brush. /// The stroke pen. /// The rectangle to draw. - /// The rectangle corner radius. + /// The box shadow parameters /// Child scenes for drawing visual brushes. public RectangleNode( Matrix transform, IBrush brush, IPen pen, - Rect rect, - float cornerRadius, + RoundedRect rect, + BoxShadows boxShadows, IDictionary childScenes = null) - : base(rect, transform, pen) + : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform) { Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; - CornerRadius = cornerRadius; ChildScenes = childScenes; + BoxShadows = boxShadows; } /// @@ -58,12 +56,12 @@ namespace Avalonia.Rendering.SceneGraph /// /// Gets the rectangle to draw. /// - public Rect Rect { get; } - + public RoundedRect Rect { get; } + /// - /// Gets the rectangle corner radius. + /// The parameters for the box-shadow effect /// - public float CornerRadius { get; } + public BoxShadows BoxShadows { get; } /// public override IDictionary ChildScenes { get; } @@ -75,19 +73,19 @@ namespace Avalonia.Rendering.SceneGraph /// The fill of the other draw operation. /// The stroke of the other draw operation. /// The rectangle of the other draw operation. - /// The rectangle corner radius of the other draw operation. + /// The box shadow parameters 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, IBrush brush, IPen pen, Rect rect, float cornerRadius) + public bool Equals(Matrix transform, IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows) { return transform == Transform && - Equals(brush, Brush) && - Equals(Pen, pen) && - rect == Rect && - cornerRadius == CornerRadius; + Equals(brush, Brush) && + Equals(Pen, pen) && + Media.BoxShadows.Equals(BoxShadows, boxShadows) && + rect.Equals(Rect); } /// @@ -95,15 +93,7 @@ namespace Avalonia.Rendering.SceneGraph { context.Transform = Transform; - if (Brush != null) - { - context.FillRectangle(Brush, Rect, CornerRadius); - } - - if (Pen != null) - { - context.DrawRectangle(Pen, Rect, CornerRadius); - } + context.DrawRectangle(Brush, Pen, Rect, BoxShadows); } /// @@ -116,13 +106,13 @@ namespace Avalonia.Rendering.SceneGraph if (Brush != null) { - var rect = Rect.Inflate((Pen?.Thickness / 2) ?? 0); + var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); return rect.Contains(p); } else { - var borderRect = Rect.Inflate((Pen?.Thickness / 2) ?? 0); - var emptyRect = Rect.Deflate((Pen?.Thickness / 2) ?? 0); + var borderRect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); + var emptyRect = Rect.Rect.Deflate((Pen?.Thickness / 2) ?? 0); return borderRect.Contains(p) && !emptyRect.Contains(p); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs index 1afc096c98..0f6001516d 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs @@ -1,9 +1,8 @@ -// 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 System; +using System; +using System.Collections; using System.Collections.Generic; using System.Linq; +using Avalonia.Collections.Pooled; using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph @@ -13,7 +12,7 @@ namespace Avalonia.Rendering.SceneGraph /// public class Scene : IDisposable { - private Dictionary _index; + private readonly Dictionary _index; /// /// Initializes a new instance of the class. @@ -84,7 +83,7 @@ namespace Avalonia.Rendering.SceneGraph /// The cloned scene. public Scene CloneScene() { - var index = new Dictionary(); + var index = new Dictionary(_index.Count); var root = Clone((VisualNode)Root, null, index); var result = new Scene(root, index, Layers.Clone(), Generation + 1) @@ -128,7 +127,20 @@ namespace Avalonia.Rendering.SceneGraph public IEnumerable HitTest(Point p, IVisual root, Func filter) { var node = FindNode(root); - return (node != null) ? HitTest(node, p, null, filter) : Enumerable.Empty(); + return (node != null) ? new HitTestEnumerable(node, filter, p, Root) : Enumerable.Empty(); + } + + /// + /// Gets the visual at a point in the scene. + /// + /// The point. + /// The root of the subtree to search. + /// A filter. May be null. + /// The visual at the specified point. + public IVisual HitTestFirst(Point p, IVisual root, Func filter) + { + var node = FindNode(root); + return (node != null) ? HitTestFirst(node, p, filter) : null; } /// @@ -150,46 +162,175 @@ namespace Avalonia.Rendering.SceneGraph index.Add(result.Visual, result); - foreach (var child in source.Children) + var children = source.Children; + var childrenCount = children.Count; + + if (childrenCount > 0) { - result.AddChild(Clone((VisualNode)child, result, index)); + result.TryPreallocateChildren(childrenCount); + + for (var i = 0; i < childrenCount; i++) + { + var child = children[i]; + + result.AddChild(Clone((VisualNode)child, result, index)); + } } return result; } - private IEnumerable HitTest(IVisualNode node, Point p, Rect? clip, Func filter) + private IVisual HitTestFirst(IVisualNode root, Point p, Func filter) + { + using var enumerator = new HitTestEnumerator(root, filter, p, Root); + + enumerator.MoveNext(); + + return enumerator.Current; + } + + private class HitTestEnumerable : IEnumerable { - if (filter?.Invoke(node.Visual) != false && node.Visual.IsAttachedToVisualTree) + private readonly IVisualNode _root; + private readonly Func _filter; + private readonly IVisualNode _sceneRoot; + private readonly Point _point; + + public HitTestEnumerable(IVisualNode root, Func filter, Point point, IVisualNode sceneRoot) { - var clipped = false; + _root = root; + _filter = filter; + _point = point; + _sceneRoot = sceneRoot; + } - if (node.ClipToBounds) - { - clip = clip == null ? node.ClipBounds : clip.Value.Intersect(node.ClipBounds); - clipped = !clip.Value.Contains(p); - } + public IEnumerator GetEnumerator() + { + return new HitTestEnumerator(_root, _filter, _point, _sceneRoot); + } - if (node.GeometryClip != null) - { - var controlPoint = Root.Visual.TranslatePoint(p, node.Visual); - clipped = !node.GeometryClip.FillContains(controlPoint.Value); - } + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } - if (!clipped) + private struct HitTestEnumerator : IEnumerator + { + private readonly PooledStack _nodeStack; + private readonly Func _filter; + private readonly IVisualNode _sceneRoot; + private IVisual _current; + private readonly Point _point; + + public HitTestEnumerator(IVisualNode root, Func filter, Point point, IVisualNode sceneRoot) + { + _nodeStack = new PooledStack(); + _nodeStack.Push(new Entry(root, false, null, true)); + + _filter = filter; + _point = point; + _sceneRoot = sceneRoot; + + _current = null; + } + + public bool MoveNext() + { + while (_nodeStack.Count > 0) { - for (var i = node.Children.Count - 1; i >= 0; --i) + (var wasVisited, var isRoot, IVisualNode node, Rect? clip) = _nodeStack.Pop(); + + if (wasVisited && isRoot) { - foreach (var h in HitTest(node.Children[i], p, clip, filter)) + break; + } + + var children = node.Children; + int childCount = children.Count; + + if (childCount == 0 || wasVisited) + { + if ((wasVisited || FilterAndClip(node, ref clip)) && node.HitTest(_point)) + { + _current = node.Visual; + + return true; + } + } + else if (FilterAndClip(node, ref clip)) + { + _nodeStack.Push(new Entry(node, true, null)); + + for (var i = 0; i < childCount; i++) { - yield return h; + _nodeStack.Push(new Entry(children[i], false, clip)); } } + } + + return false; + } + + public void Reset() + { + throw new NotSupportedException(); + } + + public IVisual Current => _current; + + object IEnumerator.Current => Current; + + public void Dispose() + { + _nodeStack.Dispose(); + } + + private bool FilterAndClip(IVisualNode node, ref Rect? clip) + { + if (_filter?.Invoke(node.Visual) != false && node.Visual.IsAttachedToVisualTree) + { + var clipped = false; + + if (node.ClipToBounds) + { + clip = clip == null ? node.ClipBounds : clip.Value.Intersect(node.ClipBounds); + clipped = !clip.Value.Contains(_point); + } - if (node.HitTest(p)) + if (node.GeometryClip != null) { - yield return node.Visual; + var controlPoint = _sceneRoot.Visual.TranslatePoint(_point, node.Visual); + clipped = !node.GeometryClip.FillContains(controlPoint.Value); } + + return !clipped; + } + + return false; + } + + private readonly struct Entry + { + public readonly bool WasVisited; + public readonly bool IsRoot; + public readonly IVisualNode Node; + public readonly Rect? Clip; + + public Entry(IVisualNode node, bool wasVisited, Rect? clip, bool isRoot = false) + { + Node = node; + WasVisited = wasVisited; + IsRoot = isRoot; + Clip = clip; + } + + public void Deconstruct(out bool wasVisited, out bool isRoot, out IVisualNode node, out Rect? clip) + { + wasVisited = WasVisited; + isRoot = IsRoot; + node = Node; + clip = Clip; } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 161cbc099e..872f69c884 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Linq; using Avalonia.Media; using Avalonia.Platform; @@ -51,7 +48,7 @@ namespace Avalonia.Rendering.SceneGraph UpdateSize(scene); } - if (visual.VisualRoot != null) + if (visual.VisualRoot == scene.Root.Visual) { if (node?.Parent != null && visual.VisualParent != null && @@ -148,11 +145,28 @@ namespace Avalonia.Rendering.SceneGraph return (VisualNode)node; } + private static object GetOrCreateChildNode(Scene scene, IVisual child, VisualNode parent) + { + var result = (VisualNode)scene.FindNode(child); + + if (result != null && result.Parent != parent) + { + Deindex(scene, result); + result = null; + } + + return result ?? CreateNode(scene, child, parent); + } + private static void Update(DrawingContext context, Scene scene, VisualNode node, Rect clip, bool forceRecurse) { 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; @@ -191,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; @@ -231,7 +246,7 @@ namespace Avalonia.Rendering.SceneGraph { foreach (var child in visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance)) { - var childNode = scene.FindNode(child) ?? CreateNode(scene, child, node); + var childNode = GetOrCreateChildNode(scene, child, node); Update(context, scene, (VisualNode)childNode, clip, forceRecurse); } @@ -240,6 +255,10 @@ namespace Avalonia.Rendering.SceneGraph } } } + else + { + contextImpl.BeginUpdate(node).Dispose(); + } } private void UpdateSize(Scene scene) @@ -375,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/SceneLayers.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs index 5960b4f560..25f7383a1a 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs @@ -11,16 +11,28 @@ namespace Avalonia.Rendering.SceneGraph public class SceneLayers : IEnumerable { private readonly IVisual _root; - private readonly List _inner = new List(); - private readonly Dictionary _index = new Dictionary(); + private readonly List _inner; + private readonly Dictionary _index; /// /// Initializes a new instance of the class. /// /// The scene's root visual. - public SceneLayers(IVisual root) + public SceneLayers(IVisual root) : this(root, 0) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The scene's root visual. + /// Initial layer capacity. + public SceneLayers(IVisual root, int capacity) { _root = root; + + _inner = new List(capacity); + _index = new Dictionary(capacity); } /// @@ -84,7 +96,7 @@ namespace Avalonia.Rendering.SceneGraph /// The cloned layers. public SceneLayers Clone() { - var result = new SceneLayers(_root); + var result = new SceneLayers(_root, Count); foreach (var src in _inner) { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs index ef392cd2af..4b6c331023 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs @@ -1,7 +1,4 @@ -// 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 System.Collections.Generic; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Platform; using Avalonia.VisualTree; @@ -27,7 +24,7 @@ namespace Avalonia.Rendering.SceneGraph Point origin, IFormattedTextImpl text, IDictionary childScenes = null) - : base(text.Bounds.Translate(origin), transform, null) + : base(text.Bounds.Translate(origin), transform) { Transform = transform; Foreground = foreground?.ToImmutable(); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index f579bf0a62..8fb6b2542a 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -1,10 +1,7 @@ -// 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 System; +using System; using System.Collections.Generic; -using System.Linq; using System.Reactive.Disposables; +using Avalonia.Collections; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Utilities; @@ -49,6 +46,9 @@ namespace Avalonia.Rendering.SceneGraph /// public IVisualNode Parent { get; } + /// + public CornerRadius ClipToBoundsRadius { get; set; } + /// public Matrix Transform { get; set; } @@ -119,6 +119,11 @@ namespace Avalonia.Rendering.SceneGraph throw new ObjectDisposedException("Visual node for {node.Visual}"); } + if (child.Parent != this) + { + throw new AvaloniaInternalException("VisualNode added to wrong parent."); + } + EnsureChildrenCreated(); _children.Add(child); } @@ -155,6 +160,11 @@ namespace Avalonia.Rendering.SceneGraph throw new ObjectDisposedException("Visual node for {node.Visual}"); } + if (node.Parent != this) + { + throw new AvaloniaInternalException("VisualNode added to wrong parent."); + } + EnsureChildrenCreated(); _children[index] = node; } @@ -218,7 +228,7 @@ namespace Avalonia.Rendering.SceneGraph if (first < _children?.Count) { EnsureChildrenCreated(); - for (int i = first; i < _children.Count - first; i++) + for (int i = first; i < _children.Count; i++) { _children[i].Dispose(); } @@ -255,6 +265,7 @@ namespace Avalonia.Rendering.SceneGraph { Transform = Transform, ClipBounds = ClipBounds, + ClipToBoundsRadius = ClipToBoundsRadius, ClipToBounds = ClipToBounds, LayoutBounds = LayoutBounds, GeometryClip = GeometryClip, @@ -270,8 +281,13 @@ namespace Avalonia.Rendering.SceneGraph /// public bool HitTest(Point p) { - foreach (var operation in DrawOperations) + var drawOperations = DrawOperations; + var drawOperationsCount = drawOperations.Count; + + for (var i = 0; i < drawOperationsCount; i++) { + var operation = drawOperations[i]; + if (operation?.Item?.HitTest(p) == true) { return true; @@ -289,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; @@ -337,6 +356,11 @@ namespace Avalonia.Rendering.SceneGraph context.Transform = transformRestore; } + internal void TryPreallocateChildren(int count) + { + EnsureChildrenCreated(count); + } + private Rect CalculateBounds() { var result = new Rect(); @@ -350,11 +374,11 @@ namespace Avalonia.Rendering.SceneGraph return result; } - private void EnsureChildrenCreated() + private void EnsureChildrenCreated(int capacity = 0) { if (_children == null) { - _children = new List(); + _children = new List(capacity); } } @@ -371,7 +395,15 @@ namespace Avalonia.Rendering.SceneGraph } else if (_drawOperationsCloned) { - _drawOperations = new List>(_drawOperations.Select(op => op.Clone())); + var oldDrawOperations = _drawOperations; + + _drawOperations = new List>(oldDrawOperations.Count); + + foreach (var drawOperation in oldDrawOperations) + { + _drawOperations.Add(drawOperation.Clone()); + } + _drawOperationsRefCounter.Dispose(); _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations)); _drawOperationsCloned = false; @@ -387,9 +419,9 @@ namespace Avalonia.Rendering.SceneGraph /// Disposable for given draw operations. private static IDisposable CreateDisposeDrawOperations(List> drawOperations) { - return Disposable.Create(() => + return Disposable.Create(drawOperations, operations => { - foreach (var operation in drawOperations) + foreach (var operation in operations) { operation.Dispose(); } diff --git a/src/Avalonia.Visuals/Rendering/SceneInvalidatedEventArgs.cs b/src/Avalonia.Visuals/Rendering/SceneInvalidatedEventArgs.cs index 40cb9f3356..cac4d1693a 100644 --- a/src/Avalonia.Visuals/Rendering/SceneInvalidatedEventArgs.cs +++ b/src/Avalonia.Visuals/Rendering/SceneInvalidatedEventArgs.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; namespace Avalonia.Rendering { diff --git a/src/Avalonia.Visuals/Rendering/Utilities/TileBrushCalculator.cs b/src/Avalonia.Visuals/Rendering/Utilities/TileBrushCalculator.cs index 27949fcf55..af2c7f71dc 100644 --- a/src/Avalonia.Visuals/Rendering/Utilities/TileBrushCalculator.cs +++ b/src/Avalonia.Visuals/Rendering/Utilities/TileBrushCalculator.cs @@ -1,7 +1,4 @@ -// 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.Media; +using Avalonia.Media; namespace Avalonia.Rendering.Utilities { diff --git a/src/Avalonia.Visuals/RoundedRect.cs b/src/Avalonia.Visuals/RoundedRect.cs new file mode 100644 index 0000000000..3452bc1ff8 --- /dev/null +++ b/src/Avalonia.Visuals/RoundedRect.cs @@ -0,0 +1,147 @@ +using System; + +namespace Avalonia +{ + public struct RoundedRect + { + public bool Equals(RoundedRect other) + { + return Rect.Equals(other.Rect) && RadiiTopLeft.Equals(other.RadiiTopLeft) && RadiiTopRight.Equals(other.RadiiTopRight) && RadiiBottomLeft.Equals(other.RadiiBottomLeft) && RadiiBottomRight.Equals(other.RadiiBottomRight); + } + + public override bool Equals(object obj) + { + return obj is RoundedRect other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = Rect.GetHashCode(); + hashCode = (hashCode * 397) ^ RadiiTopLeft.GetHashCode(); + hashCode = (hashCode * 397) ^ RadiiTopRight.GetHashCode(); + hashCode = (hashCode * 397) ^ RadiiBottomLeft.GetHashCode(); + hashCode = (hashCode * 397) ^ RadiiBottomRight.GetHashCode(); + return hashCode; + } + } + + 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; } + public Vector RadiiBottomLeft { get; } + public Vector RadiiBottomRight { get; } + + public RoundedRect(Rect rect, Vector radiiTopLeft, Vector radiiTopRight, Vector radiiBottomRight, Vector radiiBottomLeft) + { + Rect = rect; + RadiiTopLeft = radiiTopLeft; + RadiiTopRight = radiiTopRight; + RadiiBottomRight = radiiBottomRight; + RadiiBottomLeft = radiiBottomLeft; + } + + public RoundedRect(Rect rect, double radiusTopLeft, double radiusTopRight, double radiusBottomRight, + double radiusBottomLeft) + : this(rect, + new Vector(radiusTopLeft, radiusTopLeft), + new Vector(radiusTopRight, radiusTopRight), + new Vector(radiusBottomRight, radiusBottomRight), + new Vector(radiusBottomLeft, radiusBottomLeft) + ) + { + + } + + public RoundedRect(Rect rect, Vector radii) : this(rect, radii, radii, radii, radii) + { + + } + + public RoundedRect(Rect rect, double radiusX, double radiusY) : this(rect, new Vector(radiusX, radiusY)) + { + + } + + public RoundedRect(Rect rect, double radius) : this(rect, radius, radius) + { + + } + + public RoundedRect(Rect rect) : this(rect, 0) + { + + } + + 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 || + RadiiBottomLeft != default; + + public bool IsUniform => + RadiiTopLeft.Equals(RadiiTopRight) && + RadiiTopLeft.Equals(RadiiBottomRight) && + RadiiTopLeft.Equals(RadiiBottomLeft); + + public RoundedRect Inflate(double dx, double dy) + { + return Deflate(-dx, -dy); + } + + public unsafe RoundedRect Deflate(double dx, double dy) + { + if (!IsRounded) + return new RoundedRect(Rect.Deflate(new Thickness(dx, dy))); + + // Ported from SKRRect + var left = Rect.X + dx; + var top = Rect.Y + dy; + var right = left + Rect.Width - dx * 2; + var bottom = top + Rect.Height - dy * 2; + var radii = stackalloc Vector[4]; + radii[0] = RadiiTopLeft; + radii[1] = RadiiTopRight; + radii[2] = RadiiBottomRight; + radii[3] = RadiiBottomLeft; + + bool degenerate = false; + if (right <= left) { + degenerate = true; + left = right = (left + right)*0.5; + } + if (bottom <= top) { + degenerate = true; + top = bottom = (top + bottom) * 0.5; + } + if (degenerate) + { + return new RoundedRect(new Rect(left, top, right - left, bottom - top)); + } + + for (var c = 0; c < 4; c++) + { + var rx = Math.Max(0, radii[c].X - dx); + var ry = Math.Max(0, radii[c].Y - dy); + if (rx == 0 || ry == 0) + radii[c] = default; + else + radii[c] = new Vector(rx, ry); + } + + return new RoundedRect(new Rect(left, top, right - left, bottom - top), + radii[0], radii[1], radii[2], radii[3]); + } + } +} diff --git a/src/Avalonia.Visuals/Size.cs b/src/Avalonia.Visuals/Size.cs index aba2ed8d62..f87b336b50 100644 --- a/src/Avalonia.Visuals/Size.cs +++ b/src/Avalonia.Visuals/Size.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Globalization; using Avalonia.Animation.Animators; @@ -192,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. @@ -204,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/Thickness.cs b/src/Avalonia.Visuals/Thickness.cs index 44ff66069f..b03e91bf34 100644 --- a/src/Avalonia.Visuals/Thickness.cs +++ b/src/Avalonia.Visuals/Thickness.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Globalization; using Avalonia.Animation.Animators; diff --git a/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs b/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs new file mode 100644 index 0000000000..ff2b3b9363 --- /dev/null +++ b/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia.Utilities; + +namespace Avalonia.Utility +{ + /// + /// ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region. + /// + /// The type of elements in the slice. + [DebuggerTypeProxy(typeof(ReadOnlySlice<>.ReadOnlySliceDebugView))] + public readonly struct ReadOnlySlice : IReadOnlyList + { + public ReadOnlySlice(ReadOnlyMemory buffer) : this(buffer, 0, buffer.Length) { } + + public ReadOnlySlice(ReadOnlyMemory buffer, int start, int length) + { + Buffer = buffer; + Start = start; + Length = length; + } + + /// + /// Gets the start. + /// + /// + /// The start. + /// + public int Start { get; } + + /// + /// Gets the end. + /// + /// + /// The end. + /// + public int End => Start + Length - 1; + + /// + /// Gets the length. + /// + /// + /// The length. + /// + public int Length { get; } + + /// + /// Gets a value that indicates whether this instance of is Empty. + /// + public bool IsEmpty => Length == 0; + + /// + /// The buffer. + /// + public ReadOnlyMemory Buffer { get; } + + public T this[int index] => Buffer.Span[index]; + + /// + /// Returns a sub slice of elements that start at the specified index and has the specified number of elements. + /// + /// The start of the sub slice. + /// The length of the sub slice. + /// A that contains the specified number of elements from the specified start. + public ReadOnlySlice AsSlice(int start, int length) + { + if (IsEmpty) + { + return this; + } + + if (start < Start || start > End) + { + throw new ArgumentOutOfRangeException(nameof(start)); + } + + if (start + length > Start + Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + var bufferOffset = start - Start; + + return new ReadOnlySlice(Buffer.Slice(bufferOffset), start, length); + } + + /// + /// 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 ReadOnlySlice Take(int length) + { + if (IsEmpty) + { + return this; + } + + if (length > Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return new ReadOnlySlice(Buffer.Slice(0, length), 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 ReadOnlySlice Skip(int length) + { + if (IsEmpty) + { + return this; + } + + if (length > Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return new ReadOnlySlice(Buffer.Slice(length), Start + length, Length - length); + } + + /// + /// Returns an enumerator for the slice. + /// + public ImmutableReadOnlyListStructEnumerator GetEnumerator() + { + return new ImmutableReadOnlyListStructEnumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + int IReadOnlyCollection.Count => Length; + + T IReadOnlyList.this[int index] => this[index]; + + public static implicit operator ReadOnlySlice(T[] array) + { + return new ReadOnlySlice(array); + } + + public static implicit operator ReadOnlySlice(ReadOnlyMemory memory) + { + return new ReadOnlySlice(memory); + } + + internal class ReadOnlySliceDebugView + { + private readonly ReadOnlySlice _readOnlySlice; + + public ReadOnlySliceDebugView(ReadOnlySlice readOnlySlice) + { + _readOnlySlice = readOnlySlice; + } + + public int Start => _readOnlySlice.Start; + + public int End => _readOnlySlice.End; + + public int Length => _readOnlySlice.Length; + + public bool IsEmpty => _readOnlySlice.IsEmpty; + + public ReadOnlyMemory Items => _readOnlySlice.Buffer; + } + } +} diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index 576d2daaaa..6059dc3971 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -1,10 +1,9 @@ -// 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 System; using System.Globalization; using Avalonia.Animation.Animators; -using JetBrains.Annotations; +using Avalonia.Utilities; + +#nullable enable namespace Avalonia { @@ -19,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; @@ -40,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; @@ -59,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) @@ -79,19 +78,35 @@ namespace Avalonia /// /// Scales a vector. /// - /// The vector + /// The vector. /// The divisor. /// The scaled vector. public static Vector operator /(Vector vector, double scale) => Divide(vector, scale); /// - /// Length of the vector + /// Parses a string. + /// + /// The string. + /// The . + public static Vector Parse(string s) + { + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Vector.")) + { + return new Vector( + tokenizer.ReadDouble(), + tokenizer.ReadDouble() + ); + } + } + + /// + /// 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; @@ -140,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); @@ -166,18 +180,18 @@ namespace Avalonia } /// - /// Returns the string representation of the point. + /// Returns the string representation of the vector. /// - /// The string representation of the point. + /// The string representation of the vector. public override string ToString() { return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", _x, _y); } /// - /// 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) { @@ -185,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) { @@ -297,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 f4306d3929..cd6eb6aac7 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -1,10 +1,5 @@ -// 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 System; using System.Collections.Specialized; -using System.Linq; -using System.Reactive.Linq; using Avalonia.Collections; using Avalonia.Data; using Avalonia.Logging; @@ -73,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. @@ -119,9 +114,12 @@ namespace Avalonia /// public Visual() { + // Disable transitions until we're added to the visual tree. + DisableTransitions(); + var visualChildren = new AvaloniaList(); visualChildren.ResetBehavior = ResetBehavior.Remove; - visualChildren.Validate = ValidateVisualChild; + visualChildren.Validate = visual => ValidateVisualChild(visual); visualChildren.CollectionChanged += VisualChildrenChanged; VisualChildren = visualChildren; } @@ -173,7 +171,22 @@ namespace Avalonia /// public bool IsEffectivelyVisible { - get { return this.GetSelfAndVisualAncestors().All(x => x.IsVisible); } + get + { + IVisual node = this; + + while (node != null) + { + if (!node.IsVisible) + { + return false; + } + + node = node.VisualParent; + } + + return true; + } } /// @@ -206,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); } @@ -322,7 +335,15 @@ namespace Avalonia protected static void AffectsRender(params AvaloniaProperty[] properties) where T : Visual { - void Invalidate(AvaloniaPropertyChangedEventArgs e) + static void Invalidate(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is T sender) + { + sender.InvalidateVisual(); + } + } + + static void InvalidateAndSubscribe(AvaloniaPropertyChangedEventArgs e) { if (e.Sender is T sender) { @@ -333,7 +354,7 @@ namespace Avalonia if (e.NewValue is IAffectsRender newValue) { - WeakEventHandlerManager.Subscribe(newValue, nameof(newValue.Invalidated), sender.AffectsRenderInvalidated); + WeakEventHandlerManager.Subscribe(newValue, nameof(newValue.Invalidated), sender.AffectsRenderInvalidated); } sender.InvalidateVisual(); @@ -342,7 +363,14 @@ namespace Avalonia foreach (var property in properties) { - property.Changed.Subscribe(Invalidate); + if (property.CanValueAffectRender()) + { + property.Changed.Subscribe(e => InvalidateAndSubscribe(e)); + } + else + { + property.Changed.Subscribe(e => Invalidate(e)); + } } } @@ -359,24 +387,32 @@ 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(); OnAttachedToVisualTree(e); AttachedToVisualTree?.Invoke(this, e); InvalidateVisual(); - if (VisualChildren != null) + var visualChildren = VisualChildren; + + if (visualChildren != null) { - foreach (Visual child in VisualChildren.OfType()) + var visualChildrenCount = visualChildren.Count; + + for (var i = 0; i < visualChildrenCount; i++) { - child.OnAttachedToVisualTreeCore(e); + if (visualChildren[i] is Visual child) + { + child.OnAttachedToVisualTreeCore(e); + } } } } @@ -388,24 +424,32 @@ 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(); OnDetachedFromVisualTree(e); DetachedFromVisualTree?.Invoke(this, e); e.Root?.Renderer?.AddDirty(this); - if (VisualChildren != null) + var visualChildren = VisualChildren; + + if (visualChildren != null) { - foreach (Visual child in VisualChildren.OfType()) + var visualChildrenCount = visualChildren.Count; + + for (var i = 0; i < visualChildrenCount; i++) { - child.OnDetachedFromVisualTreeCore(e); + if (visualChildren[i] is Visual child) + { + child.OnDetachedFromVisualTreeCore(e); + } } } } @@ -433,7 +477,11 @@ namespace Avalonia /// The new visual parent. protected virtual void OnVisualParentChanged(IVisual oldParent, IVisual newParent) { - RaisePropertyChanged(VisualParentProperty, oldParent, newParent, BindingPriority.LocalValue); + RaisePropertyChanged( + VisualParentProperty, + new Optional(oldParent), + new BindingValue(newParent), + BindingPriority.LocalValue); } protected override sealed void LogBindingError(AvaloniaProperty property, Exception e) @@ -453,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, @@ -552,7 +599,7 @@ namespace Avalonia if (_visualParent is IRenderRoot || _visualParent?.IsAttachedToVisualTree == true) { - var root = this.GetVisualAncestors().OfType().FirstOrDefault(); + var root = this.FindAncestorOfType(); var e = new VisualTreeAttachmentEventArgs(_visualParent, root); OnAttachedToVisualTreeCore(e); } diff --git a/src/Avalonia.Visuals/VisualExtensions.cs b/src/Avalonia.Visuals/VisualExtensions.cs index b191d044db..6079e5941f 100644 --- a/src/Avalonia.Visuals/VisualExtensions.cs +++ b/src/Avalonia.Visuals/VisualExtensions.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.VisualTree; diff --git a/src/Avalonia.Visuals/VisualTree/IHostedVisualTreeRoot.cs b/src/Avalonia.Visuals/VisualTree/IHostedVisualTreeRoot.cs index 6baf5d18a1..cb757e0a57 100644 --- a/src/Avalonia.Visuals/VisualTree/IHostedVisualTreeRoot.cs +++ b/src/Avalonia.Visuals/VisualTree/IHostedVisualTreeRoot.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.VisualTree { /// diff --git a/src/Avalonia.Visuals/VisualTree/IVisual.cs b/src/Avalonia.Visuals/VisualTree/IVisual.cs index dd3686ce3d..50787655d9 100644 --- a/src/Avalonia.Visuals/VisualTree/IVisual.cs +++ b/src/Avalonia.Visuals/VisualTree/IVisual.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Collections; using Avalonia.Media; @@ -79,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/IVisualTreeHost.cs b/src/Avalonia.Visuals/VisualTree/IVisualTreeHost.cs index 43560476da..b63c49bd5a 100644 --- a/src/Avalonia.Visuals/VisualTree/IVisualTreeHost.cs +++ b/src/Avalonia.Visuals/VisualTree/IVisualTreeHost.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.VisualTree { /// 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 b2121aa8da..3aa0392496 100644 --- a/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs +++ b/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.VisualTree @@ -79,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.Visuals/VisualTree/VisualExtensions.cs b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs index 567b676b1e..8c4004efdc 100644 --- a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs +++ b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Linq; @@ -14,7 +11,7 @@ namespace Avalonia.VisualTree public static class VisualExtensions { /// - /// Calculates the distance from a visual's . + /// Calculates the distance from a visual's ancestor. /// /// The visual. /// The ancestor visual. @@ -30,13 +27,39 @@ namespace Avalonia.VisualTree while (visual != null && visual != ancestor) { - ++result; visual = visual.VisualParent; + + result++; } return visual != null ? result : -1; } + /// + /// Calculates the distance from a visual's root. + /// + /// The visual. + /// + /// The number of steps from the visual to the root. + /// + public static int CalculateDistanceFromRoot(IVisual visual) + { + Contract.Requires(visual != null); + + var result = 0; + + visual = visual?.VisualParent; + + while (visual != null) + { + visual = visual.VisualParent; + + result++; + } + + return result; + } + /// /// Tries to get the first common ancestor of two visuals. /// @@ -47,8 +70,53 @@ namespace Avalonia.VisualTree { Contract.Requires(visual != null); - return visual.GetSelfAndVisualAncestors().Intersect(target.GetSelfAndVisualAncestors()) - .FirstOrDefault(); + if (target is null) + { + return null; + } + + void GoUpwards(ref IVisual node, int count) + { + for (int i = 0; i < count; ++i) + { + node = node.VisualParent; + } + } + + // We want to find lowest node first, then make sure that both nodes are at the same height. + // By doing that we can sometimes find out that other node is our lowest common ancestor. + var firstHeight = CalculateDistanceFromRoot(visual); + var secondHeight = CalculateDistanceFromRoot(target); + + if (firstHeight > secondHeight) + { + GoUpwards(ref visual, firstHeight - secondHeight); + } + else + { + GoUpwards(ref target, secondHeight - firstHeight); + } + + if (visual == target) + { + return visual; + } + + while (visual != null && target != null) + { + IVisual firstParent = visual.VisualParent; + IVisual secondParent = target.VisualParent; + + if (firstParent == secondParent) + { + return firstParent; + } + + visual = visual.VisualParent; + target = target.VisualParent; + } + + return null; } /// @@ -69,6 +137,57 @@ namespace Avalonia.VisualTree } } + /// + /// Finds first ancestor of given type. + /// + /// Ancestor type. + /// The visual. + /// If given visual should be included in search. + /// First ancestor of given type. + public static T FindAncestorOfType(this IVisual visual, bool includeSelf = false) where T : class + { + if (visual is null) + { + return null; + } + + IVisual parent = includeSelf ? visual : visual.VisualParent; + + while (parent != null) + { + if (parent is T result) + { + return result; + } + + parent = parent.VisualParent; + } + + return null; + } + + /// + /// Finds first descendant of given type. + /// + /// Descendant type. + /// The visual. + /// If given visual should be included in search. + /// First descendant of given type. + public static T FindDescendantOfType(this IVisual visual, bool includeSelf = false) where T : class + { + if (visual is null) + { + return null; + } + + if (includeSelf && visual is T result) + { + return result; + } + + return FindDescendantOfTypeCore(visual); + } + /// /// Enumerates an and its ancestors in the visual tree. /// @@ -91,12 +210,37 @@ namespace Avalonia.VisualTree /// /// The root visual to test. /// The point. - /// The visuals at the requested point. + /// The visual at the requested point. public static IVisual GetVisualAt(this IVisual visual, Point p) { Contract.Requires(visual != null); - return visual.GetVisualsAt(p).FirstOrDefault(); + return visual.GetVisualAt(p, x => x.IsVisible); + } + + /// + /// Gets the first visual in the visual tree whose bounds contain a point. + /// + /// The root visual to test. + /// The point. + /// + /// A filter predicate. If the predicate returns false then the visual and all its + /// children will be excluded from the results. + /// + /// The visual at the requested point. + public static IVisual GetVisualAt(this IVisual visual, Point p, Func filter) + { + Contract.Requires(visual != null); + + var root = visual.GetVisualRoot(); + var rootPoint = visual.TranslatePoint(p, root); + + if (rootPoint.HasValue) + { + return root.Renderer.HitTestFirst(rootPoint.Value, visual, filter); + } + + return null; } /// @@ -233,7 +377,19 @@ namespace Avalonia.VisualTree /// public static bool IsVisualAncestorOf(this IVisual visual, IVisual target) { - return target.GetVisualAncestors().Any(x => x == visual); + IVisual current = target?.VisualParent; + + while (current != null) + { + if (current == visual) + { + return true; + } + + current = current.VisualParent; + } + + return false; } public static IEnumerable SortByZIndex(this IEnumerable elements) @@ -249,6 +405,31 @@ namespace Avalonia.VisualTree .Select(x => x.Element); } + private static T FindDescendantOfTypeCore(IVisual visual) where T : class + { + var visualChildren = visual.VisualChildren; + var visualChildrenCount = visualChildren.Count; + + for (var i = 0; i < visualChildrenCount; i++) + { + IVisual child = visualChildren[i]; + + if (child is T result) + { + return result; + } + + var childResult = FindDescendantOfTypeCore(child); + + if (!(childResult is null)) + { + return childResult; + } + } + + return null; + } + private class ZOrderElement : IComparable { public IVisual Element { get; set; } diff --git a/src/Avalonia.Visuals/VisualTree/VisualLocator.cs b/src/Avalonia.Visuals/VisualTree/VisualLocator.cs index eaca9ed9d7..1e0662c17a 100644 --- a/src/Avalonia.Visuals/VisualTree/VisualLocator.cs +++ b/src/Avalonia.Visuals/VisualTree/VisualLocator.cs @@ -1,7 +1,5 @@ using System; using System.Linq; -using System.Reactive.Linq; -using System.Reflection; using Avalonia.Reactive; namespace Avalonia.VisualTree @@ -50,7 +48,7 @@ namespace Avalonia.VisualTree if (_relativeTo.IsAttachedToVisualTree) { return _relativeTo.GetVisualAncestors() - .Where(x => _ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) + .Where(x => _ancestorType?.IsAssignableFrom(x.GetType()) ?? true) .ElementAtOrDefault(_ancestorLevel); } else diff --git a/src/Avalonia.Visuals/VisualTreeAttachmentEventArgs.cs b/src/Avalonia.Visuals/VisualTreeAttachmentEventArgs.cs index 8c6e660d10..d8f6ea8296 100644 --- a/src/Avalonia.Visuals/VisualTreeAttachmentEventArgs.cs +++ b/src/Avalonia.Visuals/VisualTreeAttachmentEventArgs.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Rendering; using Avalonia.VisualTree; diff --git a/src/Avalonia.X11/Glx/Glx.cs b/src/Avalonia.X11/Glx/Glx.cs index c3a2fd2050..37ca7f5603 100644 --- a/src/Avalonia.X11/Glx/Glx.cs +++ b/src/Avalonia.X11/Glx/Glx.cs @@ -15,11 +15,30 @@ namespace Avalonia.X11.Glx public GlxMakeContextCurrent MakeContextCurrent { get; } public delegate bool GlxMakeContextCurrent(IntPtr display, IntPtr draw, IntPtr read, IntPtr context); + [GlEntryPoint("glXGetCurrentContext")] + public GlxGetCurrentContext GetCurrentContext { get; } + public delegate IntPtr GlxGetCurrentContext(); + + [GlEntryPoint("glXGetCurrentDisplay")] + public GlxGetCurrentDisplay GetCurrentDisplay { get; } + public delegate IntPtr GlxGetCurrentDisplay(); + + [GlEntryPoint("glXGetCurrentDrawable")] + public GlxGetCurrentDrawable GetCurrentDrawable { get; } + public delegate IntPtr GlxGetCurrentDrawable(); + + [GlEntryPoint("glXGetCurrentReadDrawable")] + public GlxGetCurrentReadDrawable GetCurrentReadDrawable { get; } + public delegate IntPtr GlxGetCurrentReadDrawable(); + [GlEntryPoint("glXCreatePbuffer")] public GlxCreatePbuffer CreatePbuffer { get; } - public delegate IntPtr GlxCreatePbuffer(IntPtr dpy, IntPtr fbc, int[] attrib_list); + [GlEntryPoint("glXDestroyPbuffer")] + public GlxDestroyPbuffer DestroyPbuffer { get; } + public delegate IntPtr GlxDestroyPbuffer(IntPtr dpy, IntPtr fb); + [GlEntryPointAttribute("glXChooseVisual")] public GlxChooseVisual ChooseVisual { get; } public delegate XVisualInfo* GlxChooseVisual(IntPtr dpy, int screen, int[] attribList); @@ -78,14 +97,42 @@ namespace Avalonia.X11.Glx [GlEntryPointAttribute("glXWaitGL")] public GlxWaitGL WaitGL { get; } - public delegate void GlxWaitGL(); + public delegate void GlxWaitGL(); public delegate int GlGetError(); [GlEntryPoint("glGetError")] public GlGetError GetError { get; } - public GlxInterface() : base(GlxGetProcAddress) + public delegate IntPtr GlxQueryExtensionsString(IntPtr display, int screen); + [GlEntryPoint("glXQueryExtensionsString")] + public GlxQueryExtensionsString QueryExtensionsString { get; } + + public GlxInterface() : base(SafeGetProcAddress) + { + } + + // Ignores egl functions. + // On some Linux systems, glXGetProcAddress will return valid pointers for even EGL functions. + // This makes Skia try to load some data from EGL, + // which can then cause segmentation faults because they return garbage. + public static IntPtr SafeGetProcAddress(string proc) + { + if (proc.StartsWith("egl", StringComparison.InvariantCulture)) + { + return IntPtr.Zero; + } + + return GlxConverted(proc); + } + + private static readonly Func GlxConverted = ConvertNative(GlxGetProcAddress); + + public string[] GetExtensions(IntPtr display) { + var s = Marshal.PtrToStringAnsi(QueryExtensionsString(display, 0)); + return s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()).ToArray(); + } } } diff --git a/src/Avalonia.X11/Glx/GlxConsts.cs b/src/Avalonia.X11/Glx/GlxConsts.cs index 65f253696a..9d153a6ade 100644 --- a/src/Avalonia.X11/Glx/GlxConsts.cs +++ b/src/Avalonia.X11/Glx/GlxConsts.cs @@ -100,6 +100,8 @@ namespace Avalonia.X11.Glx public const int GLX_CONTEXT_FLAGS_ARB = 0x2094; public const int GLX_CONTEXT_CORE_PROFILE_BIT_ARB = 0x00000001; public const int GLX_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB = 0x00000002; + public const int GLX_CONTEXT_ES2_PROFILE_BIT_EXT = 0x00000004; + public const int GLX_CONTEXT_PROFILE_MASK_ARB = 0x9126; } diff --git a/src/Avalonia.X11/Glx/GlxContext.cs b/src/Avalonia.X11/Glx/GlxContext.cs index dd95841d57..0349a6e26e 100644 --- a/src/Avalonia.X11/Glx/GlxContext.cs +++ b/src/Avalonia.X11/Glx/GlxContext.cs @@ -10,32 +10,80 @@ namespace Avalonia.X11.Glx public GlxInterface Glx { get; } private readonly X11Info _x11; private readonly IntPtr _defaultXid; + private readonly bool _ownsPBuffer; private readonly object _lock = new object(); - public GlxContext(GlxInterface glx, IntPtr handle, GlxDisplay display, X11Info x11, IntPtr defaultXid) + public GlxContext(GlxInterface glx, IntPtr handle, GlxDisplay display, + GlVersion version, int sampleCount, int stencilSize, + X11Info x11, IntPtr defaultXid, + bool ownsPBuffer) { Handle = handle; Glx = glx; _x11 = x11; _defaultXid = defaultXid; + _ownsPBuffer = ownsPBuffer; Display = display; + Version = version; + SampleCount = sampleCount; + StencilSize = stencilSize; + using (MakeCurrent()) + GlInterface = new GlInterface(version, GlxInterface.SafeGetProcAddress); } public GlxDisplay Display { get; } - IGlDisplay IGlContext.Display => Display; + public GlVersion Version { get; } + public GlInterface GlInterface { get; } + public int SampleCount { get; } + public int StencilSize { get; } public IDisposable Lock() { Monitor.Enter(_lock); return Disposable.Create(() => Monitor.Exit(_lock)); } + + class RestoreContext : IDisposable + { + private GlxInterface _glx; + private IntPtr _defaultDisplay; + private IntPtr _display; + private IntPtr _context; + private IntPtr _read; + private IntPtr _draw; + + public RestoreContext(GlxInterface glx, IntPtr defaultDisplay) + { + _glx = glx; + _defaultDisplay = defaultDisplay; + _display = _glx.GetCurrentDisplay(); + _context = _glx.GetCurrentContext(); + _read = _glx.GetCurrentReadDrawable(); + _draw = _glx.GetCurrentDrawable(); + } + + public void Dispose() + { + var disp = _display == IntPtr.Zero ? _defaultDisplay : _display; + _glx.MakeContextCurrent(disp, _draw, _read, _context); + } + } - public void MakeCurrent() => MakeCurrent(_defaultXid); + public IDisposable MakeCurrent() => MakeCurrent(_defaultXid); - public void MakeCurrent(IntPtr xid) + public IDisposable MakeCurrent(IntPtr xid) { + var old = new RestoreContext(Glx, _x11.Display); if (!Glx.MakeContextCurrent(_x11.Display, xid, xid, Handle)) throw new OpenGlException("glXMakeContextCurrent failed "); + return old; + } + + public void Dispose() + { + Glx.DestroyContext(_x11.Display, Handle); + if (_ownsPBuffer) + Glx.DestroyPbuffer(_x11.Display, _defaultXid); } } } diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index 04f2a7137c..903d6b570b 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -1,28 +1,28 @@ using System; +using System.Collections.Generic; using System.Linq; using Avalonia.OpenGL; using static Avalonia.X11.Glx.GlxConsts; namespace Avalonia.X11.Glx { - unsafe class GlxDisplay : IGlDisplay + unsafe class GlxDisplay { private readonly X11Info _x11; + private readonly List _probeProfiles; private readonly IntPtr _fbconfig; private readonly XVisualInfo* _visual; - public GlDisplayType Type => GlDisplayType.OpenGL2; - public GlInterface GlInterface { get; } + private string[] _displayExtensions; + private GlVersion? _version; public XVisualInfo* VisualInfo => _visual; - public int SampleCount { get; } - public int StencilSize { get; } - - public GlxContext ImmediateContext { get; } public GlxContext DeferredContext { get; } public GlxInterface Glx { get; } = new GlxInterface(); - public GlxDisplay(X11Info x11) + public GlxDisplay(X11Info x11, List probeProfiles) { _x11 = x11; + _probeProfiles = probeProfiles.ToList(); + _displayExtensions = Glx.GetExtensions(_x11.Display); var baseAttribs = new[] { @@ -38,7 +38,8 @@ namespace Avalonia.X11.Glx GLX_STENCIL_SIZE, 8, }; - + int sampleCount = 0; + int stencilSize = 0; foreach (var attribs in new[] { //baseAttribs.Concat(multiattribs), @@ -71,9 +72,9 @@ namespace Avalonia.X11.Glx if (_visual == null) throw new OpenGlException("Unable to get visual info from FBConfig"); if (Glx.GetFBConfigAttrib(_x11.Display, _fbconfig, GLX_SAMPLES, out var samples) == 0) - SampleCount = samples; + sampleCount = samples; if (Glx.GetFBConfigAttrib(_x11.Display, _fbconfig, GLX_STENCIL_SIZE, out var stencil) == 0) - StencilSize = stencil; + stencilSize = stencil; var pbuffers = Enumerable.Range(0, 2).Select(_ => Glx.CreatePbuffer(_x11.Display, _fbconfig, new[] { @@ -81,67 +82,104 @@ namespace Avalonia.X11.Glx })).ToList(); XLib.XFlush(_x11.Display); - - ImmediateContext = CreateContext(pbuffers[0],null); - DeferredContext = CreateContext(pbuffers[1], ImmediateContext); - ImmediateContext.MakeCurrent(); - var err = Glx.GetError(); - - GlInterface = new GlInterface(GlxInterface.GlxGetProcAddress); - if (GlInterface.Version == null) - throw new OpenGlException("GL version string is null, aborting"); - if (GlInterface.Renderer == null) - throw new OpenGlException("GL renderer string is null, aborting"); - if (Environment.GetEnvironmentVariable("AVALONIA_GLX_IGNORE_RENDERER_BLACKLIST") != "1") + DeferredContext = CreateContext(CreatePBuffer(), null, + sampleCount, stencilSize, true); + using (DeferredContext.MakeCurrent()) { - var blacklist = AvaloniaLocator.Current.GetService() - ?.GlxRendererBlacklist; - if (blacklist != null) - foreach(var item in blacklist) - if (GlInterface.Renderer.Contains(item)) - throw new OpenGlException($"Renderer '{GlInterface.Renderer}' is blacklisted by '{item}'"); + var glInterface = DeferredContext.GlInterface; + if (glInterface.Version == null) + throw new OpenGlException("GL version string is null, aborting"); + if (glInterface.Renderer == null) + throw new OpenGlException("GL renderer string is null, aborting"); + + if (Environment.GetEnvironmentVariable("AVALONIA_GLX_IGNORE_RENDERER_BLACKLIST") != "1") + { + var blacklist = AvaloniaLocator.Current.GetService() + ?.GlxRendererBlacklist; + if (blacklist != null) + foreach (var item in blacklist) + if (glInterface.Renderer.Contains(item)) + throw new OpenGlException( + $"Renderer '{glInterface.Renderer}' is blacklisted by '{item}'"); + } } - + + } + + IntPtr CreatePBuffer() + { + return Glx.CreatePbuffer(_x11.Display, _fbconfig, new[] { GLX_PBUFFER_WIDTH, 1, GLX_PBUFFER_HEIGHT, 1, 0 }); } - public void ClearContext() => Glx.MakeContextCurrent(_x11.Display, - IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); - public GlxContext CreateContext(IGlContext share) => CreateContext(IntPtr.Zero, share); - public GlxContext CreateContext(IntPtr defaultXid, IGlContext share) + public GlxContext CreateContext() => CreateContext(DeferredContext); + + GlxContext CreateContext(IGlContext share) => CreateContext(CreatePBuffer(), share, + share.SampleCount, share.StencilSize, true); + + GlxContext CreateContext(IntPtr defaultXid, IGlContext share, + int sampleCount, int stencilSize, bool ownsPBuffer) { var sharelist = ((GlxContext)share)?.Handle ?? IntPtr.Zero; IntPtr handle = default; - foreach (var ver in new[] - { - new Version(4, 0), new Version(3, 2), - new Version(3, 0), new Version(2, 0) - }) + + GlxContext Create(GlVersion profile) { + var profileMask = GLX_CONTEXT_CORE_PROFILE_BIT_ARB; + if (profile.Type == GlProfileType.OpenGLES) + profileMask = GLX_CONTEXT_ES2_PROFILE_BIT_EXT; - var attrs = new[] + var attrs = new int[] { - GLX_CONTEXT_MAJOR_VERSION_ARB, ver.Major, - GLX_CONTEXT_MINOR_VERSION_ARB, ver.Minor, - GLX_CONTEXT_PROFILE_MASK_ARB, GLX_CONTEXT_CORE_PROFILE_BIT_ARB, + GLX_CONTEXT_MAJOR_VERSION_ARB, profile.Major, + GLX_CONTEXT_MINOR_VERSION_ARB, profile.Minor, + GLX_CONTEXT_PROFILE_MASK_ARB, profileMask, 0 }; + try { handle = Glx.CreateContextAttribsARB(_x11.Display, _fbconfig, sharelist, true, attrs); if (handle != IntPtr.Zero) - break; + { + _version = profile; + return new GlxContext(new GlxInterface(), handle, this, profile, + sampleCount, stencilSize, _x11, defaultXid, ownsPBuffer); + + } } catch { - break; + return null; } + + return null; + } + + GlxContext rv = null; + if (_version.HasValue) + { + rv = Create(_version.Value); } - if (handle == IntPtr.Zero) - throw new OpenGlException("Unable to create direct GLX context"); - return new GlxContext(new GlxInterface(), handle, this, _x11, defaultXid); + foreach (var v in _probeProfiles) + { + if (v.Type == GlProfileType.OpenGLES + && !_displayExtensions.Contains("GLX_EXT_create_context_es2_profile")) + continue; + rv = Create(v); + if (rv != null) + { + _version = v; + break; + } + } + + if (rv != null) + return rv; + + throw new OpenGlException("Unable to create direct GLX context"); } public void SwapBuffers(IntPtr xid) => Glx.SwapBuffers(_x11.Display, xid); diff --git a/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs b/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs index 45a46bd6f5..ae6b0eb353 100644 --- a/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs +++ b/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs @@ -43,8 +43,8 @@ namespace Avalonia.X11.Glx var l = _context.Lock(); try { - _context.MakeCurrent(_info.Handle); - return new Session(_context, _info, l); + + return new Session(_context, _info, l, _context.MakeCurrent(_info.Handle)); } catch { @@ -58,28 +58,31 @@ namespace Avalonia.X11.Glx private readonly GlxContext _context; private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; private IDisposable _lock; + private readonly IDisposable _clearContext; + public IGlContext Context => _context; public Session(GlxContext context, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info, - IDisposable @lock) + IDisposable @lock, IDisposable clearContext) { _context = context; _info = info; _lock = @lock; + _clearContext = clearContext; } public void Dispose() { - _context.Display.GlInterface.Flush(); + _context.GlInterface.Flush(); _context.Glx.WaitGL(); _context.Display.SwapBuffers(_info.Handle); _context.Glx.WaitX(); - _context.Display.ClearContext(); + _clearContext.Dispose(); _lock.Dispose(); } - public IGlDisplay Display => _context.Display; public PixelSize Size => _info.Size; public double Scaling => _info.Scaling; + public bool IsYFlipped { get; } } } } diff --git a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs index 3dc2e8e41f..e3250e6733 100644 --- a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs +++ b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.Logging; using Avalonia.OpenGL; @@ -7,12 +8,13 @@ namespace Avalonia.X11.Glx class GlxGlPlatformFeature : IWindowingPlatformGlFeature { public GlxDisplay Display { get; private set; } - public IGlContext ImmediateContext { get; private set; } + public IGlContext CreateContext() => Display.CreateContext(); public GlxContext DeferredContext { get; private set; } + public IGlContext MainContext => DeferredContext; - public static bool TryInitialize(X11Info x11) + public static bool TryInitialize(X11Info x11, List glProfiles) { - var feature = TryCreate(x11); + var feature = TryCreate(x11, glProfiles); if (feature != null) { AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); @@ -22,21 +24,20 @@ namespace Avalonia.X11.Glx return false; } - public static GlxGlPlatformFeature TryCreate(X11Info x11) + public static GlxGlPlatformFeature TryCreate(X11Info x11, List glProfiles) { try { - var disp = new GlxDisplay(x11); + var disp = new GlxDisplay(x11, glProfiles); return new GlxGlPlatformFeature { Display = disp, - ImmediateContext = disp.ImmediateContext, DeferredContext = disp.DeferredContext }; } 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 42acb087bc..82cf3c934f 100644 --- a/src/Avalonia.X11/NativeDialogs/Gtk.cs +++ b/src/Avalonia.X11/NativeDialogs/Gtk.cs @@ -190,6 +190,9 @@ namespace Avalonia.X11.NativeDialogs [DllImport(GtkName)] public static extern void gtk_file_chooser_set_filename(IntPtr chooser, Utf8Buffer file); + [DllImport(GtkName)] + public static extern void gtk_file_chooser_set_current_name(IntPtr chooser, Utf8Buffer file); + [DllImport(GtkName)] public static extern IntPtr gtk_file_filter_new(); @@ -204,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); @@ -216,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/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs index ffd1c1b57e..287a541bc8 100644 --- a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs +++ b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs @@ -91,29 +91,41 @@ namespace Avalonia.X11.NativeDialogs gtk_dialog_add_button(dlg, open, GtkResponseType.Cancel); if (initialFileName != null) using (var fn = new Utf8Buffer(initialFileName)) - gtk_file_chooser_set_filename(dlg, fn); + { + if (action == GtkFileChooserAction.Save) + gtk_file_chooser_set_current_name(dlg, fn); + else + gtk_file_chooser_set_filename(dlg, fn); + } + gtk_window_present(dlg); return tcs.Task; } - public async Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + public async Task ShowFileDialogAsync(FileDialog dialog, Window parent) { await EnsureInitialized(); + + var platformImpl = parent?.PlatformImpl; + return await await RunOnGlibThread( - () => ShowDialog(dialog.Title, parent, + () => ShowDialog(dialog.Title, platformImpl, dialog is OpenFileDialog ? GtkFileChooserAction.Open : GtkFileChooserAction.Save, (dialog as OpenFileDialog)?.AllowMultiple ?? false, - Path.Combine(string.IsNullOrEmpty(dialog.InitialDirectory) ? "" : dialog.InitialDirectory, + Path.Combine(string.IsNullOrEmpty(dialog.Directory) ? "" : dialog.Directory, string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName), dialog.Filters)); } - public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) { await EnsureInitialized(); + + var platformImpl = parent?.PlatformImpl; + return await await RunOnGlibThread(async () => { - var res = await ShowDialog(dialog.Title, parent, - GtkFileChooserAction.SelectFolder, false, dialog.InitialDirectory, null); + var res = await ShowDialog(dialog.Title, platformImpl, + GtkFileChooserAction.SelectFolder, false, dialog.Directory, null); return res?.FirstOrDefault(); }); } diff --git a/src/Avalonia.X11/TransparencyHelper.cs b/src/Avalonia.X11/TransparencyHelper.cs new file mode 100644 index 0000000000..0578680136 --- /dev/null +++ b/src/Avalonia.X11/TransparencyHelper.cs @@ -0,0 +1,82 @@ +using System; +using Avalonia.Controls; + +namespace Avalonia.X11 +{ + class TransparencyHelper : IDisposable, X11Globals.IGlobalsSubscriber + { + private readonly X11Info _x11; + private readonly IntPtr _window; + private readonly X11Globals _globals; + private WindowTransparencyLevel _currentLevel; + private WindowTransparencyLevel _requestedLevel; + private bool _isCompositing; + private bool _blurAtomsAreSet; + + public Action TransparencyLevelChanged { get; set; } + public WindowTransparencyLevel CurrentLevel => _currentLevel; + + public TransparencyHelper(X11Info x11, IntPtr window, X11Globals globals) + { + _x11 = x11; + _window = window; + _globals = globals; + _globals.AddSubscriber(this); + } + + public void SetTransparencyRequest(WindowTransparencyLevel level) + { + _requestedLevel = level; + UpdateTransparency(); + } + + private void UpdateTransparency() + { + var newLevel = UpdateAtomsAndGetTransparency(); + if (newLevel != _currentLevel) + { + _currentLevel = newLevel; + TransparencyLevelChanged?.Invoke(newLevel); + } + } + + private WindowTransparencyLevel UpdateAtomsAndGetTransparency() + { + if (_requestedLevel >= WindowTransparencyLevel.Blur) + { + if (!_blurAtomsAreSet) + { + IntPtr value = IntPtr.Zero; + XLib.XChangeProperty(_x11.Display, _window, _x11.Atoms._KDE_NET_WM_BLUR_BEHIND_REGION, + _x11.Atoms.XA_CARDINAL, 32, PropertyMode.Replace, ref value, 1); + _blurAtomsAreSet = true; + } + } + else + { + if (_blurAtomsAreSet) + { + XLib.XDeleteProperty(_x11.Display, _window, _x11.Atoms._KDE_NET_WM_BLUR_BEHIND_REGION); + _blurAtomsAreSet = false; + } + } + + if (!_globals.IsCompositionEnabled) + return WindowTransparencyLevel.None; + if (_requestedLevel >= WindowTransparencyLevel.Blur && CanBlur) + return WindowTransparencyLevel.Blur; + return WindowTransparencyLevel.Transparent; + } + + private bool CanBlur => _globals.WmName == "KWin" && _globals.IsCompositionEnabled; + + public void Dispose() + { + _globals.RemoveSubscriber(this); + } + + void X11Globals.IGlobalsSubscriber.WmChanged(string wmName) => UpdateTransparency(); + + void X11Globals.IGlobalsSubscriber.CompositionChanged(bool compositing) => UpdateTransparency(); + } +} diff --git a/src/Avalonia.X11/X11Atoms.cs b/src/Avalonia.X11/X11Atoms.cs index db74a32b99..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; @@ -156,6 +158,7 @@ namespace Avalonia.X11 public readonly IntPtr _NET_SYSTEM_TRAY_OPCODE; public readonly IntPtr _NET_WM_STATE_MAXIMIZED_HORZ; public readonly IntPtr _NET_WM_STATE_MAXIMIZED_VERT; + public readonly IntPtr _NET_WM_STATE_FULLSCREEN; public readonly IntPtr _XEMBED; public readonly IntPtr _XEMBED_INFO; public readonly IntPtr _MOTIF_WM_HINTS; @@ -184,10 +187,15 @@ namespace Avalonia.X11 public readonly IntPtr UTF8_STRING; public readonly IntPtr UTF16_STRING; 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 @@ -201,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 cb9d3389e4..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, - target, 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/X11Framebuffer.cs b/src/Avalonia.X11/X11Framebuffer.cs index 00288f300d..94f930e9ec 100644 --- a/src/Avalonia.X11/X11Framebuffer.cs +++ b/src/Avalonia.X11/X11Framebuffer.cs @@ -14,6 +14,10 @@ namespace Avalonia.X11 public X11Framebuffer(IntPtr display, IntPtr xid, int depth, int width, int height, double factor) { + // HACK! Please fix renderer, should never ask for 0x0 bitmap. + width = Math.Max(1, width); + height = Math.Max(1, height); + _display = display; _xid = xid; _depth = depth; diff --git a/src/Avalonia.X11/X11Globals.cs b/src/Avalonia.X11/X11Globals.cs new file mode 100644 index 0000000000..afde15b808 --- /dev/null +++ b/src/Avalonia.X11/X11Globals.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using static Avalonia.X11.XLib; +namespace Avalonia.X11 +{ + unsafe class X11Globals + { + private readonly AvaloniaX11Platform _plat; + private readonly int _screenNumber; + private readonly X11Info _x11; + private readonly IntPtr _rootWindow; + private readonly IntPtr _compositingAtom; + private readonly List _subscribers = new List(); + + private string _wmName; + private IntPtr _compositionAtomOwner; + private bool _isCompositionEnabled; + + public X11Globals(AvaloniaX11Platform plat) + { + _plat = plat; + _x11 = plat.Info; + _screenNumber = XDefaultScreen(_x11.Display); + _rootWindow = XRootWindow(_x11.Display, _screenNumber); + plat.Windows[_rootWindow] = OnRootWindowEvent; + XSelectInput(_x11.Display, _rootWindow, + new IntPtr((int)(EventMask.StructureNotifyMask | EventMask.PropertyChangeMask))); + _compositingAtom = XInternAtom(_x11.Display, "_NET_WM_CM_S" + _screenNumber, false); + UpdateWmName(); + UpdateCompositingAtomOwner(); + } + + public string WmName + { + get => _wmName; + private set + { + if (_wmName != value) + { + _wmName = value; + // The collection might change during enumeration + foreach (var s in _subscribers.ToList()) + s.WmChanged(value); + } + } + } + + private IntPtr CompositionAtomOwner + { + get => _compositionAtomOwner; + set + { + if (_compositionAtomOwner != value) + { + _compositionAtomOwner = value; + IsCompositionEnabled = _compositionAtomOwner != IntPtr.Zero; + } + } + } + + public bool IsCompositionEnabled + { + get => _isCompositionEnabled; + set + { + if (_isCompositionEnabled != value) + { + _isCompositionEnabled = value; + // The collection might change during enumeration + foreach (var s in _subscribers.ToList()) + s.CompositionChanged(value); + } + } + } + + IntPtr GetSupportingWmCheck(IntPtr window) + { + XGetWindowProperty(_x11.Display, _rootWindow, _x11.Atoms._NET_SUPPORTING_WM_CHECK, + IntPtr.Zero, new IntPtr(IntPtr.Size), false, + _x11.Atoms.XA_WINDOW, out IntPtr actualType, out int actualFormat, out IntPtr nitems, + out IntPtr bytesAfter, out IntPtr prop); + if (nitems.ToInt32() != 1) + return IntPtr.Zero; + try + { + if (actualType != _x11.Atoms.XA_WINDOW) + return IntPtr.Zero; + return *(IntPtr*)prop.ToPointer(); + } + finally + { + XFree(prop); + } + } + + void UpdateCompositingAtomOwner() + { + // This procedure is described in https://tronche.com/gui/x/icccm/sec-2.html#s-2.8 + + // Check the server-side selection owner + var newOwner = XGetSelectionOwner(_x11.Display, _compositingAtom); + while (CompositionAtomOwner != newOwner) + { + // We have a new owner, unsubscribe from the previous one first + if (CompositionAtomOwner != IntPtr.Zero) + { + _plat.Windows.Remove(CompositionAtomOwner); + XSelectInput(_x11.Display, CompositionAtomOwner, IntPtr.Zero); + } + + // Set it as the current owner and select input + CompositionAtomOwner = newOwner; + if (CompositionAtomOwner != IntPtr.Zero) + { + _plat.Windows[newOwner] = HandleCompositionAtomOwnerEvents; + XSelectInput(_x11.Display, CompositionAtomOwner, new IntPtr((int)(EventMask.StructureNotifyMask))); + } + + // Check for the new owner again and repeat the procedure if it was changed between XGetSelectionOwner and XSelectInput call + newOwner = XGetSelectionOwner(_x11.Display, _compositingAtom); + } + } + + private void HandleCompositionAtomOwnerEvents(XEvent ev) + { + if(ev.type == XEventName.DestroyNotify) + UpdateCompositingAtomOwner(); + } + + void UpdateWmName() => WmName = GetWmName(); + + string GetWmName() + { + var wm = GetSupportingWmCheck(_rootWindow); + if (wm == IntPtr.Zero || wm != GetSupportingWmCheck(wm)) + return null; + XGetWindowProperty(_x11.Display, wm, _x11.Atoms._NET_WM_NAME, + IntPtr.Zero, new IntPtr(0x7fffffff), + false, _x11.Atoms.UTF8_STRING, out var actualType, out var actualFormat, + out var nitems, out _, out var prop); + if (nitems == IntPtr.Zero) + return null; + try + { + if (actualFormat != 8) + return null; + return Marshal.PtrToStringAnsi(prop, nitems.ToInt32()); + } + finally + { + XFree(prop); + } + } + + private void OnRootWindowEvent(XEvent ev) + { + if (ev.type == XEventName.PropertyNotify) + { + if(ev.PropertyEvent.atom == _x11.Atoms._NET_SUPPORTING_WM_CHECK) + UpdateWmName(); + } + + if (ev.type == XEventName.ClientMessage) + { + if(ev.ClientMessageEvent.message_type == _x11.Atoms.MANAGER + && ev.ClientMessageEvent.ptr2 == _compositingAtom) + UpdateCompositingAtomOwner(); + } + } + + public interface IGlobalsSubscriber + { + void WmChanged(string wmName); + void CompositionChanged(bool compositing); + } + + public void AddSubscriber(IGlobalsSubscriber subscriber) => _subscribers.Add(subscriber); + public void RemoveSubscriber(IGlobalsSubscriber subscriber) => _subscribers.Remove(subscriber); + } +} diff --git a/src/Avalonia.X11/X11IconLoader.cs b/src/Avalonia.X11/X11IconLoader.cs index f0e75536d0..093f2b12c1 100644 --- a/src/Avalonia.X11/X11IconLoader.cs +++ b/src/Avalonia.X11/X11IconLoader.cs @@ -59,7 +59,7 @@ namespace Avalonia.X11 } using(var rt = AvaloniaLocator.Current.GetService().CreateRenderTarget(new[]{this})) using (var ctx = rt.CreateDrawingContext(null)) - ctx.DrawImage(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), + ctx.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), new Rect(0, 0, _width, _height)); Data = new UIntPtr[_width * _height + 2]; Data[0] = new UIntPtr((uint)_width); diff --git a/src/Avalonia.X11/X11ImmediateRendererProxy.cs b/src/Avalonia.X11/X11ImmediateRendererProxy.cs new file mode 100644 index 0000000000..7280567bb3 --- /dev/null +++ b/src/Avalonia.X11/X11ImmediateRendererProxy.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.X11 +{ + public class X11ImmediateRendererProxy : IRenderer, IRenderLoopTask + { + private readonly IRenderLoop _loop; + private ImmediateRenderer _renderer; + private bool _invalidated; + private bool _running; + private object _lock = new object(); + + public X11ImmediateRendererProxy(IVisual root, IRenderLoop loop) + { + _loop = loop; + _renderer = new ImmediateRenderer(root); + + } + + public void Dispose() + { + _running = false; + _renderer.Dispose(); + } + + public bool DrawFps + { + get => _renderer.DrawFps; + set => _renderer.DrawFps = value; + } + + public bool DrawDirtyRects + { + get => _renderer.DrawDirtyRects; + set => _renderer.DrawDirtyRects = value; + } + + public event EventHandler SceneInvalidated + { + add => _renderer.SceneInvalidated += value; + remove => _renderer.SceneInvalidated -= value; + } + + public void AddDirty(IVisual visual) + { + lock (_lock) + _invalidated = true; + _renderer.AddDirty(visual); + } + + public IEnumerable HitTest(Point p, IVisual root, Func filter) + { + return _renderer.HitTest(p, root, filter); + } + + public IVisual HitTestFirst(Point p, IVisual root, Func filter) + { + return _renderer.HitTestFirst(p, root, filter); + } + + public void RecalculateChildren(IVisual visual) + { + _renderer.RecalculateChildren(visual); + } + + public void Resized(Size size) + { + _renderer.Resized(size); + } + + public void Paint(Rect rect) + { + _invalidated = false; + _renderer.Paint(rect); + } + + public void Start() + { + _running = true; + _loop.Add(this); + _renderer.Start(); + } + + public void Stop() + { + _running = false; + _loop.Remove(this); + _renderer.Stop(); + } + + public bool NeedsUpdate => false; + public void Update(TimeSpan time) + { + + } + + public void Render() + { + if (_invalidated) + { + lock (_lock) + _invalidated = false; + Dispatcher.UIThread.Post(() => + { + if (_running) + Paint(new Rect(0, 0, 100000, 100000)); + }); + } + } + } +} diff --git a/src/Avalonia.X11/X11NativeControlHost.cs b/src/Avalonia.X11/X11NativeControlHost.cs new file mode 100644 index 0000000000..add3bc6585 --- /dev/null +++ b/src/Avalonia.X11/X11NativeControlHost.cs @@ -0,0 +1,190 @@ +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 Hide() + { + if(_attachedTo == null || _child == null) + return; + _mapped = false; + XUnmapWindow(_display, _holder.Handle); + } + + public void ShowInBounds(TransformedBounds transformedBounds) + { + CheckDisposed(); + if (_attachedTo == null) + throw new InvalidOperationException("The control isn't currently attached to a toplevel"); + var bounds = transformedBounds.Bounds.TransformToAABB(transformedBounds.Transform) * + new Vector(_attachedTo.Window.Scaling, _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; + } + Console.WriteLine($"Moved {_child.Handle} to {pixelRect}"); + } + } + } +} diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index d7a7bb97fd..9e9c4fd30e 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -19,25 +19,28 @@ namespace Avalonia.X11 class AvaloniaX11Platform : IWindowingPlatform { private Lazy _keyboardDevice = new Lazy(() => new KeyboardDevice()); - private Lazy _mouseDevice = new Lazy(() => new MouseDevice()); public KeyboardDevice KeyboardDevice => _keyboardDevice.Value; - public MouseDevice MouseDevice => _mouseDevice.Value; public Dictionary> Windows = new Dictionary>(); public XI2Manager XI2; public X11Info Info { get; private set; } 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) { Options = options; 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(); Info = new X11Info(Display, DeferredDisplay); + Globals = new X11Globals(this); //TODO: log if (options.UseDBusMenu) DBusHelper.TryInitialize(); @@ -46,7 +49,7 @@ namespace Avalonia.X11 .Bind().ToConstant(new X11PlatformThreading(this)) .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new RenderLoop()) - .Bind().ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Control)) + .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control)) .Bind().ToFunc(() => KeyboardDevice) .Bind().ToConstant(new X11CursorFactory(Display)) .Bind().ToConstant(new X11Clipboard(this)) @@ -69,7 +72,7 @@ namespace Avalonia.X11 if (options.UseEGL) EglGlPlatformFeature.TryInitialize(); else - GlxGlPlatformFeature.TryInitialize(Info); + GlxGlPlatformFeature.TryInitialize(Info, Options.GlProfiles); } @@ -82,7 +85,7 @@ namespace Avalonia.X11 return new X11Window(this, null); } - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public IWindowImpl CreateEmbeddableWindow() { throw new NotSupportedException(); } @@ -98,6 +101,17 @@ namespace Avalonia public bool UseGpu { get; set; } = true; public bool OverlayPopups { get; set; } public bool UseDBusMenu { get; set; } + public bool UseDeferredRendering { get; set; } = true; + + public List GlProfiles { get; set; } = new List + { + new GlVersion(GlProfileType.OpenGL, 4, 0), + new GlVersion(GlProfileType.OpenGL, 3, 2), + new GlVersion(GlProfileType.OpenGL, 3, 0), + new GlVersion(GlProfileType.OpenGLES, 3, 2), + new GlVersion(GlProfileType.OpenGLES, 3, 0), + new GlVersion(GlProfileType.OpenGLES, 2, 0) + }; public List GlxRendererBlacklist { get; set; } = new List { diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 860456a838..499fe5a67a 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -21,18 +21,20 @@ 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; private readonly bool _popup; private readonly X11Info _x11; - private bool _invalidated; private XConfigureEvent? _configure; private PixelPoint? _configurePoint; private bool _triggeredExpose; private IInputRoot _inputRoot; - private readonly IMouseDevice _mouse; + private readonly MouseDevice _mouse; + private readonly TouchDevice _touch; private readonly IKeyboardDevice _keyboard; private PixelPoint? _position; private PixelSize _realSize; @@ -40,9 +42,11 @@ namespace Avalonia.X11 private IntPtr _xic; private IntPtr _renderHandle; private bool _mapped; - private HashSet _transientChildren = new HashSet(); - private X11Window _transientParent; + private bool _wasMappedAtLeastOnce = false; private double? _scalingOverride; + private bool _disabled; + private TransparencyHelper _transparencyHelper; + public object SyncRoot { get; } = new object(); class InputEventContainer @@ -57,7 +61,8 @@ namespace Avalonia.X11 _platform = platform; _popup = popupParent != null; _x11 = platform.Info; - _mouse = platform.MouseDevice; + _mouse = new MouseDevice(); + _touch = new TouchDevice(); _keyboard = platform.KeyboardDevice; var glfeature = AvaloniaLocator.Current.GetService(); @@ -100,7 +105,7 @@ namespace Avalonia.X11 valueMask |= SetWindowValuemask.ColorMap; } - int defaultWidth = 300, defaultHeight = 200; + int defaultWidth = 0, defaultHeight = 0; if (!_popup && Screen != null) { @@ -115,6 +120,10 @@ namespace Avalonia.X11 } } + // check if the calculated size is zero then compensate to hardcoded resolution + defaultWidth = Math.Max(defaultWidth, 300); + defaultHeight = Math.Max(defaultHeight, 200); + _handle = XCreateWindow(_x11.Display, _x11.RootWindow, 10, 10, defaultWidth, defaultHeight, 0, depth, (int)CreateWindowArgs.InputOutput, @@ -131,7 +140,7 @@ namespace Avalonia.X11 _renderHandle = _handle; Handle = new PlatformHandle(_handle, "XID"); - _realSize = new PixelSize(300, 200); + _realSize = new PixelSize(defaultWidth, defaultHeight); platform.Windows[_handle] = OnEvent; XEventMask ignoredMask = XEventMask.SubstructureRedirectMask | XEventMask.ResizeRedirectMask @@ -159,7 +168,7 @@ namespace Avalonia.X11 if (egl != null) surfaces.Insert(0, - new EglGlPlatformSurface((EglDisplay)egl.Display, egl.DeferredContext, + new EglGlPlatformSurface(egl.DeferredContext, new SurfaceInfo(this, _x11.DeferredDisplay, _handle, _renderHandle))); if (glx != null) surfaces.Insert(0, new GlxGlPlatformSurface(glx.Display, glx.DeferredContext, @@ -167,13 +176,18 @@ namespace Avalonia.X11 Surfaces = surfaces.ToArray(); UpdateMotifHints(); + UpdateSizeHints(null); _xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing, XNames.XNClientWindow, _handle, IntPtr.Zero); + _transparencyHelper = new TransparencyHelper(_x11, _handle, platform.Globals); + _transparencyHelper.SetTransparencyRequest(WindowTransparencyLevel.None); + XFlush(_x11.Display); if(_popup) PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize)); if (platform.Options.UseDBusMenu) NativeMenuExporter = DBusMenuExporter.TryCreate(_handle); + NativeControlHost = new X11NativeControlHost(_platform, this); } class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo @@ -213,10 +227,9 @@ namespace Avalonia.X11 var decorations = MotifDecorations.Menu | MotifDecorations.Title | MotifDecorations.Border | MotifDecorations.Maximize | MotifDecorations.Minimize | MotifDecorations.ResizeH; - if (_popup || !_systemDecorations) - { + if (_popup + || _systemDecorations == SystemDecorations.None) decorations = 0; - } if (!_canResize) { @@ -294,11 +307,24 @@ namespace Avalonia.X11 public Action Activated { get; set; } public Func Closing { get; set; } public Action WindowStateChanged { get; set; } + + public Action TransparencyLevelChanged + { + get => _transparencyHelper.TransparencyLevelChanged; + set => _transparencyHelper.TransparencyLevelChanged = value; + } + public Action Closed { get; set; } public Action PositionChanged { get; set; } + public Action LostFocus { get; set; } - public IRenderer CreateRenderer(IRenderRoot root) => - new DeferredRenderer(root, AvaloniaLocator.Current.GetService()); + public IRenderer CreateRenderer(IRenderRoot root) + { + var loop = AvaloniaLocator.Current.GetService(); + return _platform.Options.UseDeferredRendering ? + new DeferredRenderer(root, loop) : + (IRenderer)new X11ImmediateRendererProxy(root, loop); + } void OnEvent(XEvent ev) { @@ -317,7 +343,9 @@ namespace Avalonia.X11 } else if (ev.type == XEventName.UnmapNotify) _mapped = false; - else if (ev.type == XEventName.Expose) + else if (ev.type == XEventName.Expose || + (ev.type == XEventName.VisibilityNotify && + ev.VisibilityEvent.state < 2)) { if (!_triggeredExpose) { @@ -349,10 +377,17 @@ namespace Avalonia.X11 { if (ActivateTransientChildIfNeeded()) return; - if (ev.ButtonEvent.button < 4) - MouseEvent(ev.ButtonEvent.button == 1 ? RawPointerEventType.LeftButtonDown - : ev.ButtonEvent.button == 2 ? RawPointerEventType.MiddleButtonDown - : RawPointerEventType.RightButtonDown, ref ev, ev.ButtonEvent.state); + if (ev.ButtonEvent.button < 4 || ev.ButtonEvent.button == 8 || ev.ButtonEvent.button == 9) + MouseEvent( + ev.ButtonEvent.button switch + { + 1 => RawPointerEventType.LeftButtonDown, + 2 => RawPointerEventType.MiddleButtonDown, + 3 => RawPointerEventType.RightButtonDown, + 8 => RawPointerEventType.XButton1Down, + 9 => RawPointerEventType.XButton2Down + }, + ref ev, ev.ButtonEvent.state); else { var delta = ev.ButtonEvent.button == 4 @@ -370,10 +405,17 @@ namespace Avalonia.X11 } else if (ev.type == XEventName.ButtonRelease) { - if (ev.ButtonEvent.button < 4) - MouseEvent(ev.ButtonEvent.button == 1 ? RawPointerEventType.LeftButtonUp - : ev.ButtonEvent.button == 2 ? RawPointerEventType.MiddleButtonUp - : RawPointerEventType.RightButtonUp, ref ev, ev.ButtonEvent.state); + if (ev.ButtonEvent.button < 4 || ev.ButtonEvent.button == 8 || ev.ButtonEvent.button == 9) + MouseEvent( + ev.ButtonEvent.button switch + { + 1 => RawPointerEventType.LeftButtonUp, + 2 => RawPointerEventType.MiddleButtonUp, + 3 => RawPointerEventType.RightButtonUp, + 8 => RawPointerEventType.XButton1Up, + 9 => RawPointerEventType.XButton2Up + }, + ref ev, ev.ButtonEvent.state); } else if (ev.type == XEventName.ConfigureNotify) { @@ -412,7 +454,7 @@ namespace Avalonia.X11 updatedSizeViaScaling = UpdateScaling(); } - if (changedSize && !updatedSizeViaScaling) + if (changedSize && !updatedSizeViaScaling && !_popup) Resized?.Invoke(ClientSize); Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); @@ -455,7 +497,7 @@ namespace Avalonia.X11 key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32(); - ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), + ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot, ev.type == XEventName.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, X11KeyTransform.ConvertKey(key), TranslateModifiers(ev.KeyEvent.state)), ref ev); @@ -470,7 +512,7 @@ namespace Avalonia.X11 if (text[0] < ' ' || text[0] == 0x7f) //Control codes or DEL return; } - ScheduleInput(new RawTextInputEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), text), + ScheduleInput(new RawTextInputEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot, text), ref ev); } } @@ -521,14 +563,23 @@ namespace Avalonia.X11 } else if (value == WindowState.Maximized) { - SendNetWMMessage(_x11.Atoms._NET_WM_STATE, (IntPtr)0, _x11.Atoms._NET_WM_STATE_HIDDEN, IntPtr.Zero); - SendNetWMMessage(_x11.Atoms._NET_WM_STATE, (IntPtr)1, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT, + ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_HIDDEN); + ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_FULLSCREEN); + ChangeWMAtoms(true, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT, + _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ); + } + else if (value == WindowState.FullScreen) + { + ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_HIDDEN); + ChangeWMAtoms(true, _x11.Atoms._NET_WM_STATE_FULLSCREEN); + ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT, _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ); } else { - SendNetWMMessage(_x11.Atoms._NET_WM_STATE, (IntPtr)0, _x11.Atoms._NET_WM_STATE_HIDDEN, IntPtr.Zero); - SendNetWMMessage(_x11.Atoms._NET_WM_STATE, (IntPtr)0, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT, + ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_HIDDEN); + ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_FULLSCREEN); + ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT, _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ); } } @@ -555,6 +606,12 @@ namespace Avalonia.X11 break; } + if(pitems[c] == _x11.Atoms._NET_WM_STATE_FULLSCREEN) + { + state = WindowState.FullScreen; + break; + } + if (pitems[c] == _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ || pitems[c] == _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT) { @@ -584,8 +641,12 @@ namespace Avalonia.X11 rv |= RawInputModifiers.LeftMouseButton; if (state.HasFlag(XModifierMask.Button2Mask)) rv |= RawInputModifiers.RightMouseButton; - if (state.HasFlag(XModifierMask.Button2Mask)) + if (state.HasFlag(XModifierMask.Button3Mask)) rv |= RawInputModifiers.MiddleMouseButton; + if (state.HasFlag(XModifierMask.Button4Mask)) + rv |= RawInputModifiers.XButton1MouseButton; + if (state.HasFlag(XModifierMask.Button5Mask)) + rv |= RawInputModifiers.XButton2MouseButton; if (state.HasFlag(XModifierMask.ShiftMask)) rv |= RawInputModifiers.Shift; if (state.HasFlag(XModifierMask.ControlMask)) @@ -597,7 +658,7 @@ namespace Avalonia.X11 return rv; } - private bool _systemDecorations = true; + private SystemDecorations _systemDecorations = SystemDecorations.Full; private bool _canResize = true; private const int MaxWindowDimension = 100000; @@ -615,7 +676,27 @@ namespace Avalonia.X11 ScheduleInput(args); } - public void ScheduleInput(RawInputEventArgs args) + public void ScheduleXI2Input(RawInputEventArgs args) + { + if (args is RawPointerEventArgs pargs) + { + if ((pargs.Type == RawPointerEventType.TouchBegin + || pargs.Type == RawPointerEventType.TouchUpdate + || pargs.Type == RawPointerEventType.LeftButtonDown + || pargs.Type == RawPointerEventType.RightButtonDown + || pargs.Type == RawPointerEventType.MiddleButtonDown + || pargs.Type == RawPointerEventType.NonClientLeftButtonDown) + && ActivateTransientChildIfNeeded()) + return; + if (pargs.Type == RawPointerEventType.TouchEnd + && ActivateTransientChildIfNeeded()) + pargs.Type = RawPointerEventType.TouchCancel; + } + + ScheduleInput(args); + } + + private void ScheduleInput(RawInputEventArgs args) { if (args is RawPointerEventArgs mouse) mouse.Position = mouse.Position / Scaling; @@ -654,20 +735,12 @@ namespace Avalonia.X11 void DoPaint() { - _invalidated = false; Paint?.Invoke(new Rect()); } public void Invalidate(Rect rect) { - if(_invalidated) - return; - _invalidated = true; - Dispatcher.UIThread.InvokeAsync(() => - { - if (_mapped) - DoPaint(); - }); + } public IInputRoot InputRoot => _inputRoot; @@ -688,7 +761,6 @@ namespace Avalonia.X11 void Cleanup() { - SetTransientParent(null, false); if (_xic != IntPtr.Zero) { XDestroyIC(_xic); @@ -702,6 +774,8 @@ namespace Avalonia.X11 _platform.XI2?.OnWindowDestroyed(_handle); _handle = IntPtr.Zero; Closed?.Invoke(); + _mouse.Dispose(); + _touch.Dispose(); } if (_useRenderWindow && _renderHandle != IntPtr.Zero) @@ -713,48 +787,43 @@ namespace Avalonia.X11 bool ActivateTransientChildIfNeeded() { - if (_transientChildren.Count == 0) - return false; - var child = _transientChildren.First(); - if (!child.ActivateTransientChildIfNeeded()) - child.Activate(); - return true; + if (_disabled) + { + GotInputWhenDisabled?.Invoke(); + return true; + } + + return false; } - - void SetTransientParent(X11Window window, bool informServer = true) + + public void SetParent(IWindowImpl parent) { - _transientParent?._transientChildren.Remove(this); - _transientParent = window; - _transientParent?._transientChildren.Add(this); - if (informServer) - XSetTransientForHint(_x11.Display, _handle, _transientParent?._handle ?? IntPtr.Zero); + if (parent == null || parent.Handle == null || parent.Handle.Handle == IntPtr.Zero) + XDeleteProperty(_x11.Display, _handle, _x11.Atoms.XA_WM_TRANSIENT_FOR); + else + XSetTransientForHint(_x11.Display, _handle, parent.Handle.Handle); } public void Show() { - SetTransientParent(null); - ShowCore(); - } - - void ShowCore() - { + _wasMappedAtLeastOnce = true; XMapWindow(_x11.Display, _handle); XFlush(_x11.Display); } public void Hide() => XUnmapWindow(_x11.Display, _handle); - public Point PointToClient(PixelPoint point) => new Point((point.X - Position.X) / Scaling, (point.Y - Position.Y) / Scaling); public PixelPoint PointToScreen(Point point) => new PixelPoint( (int)(point.X * Scaling + Position.X), (int)(point.Y * Scaling + Position.Y)); - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { - _systemDecorations = enabled; + _systemDecorations = enabled == SystemDecorations.Full ? SystemDecorations.Full : SystemDecorations.None; UpdateMotifHints(); + UpdateSizeHints(null); } @@ -784,7 +853,7 @@ namespace Avalonia.X11 XConfigureResizeWindow(_x11.Display, _renderHandle, pixelSize); XFlush(_x11.Display); - if (force || (_popup && needImmediatePopupResize)) + if (force || !_wasMappedAtLeastOnce || (_popup && needImmediatePopupResize)) { _realSize = pixelSize; Resized?.Invoke(ClientSize); @@ -825,11 +894,18 @@ namespace Avalonia.X11 XConfigureWindow(_x11.Display, _handle, ChangeWindowFlags.CWX | ChangeWindowFlags.CWY, ref changes); XFlush(_x11.Display); + if (!_wasMappedAtLeastOnce) + { + _position = value; + PositionChanged?.Invoke(value); + } } } public IMouseDevice MouseDevice => _mouse; + public TouchDevice TouchDevice => _touch; + public IPopupImpl CreatePopup() => _platform.Options.OverlayPopups ? null : new X11Window(_platform, this); @@ -850,7 +926,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(); @@ -878,21 +954,23 @@ namespace Avalonia.X11 } - void BeginMoveResize(NetWmMoveResize side) + void BeginMoveResize(NetWmMoveResize side, PointerPressedEventArgs e) { var pos = GetCursorPos(_x11); XUngrabPointer(_x11.Display, new IntPtr(0)); SendNetWMMessage (_x11.Atoms._NET_WM_MOVERESIZE, (IntPtr) pos.x, (IntPtr) pos.y, (IntPtr) side, (IntPtr) 1, (IntPtr)1); // left button + + e.Pointer.Capture(null); } - public void BeginMoveDrag() + public void BeginMoveDrag(PointerPressedEventArgs e) { - BeginMoveResize(NetWmMoveResize._NET_WM_MOVERESIZE_MOVE); + BeginMoveResize(NetWmMoveResize._NET_WM_MOVERESIZE_MOVE, e); } - public void BeginResizeDrag(WindowEdge edge) + public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) { var side = NetWmMoveResize._NET_WM_MOVERESIZE_CANCEL; if (edge == WindowEdge.East) @@ -911,7 +989,7 @@ namespace Avalonia.X11 side = NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT; if (edge == WindowEdge.SouthWest) side = NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT; - BeginMoveResize(side); + BeginMoveResize(side, e); } public void SetTitle(string title) @@ -953,32 +1031,81 @@ namespace Avalonia.X11 public void SetTopmost(bool value) { - SendNetWMMessage(_x11.Atoms._NET_WM_STATE, - (IntPtr)(value ? 1 : 0), _x11.Atoms._NET_WM_STATE_ABOVE, IntPtr.Zero); + ChangeWMAtoms(value, _x11.Atoms._NET_WM_STATE_ABOVE); } - - public void ShowDialog(IWindowImpl parent) + + public void SetEnabled(bool enable) { - SetTransientParent((X11Window)parent); - ShowCore(); + _disabled = !enable; } + public Action GotInputWhenDisabled { get; set; } + public void SetIcon(IWindowIconImpl icon) { - var data = ((X11IconData)icon).Data; - fixed (void* pdata = data) - XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_ICON, - new IntPtr((int)Atom.XA_CARDINAL), 32, PropertyMode.Replace, - pdata, data.Length); + if (icon != null) + { + var data = ((X11IconData)icon).Data; + fixed (void* pdata = data) + XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_ICON, + new IntPtr((int)Atom.XA_CARDINAL), 32, PropertyMode.Replace, + pdata, data.Length); + } + else + { + XDeleteProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_ICON); + } } public void ShowTaskbarIcon(bool value) { + ChangeWMAtoms(!value, _x11.Atoms._NET_WM_STATE_SKIP_TASKBAR); + } + + void ChangeWMAtoms(bool enable, params IntPtr[] atoms) + { + if (atoms.Length != 1 && atoms.Length != 2) + throw new ArgumentException(); + + if (!_mapped) + { + XGetWindowProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_STATE, IntPtr.Zero, new IntPtr(256), + false, (IntPtr)Atom.XA_ATOM, out _, out _, out var nitems, out _, + out var prop); + var ptr = (IntPtr*)prop.ToPointer(); + var newAtoms = new HashSet(); + for (var c = 0; c < nitems.ToInt64(); c++) + newAtoms.Add(*ptr); + XFree(prop); + foreach(var atom in atoms) + if (enable) + newAtoms.Add(atom); + else + newAtoms.Remove(atom); + + XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_STATE, (IntPtr)Atom.XA_ATOM, 32, + PropertyMode.Replace, newAtoms.ToArray(), newAtoms.Count); + } + SendNetWMMessage(_x11.Atoms._NET_WM_STATE, - (IntPtr)(value ? 0 : 1), _x11.Atoms._NET_WM_STATE_SKIP_TASKBAR, IntPtr.Zero); + (IntPtr)(enable ? 1 : 0), + atoms[0], + atoms.Length > 1 ? atoms[1] : IntPtr.Zero, + atoms.Length > 2 ? atoms[2] : IntPtr.Zero, + atoms.Length > 3 ? atoms[3] : IntPtr.Zero + ); } 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; } } diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs index 6989d6d26d..742973e0da 100644 --- a/src/Avalonia.X11/XI2Manager.cs +++ b/src/Avalonia.X11/XI2Manager.cs @@ -92,14 +92,12 @@ namespace Avalonia.X11 private PointerDeviceInfo _pointerDevice; private AvaloniaX11Platform _platform; - private readonly TouchDevice _touchDevice = new TouchDevice(); - public bool Init(AvaloniaX11Platform platform) { _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++) @@ -198,7 +196,7 @@ namespace Avalonia.X11 (ev.Type == XiEventType.XI_TouchUpdate ? RawPointerEventType.TouchUpdate : RawPointerEventType.TouchEnd); - client.ScheduleInput(new RawTouchEventArgs(_touchDevice, + client.ScheduleXI2Input(new RawTouchEventArgs(client.TouchDevice, ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail)); return; } @@ -232,23 +230,43 @@ namespace Avalonia.X11 } if (scrollDelta != default) - client.ScheduleInput(new RawMouseWheelEventArgs(_platform.MouseDevice, ev.Timestamp, + client.ScheduleXI2Input(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, ev.Position, scrollDelta, ev.Modifiers)); if (_pointerDevice.HasMotion(ev)) - client.ScheduleInput(new RawPointerEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot, + client.ScheduleXI2Input(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, 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; - var type = - ev.Button == 1 ? (down ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp) - : ev.Button == 2 ? (down ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp) - : ev.Button == 3 ? (down ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp) - : (RawPointerEventType?)null; + var type = ev.Button switch + { + 1 => down ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp, + 2 => down ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp, + 3 => down ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp, + 8 => down ? RawPointerEventType.XButton1Down : RawPointerEventType.XButton1Up, + 9 => down ? RawPointerEventType.XButton2Down : RawPointerEventType.XButton2Up, + _ => (RawPointerEventType?)null + }; if (type.HasValue) - client.ScheduleInput(new RawPointerEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot, + client.ScheduleXI2Input(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, type.Value, ev.Position, ev.Modifiers)); } @@ -285,12 +303,14 @@ namespace Avalonia.X11 var buttons = ev->buttons.Mask; if (XIMaskIsSet(buttons, 1)) Modifiers |= RawInputModifiers.LeftMouseButton; - if (XIMaskIsSet(buttons, 2)) Modifiers |= RawInputModifiers.MiddleMouseButton; - if (XIMaskIsSet(buttons, 3)) Modifiers |= RawInputModifiers.RightMouseButton; + if (XIMaskIsSet(buttons, 8)) + Modifiers |= RawInputModifiers.XButton1MouseButton; + if (XIMaskIsSet(buttons, 9)) + Modifiers |= RawInputModifiers.XButton2MouseButton; } Valuators = new Dictionary(); @@ -309,6 +329,8 @@ namespace Avalonia.X11 interface IXI2Client { IInputRoot InputRoot { get; } - void ScheduleInput(RawInputEventArgs args); + void ScheduleXI2Input(RawInputEventArgs args); + IMouseDevice MouseDevice { get; } + TouchDevice TouchDevice { get; } } } 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 9844cdc03b..2153344363 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.LinuxFramebuffer.Input; @@ -9,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; @@ -70,13 +71,16 @@ namespace Avalonia.LinuxFramebuffer public Action Paint { get; set; } public Action Resized { get; set; } public Action ScalingChanged { get; set; } + + 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; } } } 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 273265a6dc..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 @@ -15,6 +17,8 @@ namespace Avalonia.LinuxFramebuffer.Output private readonly EglGlPlatformSurface _eglPlatformSurface; public PixelSize PixelSize => _mode.Resolution; public double Scaling { get; set; } + public IGlContext MainContext => _deferredContext; + public DrmOutput(string path = null) { var card = new DrmCard(path); @@ -48,7 +52,6 @@ namespace Avalonia.LinuxFramebuffer.Output private drmModeModeInfo _mode; private EglDisplay _eglDisplay; private EglSurface _eglSurface; - private EglContext _immediateContext; private EglContext _deferredContext; private IntPtr _currentBo; private IntPtr _gbmTargetSurface; @@ -70,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); @@ -129,13 +147,15 @@ namespace Avalonia.LinuxFramebuffer.Output return _eglDisplay.CreateContext(share, _eglDisplay.CreateWindowSurface(offSurf)); } - _immediateContext = CreateContext(null); - _deferredContext = CreateContext(_immediateContext); - - _immediateContext.MakeCurrent(_eglSurface); - _eglDisplay.GlInterface.ClearColor(0, 0, 0, 0); - _eglDisplay.GlInterface.Clear(GlConsts.GL_COLOR_BUFFER_BIT | GlConsts.GL_STENCIL_BUFFER_BIT); - _eglSurface.SwapBuffers(); + _deferredContext = CreateContext(null); + + using (_deferredContext.MakeCurrent(_eglSurface)) + { + _deferredContext.GlInterface.ClearColor(0, 0, 0, 0); + _deferredContext.GlInterface.Clear(GlConsts.GL_COLOR_BUFFER_BIT | GlConsts.GL_STENCIL_BUFFER_BIT); + _eglSurface.SwapBuffers(); + } + var bo = gbm_surface_lock_front_buffer(_gbmTargetSurface); var fbId = GetFbIdForBo(bo); var connectorId = connector.Id; @@ -153,9 +173,10 @@ namespace Avalonia.LinuxFramebuffer.Output for(var c=0;c<2;c++) using (CreateGlRenderTarget().BeginDraw()) { - _eglDisplay.GlInterface.ClearColor(0, 0, 0, 0); - _eglDisplay.GlInterface.Clear(GlConsts.GL_COLOR_BUFFER_BIT | GlConsts.GL_STENCIL_BUFFER_BIT); + _deferredContext.GlInterface.ClearColor(0, 0, 0, 0); + _deferredContext.GlInterface.Clear(GlConsts.GL_COLOR_BUFFER_BIT | GlConsts.GL_STENCIL_BUFFER_BIT); } + } public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() @@ -179,15 +200,17 @@ namespace Avalonia.LinuxFramebuffer.Output class RenderSession : IGlPlatformSurfaceRenderingSession { private readonly DrmOutput _parent; + private readonly IDisposable _clearContext; - public RenderSession(DrmOutput parent) + public RenderSession(DrmOutput parent, IDisposable clearContext) { _parent = parent; + _clearContext = clearContext; } public void Dispose() { - _parent._eglDisplay.GlInterface.Flush(); + _parent._deferredContext.GlInterface.Flush(); _parent._eglSurface.SwapBuffers(); var nextBo = gbm_surface_lock_front_buffer(_parent._gbmTargetSurface); @@ -225,27 +248,29 @@ namespace Avalonia.LinuxFramebuffer.Output gbm_surface_release_buffer(_parent._gbmTargetSurface, _parent._currentBo); _parent._currentBo = nextBo; } - _parent._eglDisplay.ClearContext(); + _clearContext.Dispose(); } - public IGlDisplay Display => _parent._eglDisplay; + public IGlContext Context => _parent._deferredContext; public PixelSize Size => _parent._mode.Resolution; public double Scaling => _parent.Scaling; + + public bool IsYFlipped { get; } } public IGlPlatformSurfaceRenderingSession BeginDraw() { - _parent._deferredContext.MakeCurrent(_parent._eglSurface); - return new RenderSession(_parent); + return new RenderSession(_parent, _parent._deferredContext.MakeCurrent(_parent._eglSurface)); } - - } - IGlContext IWindowingPlatformGlFeature.ImmediateContext => _immediateContext; + public IGlContext CreateContext() + { + throw new NotImplementedException(); + } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 00479919a3..14687da6bc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs index db0b617bcd..5c21037924 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs @@ -1,6 +1,3 @@ -// 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 System; using System.IO; using System.Reflection; diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs index 6000b71f9d..45ca1c4adc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs @@ -1,6 +1,3 @@ -// 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 System; using System.ComponentModel; using System.Globalization; @@ -42,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/BitmapTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/BitmapTypeConverter.cs index c75c54554e..dcd60f7a79 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/BitmapTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/BitmapTypeConverter.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Globalization; using Avalonia.Media.Imaging; 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/Converters/FontFamilyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/FontFamilyTypeConverter.cs index e92c155773..b677cdfc22 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/FontFamilyTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/FontFamilyTypeConverter.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.ComponentModel; using System.Globalization; diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/IconTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/IconTypeConverter.cs index 3a2f41bd3d..9e15fb888e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/IconTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/IconTypeConverter.cs @@ -1,7 +1,4 @@ -// 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.Controls; +using Avalonia.Controls; using Avalonia.Media.Imaging; using Avalonia.Platform; using System; diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs index 29c7dbfd39..099ffa8c8c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs @@ -1,13 +1,11 @@ -// 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 System; using System.Collections.Generic; using System.Globalization; namespace Avalonia.Markup.Xaml.Converters { - using System.ComponentModel; + using System.ComponentModel; + using Avalonia.Utilities; public class PointsListTypeConverter : TypeConverter { @@ -18,15 +16,17 @@ namespace Avalonia.Markup.Xaml.Converters public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { - string strValue = (string)value; - string[] pointStrs = strValue.Split(new[] { ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); - var result = new List(pointStrs.Length); - foreach (var pointStr in pointStrs) + var points = new List(); + + using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PointsList.")) { - result.Add(Point.Parse(pointStr)); + while (tokenizer.TryReadDouble(out double x)) + { + points.Add(new Point(x, tokenizer.ReadDouble())); + } } - return result; + return points; } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/TimeSpanTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/TimeSpanTypeConverter.cs index d416b14ac5..1e42a46875 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/TimeSpanTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/TimeSpanTypeConverter.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Globalization; diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index 5bdb0cc235..03fd2e60dd 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -1,18 +1,17 @@ -// 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 System; -using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; +using System; using Avalonia.Controls; using Avalonia.Data; +using Avalonia.Markup.Xaml.Converters; +using Avalonia.Media; + +#nullable enable namespace Avalonia.Markup.Xaml.MarkupExtensions { public class DynamicResourceExtension : IBinding { - private IResourceNode _anchor; + private IStyledElement? _anchor; + private IResourceProvider? _resourceProvider; public DynamicResourceExtension() { @@ -23,31 +22,57 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions ResourceKey = resourceKey; } - public object ResourceKey { get; set; } + public object? ResourceKey { get; set; } public IBinding ProvideValue(IServiceProvider serviceProvider) { var provideTarget = serviceProvider.GetService(); - if (!(provideTarget.TargetObject is IResourceNode)) + if (!(provideTarget.TargetObject is IStyledElement)) { - _anchor = serviceProvider.GetFirstParent(); + _anchor = serviceProvider.GetFirstParent(); + + if (_anchor is null) + { + _resourceProvider = serviceProvider.GetFirstParent(); + } } return this; } - InstancedBinding IBinding.Initiate( + InstancedBinding? IBinding.Initiate( IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation) { - var control = target as IResourceNode ?? _anchor; + if (ResourceKey is null) + { + return null; + } + + var control = target as IStyledElement ?? _anchor as IStyledElement; 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) + { + 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/ReflectionBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs index 470cdbd08f..10770365a3 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs @@ -1,6 +1,3 @@ -// 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; using System; @@ -41,6 +38,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions StringFormat = StringFormat, RelativeSource = RelativeSource, DefaultAnchor = new WeakReference(GetDefaultAnchor(descriptorContext)), + TargetNullValue = TargetNullValue, NameScope = new WeakReference(serviceProvider.GetService()) }; } @@ -52,6 +50,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions // the context. object anchor = context.GetFirstParent(); + if(anchor is null) + { + // Try to find IDataContextProvider, this was added to allow us to find + // a datacontext for Application class when using NativeMenuItems. + anchor = context.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 ?? @@ -79,5 +84,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public string StringFormat { get; set; } public RelativeSource RelativeSource { get; set; } + + public object TargetNullValue { get; set; } = AvaloniaProperty.UnsetValue; } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/RelativeSourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/RelativeSourceExtension.cs index f690a5ff0e..f06644758f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/RelativeSourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/RelativeSourceExtension.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Data; diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs index 3525628a79..0cedf4f364 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs @@ -2,17 +2,18 @@ using System.ComponentModel; using Avalonia.Controls; +#nullable enable + namespace Avalonia.Markup.Xaml.MarkupExtensions { /// /// Loads a resource dictionary from a specified URL. /// - public class ResourceInclude :IResourceProvider + public class ResourceInclude : IResourceProvider { - private Uri _baseUri; - private IResourceDictionary _loaded; - - public event EventHandler ResourcesChanged; + private Uri? _baseUri; + private IResourceDictionary? _loaded; + private bool _isLoading; /// /// Gets the loaded resource dictionary. @@ -23,33 +24,45 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions { if (_loaded == null) { + _isLoading = true; var loader = new AvaloniaXamlLoader(); _loaded = (IResourceDictionary)loader.Load(Source, _baseUri); - - if (_loaded.HasResources) - { - ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); - } + _isLoading = false; } return _loaded; } } + public IResourceHost? Owner => Loaded.Owner; + /// /// Gets or sets the source URL. /// - public Uri Source { get; set; } + public Uri? Source { get; set; } - /// - bool IResourceProvider.HasResources => Loaded.HasResources; + bool IResourceNode.HasResources => Loaded.HasResources; - /// - bool IResourceProvider.TryGetResource(object key, out object value) + public event EventHandler OwnerChanged { - return Loaded.TryGetResource(key, out value); + add => Loaded.OwnerChanged += value; + remove => Loaded.OwnerChanged -= value; } + bool IResourceNode.TryGetResource(object key, out object? value) + { + if (!_isLoading) + { + return Loaded.TryGetResource(key, out value); + } + + value = null; + return false; + } + + void IResourceProvider.AddOwner(IResourceHost owner) => Loaded.AddOwner(owner); + void IResourceProvider.RemoveOwner(IResourceHost owner) => Loaded.RemoveOwner(owner); + public ResourceInclude ProvideValue(IServiceProvider serviceProvider) { var tdc = (ITypeDescriptorContext)serviceProvider; diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index 6d055e033f..4ad68fc63e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -1,12 +1,10 @@ -// 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 System; +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 { @@ -25,33 +23,45 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public object ProvideValue(IServiceProvider serviceProvider) { - // Look upwards though the ambient context for IResourceProviders which might be able - // to give us the resource. - foreach (var resourceProvider in serviceProvider.GetParents()) + var stack = serviceProvider.GetService(); + var provideTarget = serviceProvider.GetService(); + + var targetType = provideTarget.TargetProperty switch { - if (resourceProvider.TryGetResource(ResourceKey, out var value)) + 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. + foreach (var e in stack.Parents) + { + object value; + + 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 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/MarkupExtensions/StyleIncludeExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StyleIncludeExtension.cs index a323050c31..129fa66912 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StyleIncludeExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StyleIncludeExtension.cs @@ -1,6 +1,3 @@ -// 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.Markup.Xaml.Styling; using Avalonia.Styling; using System.ComponentModel; diff --git a/src/Markup/Avalonia.Markup.Xaml/Properties/AssemblyInfo.cs b/src/Markup/Avalonia.Markup.Xaml/Properties/AssemblyInfo.cs index e67e994123..7e4a095bdf 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Properties/AssemblyInfo.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Properties/AssemblyInfo.cs @@ -1,6 +1,3 @@ -// 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 System.Reflection; using Avalonia.Metadata; using System.Runtime.CompilerServices; diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 7acee50d80..ea9042f779 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -1,46 +1,45 @@ -// 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.Styling; using System; using Avalonia.Controls; +using System.Collections.Generic; + +#nullable enable namespace Avalonia.Markup.Xaml.Styling { /// /// Includes a style from a URL. /// - public class StyleInclude : IStyle, ISetStyleParent + public class StyleInclude : IStyle, IResourceProvider { - private Uri _baseUri; - private IStyle _loaded; - private IResourceNode _parent; + private readonly Uri _baseUri; + private IStyle[]? _loaded; + private bool _isLoading; /// /// Initializes a new instance of the class. /// - /// + /// The base URL for the XAML context. public StyleInclude(Uri baseUri) { _baseUri = baseUri; } + /// + /// Initializes a new instance of the class. + /// + /// The XAML service provider. public StyleInclude(IServiceProvider serviceProvider) { _baseUri = serviceProvider.GetContextBaseUri(); } - - /// - public event EventHandler ResourcesChanged - { - add {} - remove {} - } + + public IResourceHost? Owner => (Loaded as IResourceProvider)?.Owner; /// /// Gets or sets the source URL. /// - public Uri Source { get; set; } + public Uri? Source { get; set; } /// /// Gets the loaded style. @@ -51,58 +50,53 @@ namespace Avalonia.Markup.Xaml.Styling { if (_loaded == null) { + _isLoading = true; var loader = new AvaloniaXamlLoader(); - _loaded = (IStyle)loader.Load(Source, _baseUri); - (_loaded as ISetStyleParent)?.SetParent(this); + var loaded = (IStyle)loader.Load(Source, _baseUri); + _loaded = new[] { loaded }; + _isLoading = false; } - return _loaded; + return _loaded?[0]!; } } - /// - bool IResourceProvider.HasResources => Loaded.HasResources; + bool IResourceNode.HasResources => (Loaded as IResourceProvider)?.HasResources ?? false; - /// - IResourceNode IResourceNode.ResourceParent => _parent; + IReadOnlyList IStyle.Children => _loaded ?? Array.Empty(); - /// - public bool Attach(IStyleable control, IStyleHost container) + public event EventHandler OwnerChanged { - if (Source != null) + add { - return Loaded.Attach(control, container); + if (Loaded is IResourceProvider rp) + { + rp.OwnerChanged += value; + } } - - return false; - } - - public void Detach() - { - if (Source != null) + remove { - Loaded.Detach(); + if (Loaded is IResourceProvider rp) + { + rp.OwnerChanged -= value; + } } } - /// - public bool TryGetResource(object key, out object value) => Loaded.TryGetResource(key, out value); + public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host); - /// - void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) + public bool TryGetResource(object key, out object? value) { - (Loaded as ISetStyleParent)?.NotifyResourcesChanged(e); - } - - /// - void ISetStyleParent.SetParent(IResourceNode parent) - { - if (_parent != null && parent != null) + if (!_isLoading && Loaded is IResourceProvider p) { - throw new InvalidOperationException("The Style already has a parent."); + return p.TryGetResource(key, out value); } - _parent = parent; + value = null; + return false; } + + void IResourceProvider.AddOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.AddOwner(owner); + void IResourceProvider.RemoveOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.RemoveOwner(owner); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs index f46ee8787b..30c7c75992 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Controls; using Avalonia.Controls.Templates; diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index c81a3718a7..5663d08412 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs @@ -1,8 +1,4 @@ -// 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 System; -using System.Reflection; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Metadata; @@ -28,7 +24,7 @@ namespace Avalonia.Markup.Xaml.Templates } else { - return DataType.GetTypeInfo().IsAssignableFrom(data.GetType().GetTypeInfo()); + return DataType.IsInstanceOfType(data); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/FocusAdornerTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/FocusAdornerTemplate.cs index 48309570cf..23e01f699f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/FocusAdornerTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/FocusAdornerTemplate.cs @@ -1,6 +1,3 @@ -// 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.Controls; using Avalonia.Metadata; using Avalonia.Styling; diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs index 0dd4e57a7f..c8843a3176 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs @@ -1,6 +1,3 @@ -// 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.Controls; using Avalonia.Metadata; using Avalonia.Styling; diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs index 5f36cfe0ac..65323ae665 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs @@ -1,6 +1,3 @@ -// 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.Controls; using Avalonia.Metadata; using Avalonia.Styling; diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs index 6a30c3861a..96f25668fb 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Controls; using System.Collections.Generic; diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index 0e2a131afd..b96486235a 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -1,13 +1,7 @@ -// 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 System; -using System.Reflection; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Data; -using Avalonia.Data.Core; -using Avalonia.Markup.Data; using Avalonia.Markup.Parsers; using Avalonia.Metadata; @@ -34,7 +28,7 @@ namespace Avalonia.Markup.Xaml.Templates } else { - return DataType.GetTypeInfo().IsAssignableFrom(data.GetType().GetTypeInfo()); + return DataType.IsInstanceOfType(data); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs index 4ee2be9ee0..949e2173a5 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs @@ -114,7 +114,6 @@ 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")); @@ -123,7 +122,7 @@ namespace Avalonia.Markup.Xaml.XamlIl _sreMappings, _sreXmlns, AvaloniaXamlIlLanguage.CustomValueConverter, new XamlIlClrPropertyInfoEmitter(_sreTypeSystem.CreateTypeBuilder(clrPropertyBuilder)), new XamlIlPropertyInfoAccessorFactoryEmitter(_sreTypeSystem.CreateTypeBuilder(indexerClosureType))), - _sreContextType); + _sreContextType) { EnableIlVerification = true }; IXamlIlType overrideType = null; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs index f9540b2fa1..c3f4c479dc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -99,6 +99,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions void Add(string type, string conv) => AddType(typeSystem.GetType(type), typeSystem.GetType(conv)); + Add("Avalonia.Media.IImage","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter"); Add("Avalonia.Media.Imaging.IBitmap","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter"); var ilist = typeSystem.GetType("System.Collections.Generic.IList`1"); AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")), 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 aab43bbd6f..40386924c3 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs @@ -25,7 +25,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers if ((tt?.Values.FirstOrDefault() is XamlIlTypeExtensionNode tn)) { - targetType = tn.Type; + targetType = tn.Value; } else { 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 aac07f5b6e..d5114244cf 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs @@ -104,6 +104,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } } + if (results != null && result != null) + { + results.Add(result); + } + return results ?? result; } @@ -158,9 +163,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers protected void EmitCall(XamlIlEmitContext 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 && - m.Parameters[0].FullName == "Avalonia.Styling.Selector" - && method(m)); + var found = selectors.FindMethod(m => m.IsStatic && m.Parameters.Count > 0 && method(m)); codeGen.EmitCall(found); } } @@ -308,8 +311,35 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers _selectors.Add(node); } - //TODO: actually find the type - public override IXamlIlType TargetType => _selectors.FirstOrDefault()?.TargetType; + public override IXamlIlType TargetType + { + get + { + IXamlIlType result = null; + + foreach (var selector in _selectors) + { + if (selector.TargetType == null) + { + return null; + } + else if (result == null) + { + result = selector.TargetType; + } + else + { + while (!result.IsAssignableFrom(selector.TargetType)) + { + result = result.BaseType; + } + } + } + + return result; + } + } + protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen) { if (_selectors.Count == 0) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index c7155c5f6c..0681622454 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit c7155c5f6c1a5153ee2d8cd78e5d1524dd6744cf +Subproject commit 068162245473ec39ee36da12150e928072b96403 diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 2882114936..bf43730481 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Linq; using System.Reactive; diff --git a/src/Markup/Avalonia.Markup/Data/BindingBase.cs b/src/Markup/Avalonia.Markup/Data/BindingBase.cs index d59c8e3fac..7c4e7b5efe 100644 --- a/src/Markup/Avalonia.Markup/Data/BindingBase.cs +++ b/src/Markup/Avalonia.Markup/Data/BindingBase.cs @@ -21,6 +21,8 @@ namespace Avalonia.Data /// public BindingBase() { + FallbackValue = AvaloniaProperty.UnsetValue; + TargetNullValue = AvaloniaProperty.UnsetValue; } /// @@ -28,8 +30,8 @@ namespace Avalonia.Data /// /// The binding mode. public BindingBase(BindingMode mode = BindingMode.Default) + :this() { - FallbackValue = AvaloniaProperty.UnsetValue; Mode = mode; } @@ -48,6 +50,11 @@ namespace Avalonia.Data /// 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. /// @@ -114,6 +121,7 @@ namespace Avalonia.Data observer, targetType, fallback, + TargetNullValue, converter ?? DefaultValueConverter.Instance, ConverterParameter, Priority); diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 29945e25c3..7aa1eed890 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Globalization; @@ -37,6 +34,11 @@ namespace Avalonia.Data /// 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. /// @@ -57,6 +59,12 @@ namespace Avalonia.Data /// public string StringFormat { get; set; } + public MultiBinding() + { + FallbackValue = AvaloniaProperty.UnsetValue; + TargetNullValue = AvaloniaProperty.UnsetValue; + } + /// public InstancedBinding Initiate( IAvaloniaObject target, @@ -99,9 +107,22 @@ 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); + if (converted == null) + { + converted = TargetNullValue; + } + if (converted == AvaloniaProperty.UnsetValue) { converted = FallbackValue; diff --git a/src/Markup/Avalonia.Markup/Data/RelativeSource.cs b/src/Markup/Avalonia.Markup/Data/RelativeSource.cs index ac974e3bda..e3d2dd4aaa 100644 --- a/src/Markup/Avalonia.Markup/Data/RelativeSource.cs +++ b/src/Markup/Avalonia.Markup/Data/RelativeSource.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Data diff --git a/src/Markup/Avalonia.Markup/Markup/Data/DelayedBinding.cs b/src/Markup/Avalonia.Markup/Markup/Data/DelayedBinding.cs index f7d228609d..0b0ed7b06a 100644 --- a/src/Markup/Avalonia.Markup/Markup/Data/DelayedBinding.cs +++ b/src/Markup/Avalonia.Markup/Markup/Data/DelayedBinding.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Reflection; @@ -150,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 4d88b34659..609ae5a57e 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs @@ -1,6 +1,3 @@ -// 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 System.Collections.Generic; using Avalonia.Data.Core; using Avalonia.Utilities; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index 0eb28c2aa6..1048148c1f 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -1,6 +1,3 @@ -// 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.Markup.Parsers.Nodes; using Avalonia.Utilities; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs index a11879238b..76fbc9a982 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections; using System.Collections.Generic; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index 2de66d5501..2e9ec178a5 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Linq; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs index 493579d676..04519bf2bb 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Globalization; diff --git a/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs b/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs index 4651dbab8d..72c1659a1e 100644 --- a/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs +++ b/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs @@ -1,6 +1,3 @@ -// 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 System.Reflection; using Avalonia.Metadata; using System.Runtime.CompilerServices; diff --git a/src/Shared/PlatformSupport/AssetLoader.cs b/src/Shared/PlatformSupport/AssetLoader.cs index dd72934560..90b341297c 100644 --- a/src/Shared/PlatformSupport/AssetLoader.cs +++ b/src/Shared/PlatformSupport/AssetLoader.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.IO; diff --git a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs index 8a5a725594..03832d3063 100644 --- a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs +++ b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Reflection; @@ -75,7 +72,7 @@ namespace Avalonia.Shared.PlatformSupport lock (_btlock) Backtraces.Remove(_backtrace); #endif - _plat.Free(_address, Size); + _plat?.Free(_address, Size); GC.RemoveMemoryPressure(Size); IsDisposed = true; _address = IntPtr.Zero; diff --git a/src/Shared/RenderHelpers/ArcToHelper.cs b/src/Shared/RenderHelpers/ArcToHelper.cs index 8445da8dd5..0bbf451970 100644 --- a/src/Shared/RenderHelpers/ArcToHelper.cs +++ b/src/Shared/RenderHelpers/ArcToHelper.cs @@ -45,6 +45,8 @@ // Commented out some unused values calculations. // These are not supposed to be removed from the source code, // as these may be helpful for debugging. +// +// Adapted from http://www.spaceroots.org/documents/ellipse/EllipticalArc.java using System; using Avalonia.Media; diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj index 4f884cdf33..68da513528 100644 --- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj +++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj @@ -12,5 +12,6 @@ + diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 47e651ce91..ae756f4eab 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Diagnostics; @@ -31,8 +28,13 @@ namespace Avalonia.Skia private double _currentOpacity = 1.0f; private readonly bool _canTextUseLcdRendering; private Matrix _currentTransform; - private GRContext _grContext; private bool _disposed; + private GRContext _grContext; + public GRContext GrContext => _grContext; + private readonly SKPaint _strokePaint = new SKPaint(); + private readonly SKPaint _fillPaint = new SKPaint(); + private readonly SKPaint _boxShadowPaint = new SKPaint(); + /// /// Context create info. /// @@ -110,7 +112,7 @@ namespace Avalonia.Skia } /// - public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { var drawableImage = (IDrawableBitmapImpl)source.Item; var s = sourceRect.ToSKRect(); @@ -122,41 +124,24 @@ namespace Avalonia.Skia Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity)) }) { - paint.FilterQuality = GetInterpolationMode(bitmapInterpolationMode); + paint.FilterQuality = bitmapInterpolationMode.ToSKFilterQuality(); drawableImage.Draw(this, s, d, paint); } } - private static SKFilterQuality GetInterpolationMode(BitmapInterpolationMode interpolationMode) - { - switch (interpolationMode) - { - case BitmapInterpolationMode.LowQuality: - return SKFilterQuality.Low; - case BitmapInterpolationMode.MediumQuality: - return SKFilterQuality.Medium; - case BitmapInterpolationMode.HighQuality: - return SKFilterQuality.High; - case BitmapInterpolationMode.Default: - return SKFilterQuality.None; - default: - throw new ArgumentOutOfRangeException(nameof(interpolationMode), interpolationMode, null); - } - } - /// - public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { PushOpacityMask(opacityMask, opacityMaskRect); - DrawImage(source, 1, new Rect(0, 0, source.Item.PixelSize.Width, source.Item.PixelSize.Height), destRect, BitmapInterpolationMode.Default); + DrawBitmap(source, 1, new Rect(0, 0, source.Item.PixelSize.Width, source.Item.PixelSize.Height), destRect, BitmapInterpolationMode.Default); PopOpacityMask(); } /// public void DrawLine(IPen pen, Point p1, Point p2) { - using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) + using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) { Canvas.DrawLine((float) p1.X, (float) p1.Y, (float) p2.X, (float) p2.Y, paint.Paint); } @@ -168,8 +153,8 @@ namespace Avalonia.Skia var impl = (GeometryImpl) geometry; var size = geometry.Bounds.Size; - using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper)) - using (var stroke = pen?.Brush != null ? CreatePaint(pen, size) : default(PaintWrapper)) + using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper)) + using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, size) : default(PaintWrapper)) { if (fill.Paint != null) { @@ -183,38 +168,186 @@ namespace Avalonia.Skia } } - /// - public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0) + struct BoxShadowFilter : IDisposable { - using (var paint = CreatePaint(pen, rect.Size)) + public SKPaint Paint; + private SKImageFilter _filter; + public SKClipOperation ClipOperation; + + static float SkBlurRadiusToSigma(double radius) { + if (radius <= 0) + return 0.0f; + return 0.288675f * (float)radius + 0.5f; + } + public static BoxShadowFilter Create(SKPaint paint, BoxShadow shadow, double opacity) { - var rc = rect.ToSKRect(); + var ac = shadow.Color; - if (Math.Abs(cornerRadius) < float.Epsilon) - { - Canvas.DrawRect(rc, paint.Paint); - } - else + SKImageFilter filter = null; + filter = SKImageFilter.CreateBlur(SkBlurRadiusToSigma(shadow.Blur), SkBlurRadiusToSigma(shadow.Blur)); + var color = new SKColor(ac.R, ac.G, ac.B, (byte)(ac.A * opacity)); + + paint.Reset(); + paint.IsAntialias = true; + paint.Color = color; + paint.ImageFilter = filter; + + return new BoxShadowFilter { - Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); - } + Paint = paint, _filter = filter, + ClipOperation = shadow.IsInset ? SKClipOperation.Intersect : SKClipOperation.Difference + }; } + + public void Dispose() + { + Paint.Reset(); + Paint = null; + _filter?.Dispose(); + } + } + + SKRect AreaCastingShadowInHole( + SKRect hole_rect, + float shadow_blur, + float shadow_spread, + float offsetX, float offsetY) + { + // Adapted from Chromium + var bounds = hole_rect; + + bounds.Inflate(shadow_blur, shadow_blur); + + if (shadow_spread < 0) + bounds.Inflate(-shadow_spread, -shadow_spread); + + var offset_bounds = bounds; + offset_bounds.Offset(-offsetX, -offsetY); + bounds.Union(offset_bounds); + return bounds; } + /// - public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0) + public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows = default) { - using (var paint = CreatePaint(brush, rect.Size)) + if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0) + return; + // Arbitrary chosen values + // On OSX Skia breaks OpenGL context when asked to draw, e. g. (0, 0, 623, 6666600) rect + if (rect.Rect.Height > 8192 || rect.Rect.Width > 8192) + boxShadows = default; + + var rc = rect.Rect.ToSKRect(); + var isRounded = rect.IsRounded; + var needRoundRect = rect.IsRounded || (boxShadows.HasInsetShadows); + 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(), + }); + + foreach (var boxShadow in boxShadows) { - var rc = rect.ToSKRect(); + if (!boxShadow.IsEmpty && !boxShadow.IsInset) + { + using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity)) + { + var spread = (float)boxShadow.Spread; + if (boxShadow.IsInset) + spread = -spread; + + Canvas.Save(); + if (isRounded) + { + using var shadowRect = new SKRoundRect(skRoundRect); + if (spread != 0) + shadowRect.Inflate(spread, spread); + Canvas.ClipRoundRect(skRoundRect, + shadow.ClipOperation, true); + + var oldTransform = Transform; + Transform = oldTransform * Matrix.CreateTranslation(boxShadow.OffsetX, boxShadow.OffsetY); + Canvas.DrawRoundRect(shadowRect, shadow.Paint); + Transform = oldTransform; + } + else + { + var shadowRect = rc; + if (spread != 0) + shadowRect.Inflate(spread, spread); + Canvas.ClipRect(rc, shadow.ClipOperation); + var oldTransform = Transform; + Transform = oldTransform * Matrix.CreateTranslation(boxShadow.OffsetX, boxShadow.OffsetY); + Canvas.DrawRect(shadowRect, shadow.Paint); + Transform = oldTransform; + } + + Canvas.Restore(); + } + } + } - if (Math.Abs(cornerRadius) < float.Epsilon) + if (brush != null) + { + using (var paint = CreatePaint(_fillPaint, brush, rect.Rect.Size)) { - Canvas.DrawRect(rc, paint.Paint); + if (isRounded) + { + Canvas.DrawRoundRect(skRoundRect, paint.Paint); + } + else + { + Canvas.DrawRect(rc, paint.Paint); + } + } - else + } + + foreach (var boxShadow in boxShadows) + { + if (!boxShadow.IsEmpty && boxShadow.IsInset) + { + using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity)) + { + var spread = (float)boxShadow.Spread; + var offsetX = (float)boxShadow.OffsetX; + var offsetY = (float)boxShadow.OffsetY; + var outerRect = AreaCastingShadowInHole(rc, (float)boxShadow.Blur, spread, offsetX, offsetY); + + Canvas.Save(); + using var shadowRect = new SKRoundRect(skRoundRect); + if (spread != 0) + shadowRect.Deflate(spread, spread); + Canvas.ClipRoundRect(skRoundRect, + shadow.ClipOperation, true); + + var oldTransform = Transform; + Transform = oldTransform * Matrix.CreateTranslation(boxShadow.OffsetX, boxShadow.OffsetY); + using (var outerRRect = new SKRoundRect(outerRect)) + Canvas.DrawRoundRectDifference(outerRRect, shadowRect, shadow.Paint); + Transform = oldTransform; + Canvas.Restore(); + } + } + } + + if (pen?.Brush != null) + { + using (var paint = CreatePaint(_strokePaint, pen, rect.Rect.Size)) { - Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); + if (isRounded) + { + Canvas.DrawRoundRect(skRoundRect, paint.Paint); + } + else + { + Canvas.DrawRect(rc, paint.Paint); + } + } } } @@ -222,13 +355,27 @@ namespace Avalonia.Skia /// public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) { - using (var paint = CreatePaint(foreground, text.Bounds.Size)) + using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds.Size)) { var textImpl = (FormattedTextImpl) text; textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering); } } + /// + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + { + using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Bounds.Size)) + { + var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; + + ConfigureTextRendering(paintWrapper); + + Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X, + (float)baselineOrigin.Y, paintWrapper.Paint); + } + } + /// public IRenderTargetBitmapImpl CreateLayer(Size size) { @@ -242,6 +389,12 @@ namespace Avalonia.Skia Canvas.ClipRect(clip.ToSKRect()); } + public void PushClip(RoundedRect clip) + { + Canvas.Save(); + Canvas.ClipRoundRect(clip.ToSKRoundRect()); + } + /// public void PopClip() { @@ -309,7 +462,7 @@ namespace Avalonia.Skia var paint = new SKPaint(); Canvas.SaveLayer(paint); - _maskStack.Push(CreatePaint(mask, bounds.Size)); + _maskStack.Push(CreatePaint(paint, mask, bounds.Size, true)); } /// @@ -350,6 +503,15 @@ namespace Avalonia.Skia } } + internal void ConfigureTextRendering(PaintWrapper wrapper) + { + var paint = wrapper.Paint; + + paint.IsEmbeddedBitmapText = true; + paint.SubpixelText = true; + paint.LcdRenderText = _canTextUseLcdRendering; + } + /// /// Configure paint wrapper for using gradient brush. /// @@ -420,7 +582,7 @@ namespace Avalonia.Skia context.Clear(Colors.Transparent); context.PushClip(calc.IntermediateClip); context.Transform = calc.IntermediateTransform; - context.DrawImage( + context.DrawBitmap( RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, sourceRect, @@ -500,17 +662,16 @@ namespace Avalonia.Skia /// /// Creates paint wrapper for given brush. /// + /// The paint to wrap. /// Source brush. /// Target size. + /// Optional dispose of the supplied paint. /// Paint wrapper for given brush. - internal PaintWrapper CreatePaint(IBrush brush, Size targetSize) + internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Size targetSize, bool disposePaint = false) { - var paint = new SKPaint - { - IsAntialias = true - }; + var paintWrapper = new PaintWrapper(paint, disposePaint); - var paintWrapper = new PaintWrapper(paint); + paint.IsAntialias = true; double opacity = brush.Opacity * _currentOpacity; @@ -558,10 +719,12 @@ namespace Avalonia.Skia /// /// Creates paint wrapper for given pen. /// + /// The paint to wrap. /// Source pen. /// Target size. + /// Optional dispose of the supplied paint. /// - private PaintWrapper CreatePaint(IPen pen, Size targetSize) + private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Size targetSize, bool disposePaint = false) { // In Skia 0 thickness means - use hairline rendering // and for us it means - there is nothing rendered. @@ -570,8 +733,7 @@ namespace Avalonia.Skia return default; } - var rv = CreatePaint(pen.Brush, targetSize); - var paint = rv.Paint; + var rv = CreatePaint(paint, pen.Brush, targetSize, disposePaint); paint.IsStroke = true; paint.StrokeWidth = (float) pen.Thickness; @@ -654,7 +816,7 @@ namespace Avalonia.Skia /// /// Skia cached paint state. /// - private struct PaintState : IDisposable + private readonly struct PaintState : IDisposable { private readonly SKColor _color; private readonly SKShader _shader; @@ -682,14 +844,16 @@ namespace Avalonia.Skia { //We are saving memory allocations there public readonly SKPaint Paint; + private readonly bool _disposePaint; private IDisposable _disposable1; private IDisposable _disposable2; private IDisposable _disposable3; - public PaintWrapper(SKPaint paint) + public PaintWrapper(SKPaint paint, bool disposePaint) { Paint = paint; + _disposePaint = disposePaint; _disposable1 = null; _disposable2 = null; @@ -737,7 +901,15 @@ namespace Avalonia.Skia /// public void Dispose() { - Paint?.Dispose(); + if (_disposePaint) + { + Paint?.Dispose(); + } + else + { + Paint?.Reset(); + } + _disposable1?.Dispose(); _disposable2?.Dispose(); _disposable3?.Dispose(); diff --git a/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs b/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs index aae1dd8cef..ac05691c67 100644 --- a/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs @@ -1,6 +1,3 @@ -// 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 SkiaSharp; namespace Avalonia.Skia diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs new file mode 100644 index 0000000000..415a89e1c1 --- /dev/null +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Media; +using Avalonia.Media.Fonts; +using Avalonia.Platform; +using SkiaSharp; + +namespace Avalonia.Skia +{ + internal class FontManagerImpl : IFontManagerImpl + { + private SKFontManager _skFontManager = SKFontManager.Default; + + public string GetDefaultFontFamilyName() + { + return SKTypeface.Default.FamilyName; + } + + public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + { + if (checkForUpdates) + { + _skFontManager = SKFontManager.CreateDefault(); + } + + return _skFontManager.FontFamilies; + } + + [ThreadStatic] private static string[] t_languageTagBuffer; + + public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) + { + SKFontStyle skFontStyle; + + switch (fontWeight) + { + case FontWeight.Normal when fontStyle == FontStyle.Normal: + skFontStyle = SKFontStyle.Normal; + break; + case FontWeight.Normal when fontStyle == FontStyle.Italic: + skFontStyle = SKFontStyle.Italic; + break; + case FontWeight.Bold when fontStyle == FontStyle.Normal: + skFontStyle = SKFontStyle.Bold; + break; + case FontWeight.Bold when fontStyle == FontStyle.Italic: + skFontStyle = SKFontStyle.BoldItalic; + break; + default: + skFontStyle = new SKFontStyle((SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle); + break; + } + + if (culture == null) + { + culture = CultureInfo.CurrentUICulture; + } + + if (t_languageTagBuffer == null) + { + t_languageTagBuffer = new string[2]; + } + + t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName; + t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName; + + if (fontFamily != null && fontFamily.FamilyNames.HasFallbacks) + { + var familyNames = fontFamily.FamilyNames; + + for (var i = 1; i < familyNames.Count; i++) + { + var skTypeface = + _skFontManager.MatchCharacter(familyNames[i], skFontStyle, t_languageTagBuffer, codepoint); + + if (skTypeface == null) + { + continue; + } + + fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle); + + return true; + } + } + else + { + var skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint); + + if (skTypeface != null) + { + fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle); + + return true; + } + } + + fontKey = default; + + return false; + } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + SKTypeface skTypeface = null; + + if (typeface.FontFamily.Key == null) + { + var defaultName = SKTypeface.Default.FamilyName; + + foreach (var familyName in typeface.FontFamily.FamilyNames) + { + skTypeface = SKTypeface.FromFamilyName(familyName, (SKFontStyleWeight)typeface.Weight, + SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style); + + if (!skTypeface.FamilyName.Equals(familyName, StringComparison.Ordinal) && + defaultName.Equals(skTypeface.FamilyName, StringComparison.Ordinal)) + { + continue; + } + + break; + } + } + else + { + var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); + + skTypeface = fontCollection.Get(typeface); + } + + if (skTypeface == null) + { + throw new InvalidOperationException( + $"Could not create glyph typeface for: {typeface.FontFamily.Name}."); + } + + return new GlyphTypefaceImpl(skTypeface); + } + } +} diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index eb7b65cdce..5f876464e2 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.Linq; @@ -18,6 +15,7 @@ namespace Avalonia.Skia public FormattedTextImpl( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, @@ -28,47 +26,22 @@ namespace Avalonia.Skia // Replace 0 characters with zero-width spaces (200B) Text = Text.Replace((char)0, (char)0x200B); - SKTypeface skiaTypeface = null; + var glyphTypeface = (GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl; - if (typeface.FontFamily.Key != null) + _paint = new SKPaint { - var typefaces = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); - skiaTypeface = typefaces.GetTypeFace(typeface); - } - else - { - if (typeface.FontFamily.FamilyNames.HasFallbacks) - { - foreach (var familyName in typeface.FontFamily.FamilyNames) - { - skiaTypeface = TypefaceCache.GetTypeface( - familyName, - typeface.Style, - typeface.Weight); - if (skiaTypeface.FamilyName != TypefaceCache.DefaultFamilyName) break; - } - } - else - { - skiaTypeface = TypefaceCache.GetTypeface( - typeface.FontFamily.Name, - typeface.Style, - typeface.Weight); - } - } - - _paint = new SKPaint(); + TextEncoding = SKTextEncoding.Utf16, + IsStroke = false, + IsAntialias = true, + LcdRenderText = true, + SubpixelText = true, + Typeface = glyphTypeface.Typeface, + TextSize = (float)fontSize, + TextAlign = textAlignment.ToSKTextAlign() + }; //currently Skia does not measure properly with Utf8 !!! //Paint.TextEncoding = SKTextEncoding.Utf8; - _paint.TextEncoding = SKTextEncoding.Utf16; - _paint.IsStroke = false; - _paint.IsAntialias = true; - _paint.LcdRenderText = true; - _paint.SubpixelText = true; - _paint.Typeface = skiaTypeface; - _paint.TextSize = (float)typeface.FontSize; - _paint.TextAlign = textAlignment.ToSKTextAlign(); _wrapping = wrapping; _constraint = constraint; @@ -99,7 +72,24 @@ namespace Avalonia.Skia public TextHitTestResult HitTestPoint(Point point) { float y = (float)point.Y; - var line = _skiaLines.Find(l => l.Top <= y && (l.Top + l.Height) > y); + + AvaloniaFormattedTextLine line = default; + + float nextTop = 0; + + foreach(var currentLine in _skiaLines) + { + if(currentLine.Top <= y) + { + line = currentLine; + nextTop = currentLine.Top + currentLine.Height; + } + else + { + nextTop = currentLine.Top; + break; + } + } if (!line.Equals(default(AvaloniaFormattedTextLine))) { @@ -127,12 +117,15 @@ namespace Avalonia.Skia line.Length : (line.Length - 1); } - return new TextHitTestResult + if (y < nextTop) { - IsInside = false, - TextPosition = line.Start + offset, - IsTrailing = Text.Length == (line.Start + offset + 1) - }; + return new TextHitTestResult + { + IsInside = false, + TextPosition = line.Start + offset, + IsTrailing = Text.Length == (line.Start + offset + 1) + }; + } } bool end = point.X > _bounds.Width || point.Y > _lines.Sum(l => l.Height); @@ -147,25 +140,27 @@ namespace Avalonia.Skia public Rect HitTestTextPosition(int index) { + if (string.IsNullOrEmpty(Text)) + { + var alignmentOffset = TransformX(0, 0, _paint.TextAlign); + return new Rect(alignmentOffset, 0, 0, _lineHeight); + } var rects = GetRects(); - - if (index < 0 || index >= rects.Count) + if (index >= Text.Length || index < 0) { var r = rects.LastOrDefault(); - return new Rect(r.X + r.Width, r.Y, 0, _lineHeight); - } - if (rects.Count == 0) - { - return new Rect(0, 0, 1, _lineHeight); - } + var c = Text[Text.Length - 1]; - if (index == rects.Count) - { - var lr = rects[rects.Count - 1]; - return new Rect(new Point(lr.X + lr.Width, lr.Y), rects[index - 1].Size); + switch (c) + { + case '\n': + case '\r': + return new Rect(r.X, r.Y, 0, _lineHeight); + default: + return new Rect(r.X + r.Width, r.Y, 0, _lineHeight); + } } - return rects[index]; } @@ -281,7 +276,8 @@ namespace Avalonia.Skia if (fb != null) { //TODO: figure out how to get the brush size - currentWrapper = context.CreatePaint(fb, new Size()); + currentWrapper = context.CreatePaint(new SKPaint { IsAntialias = true }, fb, + new Size()); } else { @@ -646,6 +642,11 @@ namespace Avalonia.Skia var lastLine = _skiaLines[_skiaLines.Count - 1]; _bounds = new Rect(0, 0, maxX, lastLine.Top + lastLine.Height); + if (double.IsPositiveInfinity(Constraint.Width)) + { + return; + } + switch (_paint.TextAlign) { case SKTextAlign.Center: diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index 1af3d2968c..8b04676b09 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.Reactive.Disposables; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Platform; diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index 23980fb913..879b18742e 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Media; using Avalonia.Platform; @@ -98,7 +95,7 @@ namespace Avalonia.Skia UpdatePathCache(strokeWidth); } - return _pathCache.CachedGeometryRenderBounds.Inflate(strokeWidth / 2.0); + return _pathCache.CachedGeometryRenderBounds; } /// diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs new file mode 100644 index 0000000000..0fdea5ed40 --- /dev/null +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -0,0 +1,25 @@ +using System; +using Avalonia.Platform; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + public class GlyphRunImpl : IGlyphRunImpl + { + public GlyphRunImpl(SKTextBlob textBlob) + { + TextBlob = textBlob; + } + + /// + /// Gets the text blob to draw. + /// + public SKTextBlob TextBlob { get; } + + void IDisposable.Dispose() + { + TextBlob.Dispose(); + } + } +} diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs new file mode 100644 index 0000000000..5571bd890d --- /dev/null +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -0,0 +1,180 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Media; +using Avalonia.Platform; +using HarfBuzzSharp; +using SkiaSharp; + +namespace Avalonia.Skia +{ + public class GlyphTypefaceImpl : IGlyphTypefaceImpl + { + private bool _isDisposed; + + public GlyphTypefaceImpl(SKTypeface typeface) + { + Typeface = typeface ?? throw new ArgumentNullException(nameof(typeface)); + + Face = new Face(GetTable) + { + UnitsPerEm = Typeface.UnitsPerEm + }; + + Font = new Font(Face); + + Font.SetFunctionsOpenType(); + + Font.GetScale(out var xScale, out _); + + DesignEmHeight = (short)xScale; + + if (!Font.TryGetHorizontalFontExtents(out var fontExtents)) + { + Font.TryGetVerticalFontExtents(out fontExtents); + } + + Ascent = -fontExtents.Ascender; + + Descent = -fontExtents.Descender; + + LineGap = fontExtents.LineGap; + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlinePosition)) + { + UnderlinePosition = underlinePosition; + } + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineThickness)) + { + UnderlineThickness = underlineThickness; + } + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutOffset, out var strikethroughPosition)) + { + StrikethroughPosition = strikethroughPosition; + } + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughThickness)) + { + StrikethroughThickness = strikethroughThickness; + } + + IsFixedPitch = Typeface.IsFixedPitch; + } + + public Face Face { get; } + + public Font Font { get; } + + public SKTypeface Typeface { get; } + + /// + public short DesignEmHeight { get; } + + /// + public int Ascent { get; } + + /// + public int Descent { get; } + + /// + public int LineGap { get; } + + /// + public int UnderlinePosition { get; } + + /// + public int UnderlineThickness { get; } + + /// + public int StrikethroughPosition { get; } + + /// + public int StrikethroughThickness { get; } + + /// + public bool IsFixedPitch { get; } + + /// + public ushort GetGlyph(uint codepoint) + { + if (Font.TryGetGlyph(codepoint, out var glyph)) + { + return (ushort)glyph; + } + + return 0; + } + + /// + public ushort[] GetGlyphs(ReadOnlySpan codepoints) + { + var glyphs = new ushort[codepoints.Length]; + + for (var i = 0; i < codepoints.Length; i++) + { + if (Font.TryGetGlyph(codepoints[i], out var glyph)) + { + glyphs[i] = (ushort)glyph; + } + } + + return glyphs; + } + + /// + public int GetGlyphAdvance(ushort glyph) + { + return Font.GetHorizontalGlyphAdvance(glyph); + } + + /// + public int[] GetGlyphAdvances(ReadOnlySpan glyphs) + { + var glyphIndices = new uint[glyphs.Length]; + + for (var i = 0; i < glyphs.Length; i++) + { + glyphIndices[i] = glyphs[i]; + } + + return Font.GetHorizontalGlyphAdvances(glyphIndices); + } + + private Blob GetTable(Face face, Tag tag) + { + var size = Typeface.GetTableSize(tag); + + var data = Marshal.AllocCoTaskMem(size); + + var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeCoTaskMem(data)); + + return Typeface.TryGetTableData(tag, 0, size, data) ? + new Blob(data, size, MemoryMode.ReadOnly, releaseDelegate) : null; + } + + private void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + + if (!disposing) + { + return; + } + + Font?.Dispose(); + Face?.Dispose(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Skia/Avalonia.Skia/ICustomSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs similarity index 51% rename from src/Skia/Avalonia.Skia/ICustomSkiaGpu.cs rename to src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs index 751dd3c1e7..987e2c089c 100644 --- a/src/Skia/Avalonia.Skia/ICustomSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs @@ -1,7 +1,5 @@ -// 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 System.Collections.Generic; +using Avalonia.OpenGL.Imaging; using SkiaSharp; namespace Avalonia.Skia @@ -9,18 +7,18 @@ namespace Avalonia.Skia /// /// Custom Skia gpu instance. /// - public interface ICustomSkiaGpu + public interface ISkiaGpu { - /// - /// Skia GrContext used. - /// - GRContext GrContext { get; } - /// /// Attempts to create custom render target from given surfaces. /// /// Surfaces. /// Created render target or if it fails. - ICustomSkiaRenderTarget TryCreateRenderTarget(IEnumerable surfaces); + ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable surfaces); + } + + public interface IOpenGlAwareSkiaGpu : ISkiaGpu + { + IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap(); } } diff --git a/src/Skia/Avalonia.Skia/ICustomSkiaRenderSession.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpuRenderSession.cs similarity index 69% rename from src/Skia/Avalonia.Skia/ICustomSkiaRenderSession.cs rename to src/Skia/Avalonia.Skia/Gpu/ISkiaGpuRenderSession.cs index 6a4591921e..c54d1bd859 100644 --- a/src/Skia/Avalonia.Skia/ICustomSkiaRenderSession.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpuRenderSession.cs @@ -1,6 +1,3 @@ -// 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 System; using SkiaSharp; @@ -9,7 +6,7 @@ namespace Avalonia.Skia /// /// Custom render session for Skia render target. /// - public interface ICustomSkiaRenderSession : IDisposable + public interface ISkiaGpuRenderSession : IDisposable { /// /// GrContext used by this session. diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpuRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpuRenderTarget.cs new file mode 100644 index 0000000000..bd5b170a68 --- /dev/null +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpuRenderTarget.cs @@ -0,0 +1,19 @@ +using System; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Custom Skia render target. + /// + public interface ISkiaGpuRenderTarget : IDisposable + { + /// + /// Start rendering to this render target. + /// + /// + ISkiaGpuRenderSession BeginRenderingSession(); + + bool IsCorrupted { get; } + } +} diff --git a/src/Skia/Avalonia.Skia/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs similarity index 53% rename from src/Skia/Avalonia.Skia/GlRenderTarget.cs rename to src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index 61ccf09e52..9b73174006 100644 --- a/src/Skia/Avalonia.Skia/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -8,7 +8,7 @@ using static Avalonia.OpenGL.GlConsts; namespace Avalonia.Skia { - internal class GlRenderTarget : IRenderTargetWithCorruptionInfo + internal class GlRenderTarget : ISkiaGpuRenderTarget { private readonly GRContext _grContext; private IGlPlatformSurfaceRenderTarget _surface; @@ -22,22 +22,52 @@ namespace Avalonia.Skia public void Dispose() => _surface.Dispose(); public bool IsCorrupted => (_surface as IGlPlatformSurfaceRenderTargetWithCorruptionInfo)?.IsCorrupted == true; - - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + + class GlGpuSession : ISkiaGpuRenderSession + { + private readonly GRBackendRenderTarget _backendRenderTarget; + private readonly SKSurface _surface; + private readonly IGlPlatformSurfaceRenderingSession _glSession; + + public GlGpuSession(GRContext grContext, + GRBackendRenderTarget backendRenderTarget, + SKSurface surface, + IGlPlatformSurfaceRenderingSession glSession) + { + GrContext = grContext; + _backendRenderTarget = backendRenderTarget; + _surface = surface; + _glSession = glSession; + } + public void Dispose() + { + _surface.Canvas.Flush(); + _surface.Dispose(); + _backendRenderTarget.Dispose(); + GrContext.Flush(); + _glSession.Dispose(); + } + + public GRContext GrContext { get; } + public SKCanvas Canvas => _surface.Canvas; + public double ScaleFactor => _glSession.Scaling; + } + + public ISkiaGpuRenderSession BeginRenderingSession() { - var session = _surface.BeginDraw(); + var glSession = _surface.BeginDraw(); bool success = false; try { - var disp = session.Display; + var disp = glSession.Context; var gl = disp.GlInterface; gl.GetIntegerv(GL_FRAMEBUFFER_BINDING, out var fb); - var size = session.Size; - var scaling = session.Scaling; + var size = glSession.Size; + var scaling = glSession.Scaling; if (size.Width <= 0 || size.Height <= 0 || scaling < 0) { - session.Dispose(); + glSession.Dispose(); throw new InvalidOperationException( $"Can't create drawing context for surface with {size} size and {scaling} scaling"); } @@ -50,40 +80,21 @@ namespace Avalonia.Skia { _grContext.ResetContext(); - GRBackendRenderTarget renderTarget = + var renderTarget = new GRBackendRenderTarget(size.Width, size.Height, disp.SampleCount, disp.StencilSize, new GRGlFramebufferInfo((uint)fb, GRPixelConfig.Rgba8888.ToGlSizedFormat())); var surface = SKSurface.Create(_grContext, renderTarget, - GRSurfaceOrigin.BottomLeft, + glSession.IsYFlipped ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft, GRPixelConfig.Rgba8888.ToColorType()); - var nfo = new DrawingContextImpl.CreateInfo - { - GrContext = _grContext, - Canvas = surface.Canvas, - Dpi = SkiaPlatform.DefaultDpi * scaling, - VisualBrushRenderer = visualBrushRenderer, - DisableTextLcdRendering = true - }; - - - var ctx = new DrawingContextImpl(nfo, Disposable.Create(() => - { - - surface.Canvas.Flush(); - surface.Dispose(); - renderTarget.Dispose(); - _grContext.Flush(); - session.Dispose(); - })); success = true; - return ctx; + return new GlGpuSession(_grContext, renderTarget, surface, glSession); } } finally { if(!success) - session.Dispose(); + glSession.Dispose(); } } } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs new file mode 100644 index 0000000000..de188f42bd --- /dev/null +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Imaging; +using SkiaSharp; + +namespace Avalonia.Skia +{ + class GlSkiaGpu : IOpenGlAwareSkiaGpu + { + private GRContext _grContext; + + public GlSkiaGpu(IWindowingPlatformGlFeature gl, long? maxResourceBytes) + { + var context = gl.MainContext; + using (context.MakeCurrent()) + { + using (var iface = context.Version.Type == GlProfileType.OpenGL ? + GRGlInterface.AssembleGlInterface((_, proc) => context.GlInterface.GetProcAddress(proc)) : + GRGlInterface.AssembleGlesInterface((_, proc) => context.GlInterface.GetProcAddress(proc))) + { + _grContext = GRContext.Create(GRBackend.OpenGL, iface); + if (maxResourceBytes.HasValue) + { + _grContext.GetResourceCacheLimits(out var maxResources, out _); + _grContext.SetResourceCacheLimits(maxResources, maxResourceBytes.Value); + } + } + } + } + + public ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable surfaces) + { + foreach (var surface in surfaces) + { + if (surface is IGlPlatformSurface glSurface) + { + return new GlRenderTarget(_grContext, glSurface); + } + } + + return null; + } + + public IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap() => new OpenGlTextureBitmapImpl(); + } +} diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGlTextureBitmapImpl.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGlTextureBitmapImpl.cs new file mode 100644 index 0000000000..8d007e35f3 --- /dev/null +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGlTextureBitmapImpl.cs @@ -0,0 +1,81 @@ +using System; +using System.IO; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Imaging; +using Avalonia.Skia.Helpers; +using Avalonia.Utilities; +using SkiaSharp; + +namespace Avalonia.Skia +{ + class OpenGlTextureBitmapImpl : IOpenGlTextureBitmapImpl, IDrawableBitmapImpl + { + private DisposableLock _lock = new DisposableLock(); + private int _textureId; + private int _internalFormat; + + public void Dispose() + { + using (Lock()) + { + _textureId = 0; + PixelSize = new PixelSize(1, 1); + Version++; + } + } + + public Vector Dpi { get; private set; } = new Vector(96, 96); + public PixelSize PixelSize { get; private set; } = new PixelSize(1, 1); + public int Version { get; private set; } = 0; + + public void Save(string fileName) => throw new System.NotSupportedException(); + public void Save(Stream stream) => throw new System.NotSupportedException(); + + public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) + { + // For now silently ignore + if (context.GrContext == null) + return; + + using (Lock()) + { + if (_textureId == 0) + return; + using (var backendTexture = new GRBackendTexture(PixelSize.Width, PixelSize.Height, false, + new GRGlTextureInfo( + GlConsts.GL_TEXTURE_2D, (uint)_textureId, + (uint)_internalFormat))) + using (var surface = SKSurface.Create(context.GrContext, backendTexture, GRSurfaceOrigin.TopLeft, + SKColorType.Rgba8888)) + { + // Again, silently ignore, if something went wrong it's not our fault + if (surface == null) + return; + + using (var snapshot = surface.Snapshot()) + context.Canvas.DrawImage(snapshot, sourceRect, destRect, paint); + } + } + } + + public IDisposable Lock() => _lock.Lock(); + + public void SetBackBuffer(int textureId, int internalFormat, PixelSize pixelSize, double dpiScaling) + { + using (_lock.Lock()) + { + _textureId = textureId; + _internalFormat = internalFormat; + PixelSize = pixelSize; + Dpi = new Vector(96 * dpiScaling, 96 * dpiScaling); + Version++; + } + } + + public void SetDirty() + { + using (_lock.Lock()) + Version++; + } + } +} diff --git a/src/Skia/Avalonia.Skia/CustomRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs similarity index 60% rename from src/Skia/Avalonia.Skia/CustomRenderTarget.cs rename to src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs index 23a509a2a4..94e513b2fd 100644 --- a/src/Skia/Avalonia.Skia/CustomRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs @@ -1,19 +1,16 @@ -// 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.Platform; using Avalonia.Rendering; namespace Avalonia.Skia { /// - /// Adapts to be used within Skia rendering pipeline. + /// Adapts to be used within our rendering pipeline. /// - internal class CustomRenderTarget : IRenderTarget + internal class SkiaGpuRenderTarget : IRenderTargetWithCorruptionInfo { - private readonly ICustomSkiaRenderTarget _renderTarget; + private readonly ISkiaGpuRenderTarget _renderTarget; - public CustomRenderTarget(ICustomSkiaRenderTarget renderTarget) + public SkiaGpuRenderTarget(ISkiaGpuRenderTarget renderTarget) { _renderTarget = renderTarget; } @@ -25,7 +22,7 @@ namespace Avalonia.Skia public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) { - ICustomSkiaRenderSession session = _renderTarget.BeginRendering(); + var session = _renderTarget.BeginRenderingSession(); var nfo = new DrawingContextImpl.CreateInfo { @@ -38,5 +35,7 @@ namespace Avalonia.Skia return new DrawingContextImpl(nfo, session); } + + public bool IsCorrupted => _renderTarget.IsCorrupted; } } diff --git a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs index d587a989cc..5fa961cc99 100644 --- a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs @@ -1,6 +1,3 @@ -// 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 System; using System.IO; using SkiaSharp; diff --git a/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs index 04756994f5..9236bdee8d 100644 --- a/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs @@ -1,7 +1,4 @@ -// 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.Platform; +using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia.Helpers diff --git a/src/Skia/Avalonia.Skia/ICustomSkiaRenderTarget.cs b/src/Skia/Avalonia.Skia/ICustomSkiaRenderTarget.cs deleted file mode 100644 index f67b28b77b..0000000000 --- a/src/Skia/Avalonia.Skia/ICustomSkiaRenderTarget.cs +++ /dev/null @@ -1,19 +0,0 @@ -// 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 System; - -namespace Avalonia.Skia -{ - /// - /// Custom Skia render target. - /// - public interface ICustomSkiaRenderTarget : IDisposable - { - /// - /// Start rendering to this render target. - /// - /// - ICustomSkiaRenderSession BeginRendering(); - } -} diff --git a/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs b/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs index f900e7af77..be751021d5 100644 --- a/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs @@ -1,6 +1,3 @@ -// 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.Platform; using SkiaSharp; diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs index f283040eac..e84c7e34de 100644 --- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -1,10 +1,11 @@ -// 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 System; using System.IO; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Skia.Helpers; +using Avalonia.Visuals.Media.Imaging; using SkiaSharp; namespace Avalonia.Skia @@ -39,6 +40,75 @@ namespace Avalonia.Skia } } + public ImmutableBitmap(ImmutableBitmap src, PixelSize destinationSize, BitmapInterpolationMode interpolationMode) + { + SKImageInfo info = new SKImageInfo(destinationSize.Width, destinationSize.Height, SKColorType.Bgra8888); + SKImage output = SKImage.Create(info); + src._image.ScalePixels(output.PeekPixels(), interpolationMode.ToSKFilterQuality()); + + _image = output; + + PixelSize = new PixelSize(_image.Width, _image.Height); + + // TODO: Skia doesn't have an API for DPI. + Dpi = new Vector(96, 96); + } + + public ImmutableBitmap(Stream stream, int decodeSize, bool horizontal, BitmapInterpolationMode interpolationMode) + { + using (var skStream = new SKManagedStream(stream)) + using (var codec = SKCodec.Create(skStream)) + { + var info = codec.Info; + + // get the scale that is nearest to what we want (eg: jpg returned 512) + var supportedScale = codec.GetScaledDimensions(horizontal ? ((float)decodeSize / info.Width) : ((float)decodeSize / info.Height)); + + // decode the bitmap at the nearest size + var nearest = new SKImageInfo(supportedScale.Width, supportedScale.Height); + var bmp = SKBitmap.Decode(codec, nearest); + + // now scale that to the size that we want + var realScale = horizontal ? ((double)info.Height / info.Width) : ((double)info.Width / info.Height); + + SKImageInfo desired; + + + if (horizontal) + { + desired = new SKImageInfo(decodeSize, (int)(realScale * decodeSize)); + } + else + { + desired = new SKImageInfo((int)(realScale * decodeSize), decodeSize); + } + + if (bmp.Width != desired.Width || bmp.Height != desired.Height) + { + if (bmp.Height != bmp.Width) + { + + } + var scaledBmp = bmp.Resize(desired, interpolationMode.ToSKFilterQuality()); + bmp.Dispose(); + bmp = scaledBmp; + } + + _image = SKImage.FromBitmap(bmp); + bmp.Dispose(); + + if (_image == null) + { + throw new ArgumentException("Unable to load bitmap from provided data"); + } + + PixelSize = new PixelSize(_image.Width, _image.Height); + + // TODO: Skia doesn't have an API for DPI. + Dpi = new Vector(96, 96); + } + } + /// /// Create immutable bitmap from given pixel data copy. /// diff --git a/src/Skia/Avalonia.Skia/LineGeometryImpl.cs b/src/Skia/Avalonia.Skia/LineGeometryImpl.cs index e929e153d1..b102a4c119 100644 --- a/src/Skia/Avalonia.Skia/LineGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/LineGeometryImpl.cs @@ -1,6 +1,3 @@ -// 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 System; using SkiaSharp; diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 15f38b1c4f..0bc5dd56ac 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -1,13 +1,16 @@ -// 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 System; +using System.Collections; using System.Collections.Generic; -using System.IO; +using System.IO; +using System.Security.Cryptography; +using System.Linq; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Media; +using Avalonia.Media.Imaging; using Avalonia.OpenGL; +using Avalonia.OpenGL.Imaging; using Avalonia.Platform; +using Avalonia.Visuals.Media.Imaging; using SkiaSharp; namespace Avalonia.Skia @@ -15,49 +18,34 @@ namespace Avalonia.Skia /// /// Skia platform render interface. /// - internal class PlatformRenderInterface : IPlatformRenderInterface + internal class PlatformRenderInterface : IPlatformRenderInterface, IOpenGlAwarePlatformRenderInterface { - private readonly ICustomSkiaGpu _customSkiaGpu; - - private GRContext GrContext { get; } - - public IEnumerable InstalledFontNames => SKFontManager.Default.FontFamilies; + private readonly ISkiaGpu _skiaGpu; - public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu) + public PlatformRenderInterface(ISkiaGpu skiaGpu, long? maxResourceBytes = null) { - if (customSkiaGpu != null) + if (skiaGpu != null) { - _customSkiaGpu = customSkiaGpu; - - GrContext = _customSkiaGpu.GrContext; - + _skiaGpu = skiaGpu; return; } var gl = AvaloniaLocator.Current.GetService(); - if (gl != null) - { - var display = gl.ImmediateContext.Display; - gl.ImmediateContext.MakeCurrent(); - using (var iface = display.Type == GlDisplayType.OpenGL2 - ? GRGlInterface.AssembleGlInterface((_, proc) => display.GlInterface.GetProcAddress(proc)) - : GRGlInterface.AssembleGlesInterface((_, proc) => display.GlInterface.GetProcAddress(proc))) - { - GrContext = GRContext.Create(GRBackend.OpenGL, iface); - } - } + if (gl != null) + _skiaGpu = new GlSkiaGpu(gl, maxResourceBytes); } /// public IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, IReadOnlyList spans) { - return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans); + return new FormattedTextImpl(text, typeface, fontSize, textAlignment, wrapping, constraint, spans); } public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect); @@ -72,12 +60,6 @@ namespace Avalonia.Skia return new StreamGeometryImpl(); } - /// - public IBitmapImpl LoadBitmap(Stream stream) - { - return new ImmutableBitmap(stream); - } - /// public IBitmapImpl LoadBitmap(string fileName) { @@ -87,12 +69,43 @@ namespace Avalonia.Skia } } + /// + public IBitmapImpl LoadBitmap(Stream stream) + { + return new ImmutableBitmap(stream); + } + /// public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride) { return new ImmutableBitmap(size, dpi, stride, format, data); } + /// + public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + return new ImmutableBitmap(stream, width, true, interpolationMode); + } + + /// + public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + return new ImmutableBitmap(stream, height, false, interpolationMode); + } + + /// + public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + if (bitmapImpl is ImmutableBitmap ibmp) + { + return new ImmutableBitmap(ibmp, destinationSize, interpolationMode); + } + else + { + throw new Exception("Invalid source bitmap type."); + } + } + /// public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) { @@ -120,26 +133,18 @@ namespace Avalonia.Skia /// public IRenderTarget CreateRenderTarget(IEnumerable surfaces) { - if (_customSkiaGpu != null) + if (!(surfaces is IList)) + surfaces = surfaces.ToList(); + var gpuRenderTarget = _skiaGpu?.TryCreateRenderTarget(surfaces); + if (gpuRenderTarget != null) { - ICustomSkiaRenderTarget customRenderTarget = _customSkiaGpu.TryCreateRenderTarget(surfaces); - - if (customRenderTarget != null) - { - return new CustomRenderTarget(customRenderTarget); - } + return new SkiaGpuRenderTarget(gpuRenderTarget); } foreach (var surface in surfaces) { - if (surface is IGlPlatformSurface glSurface && GrContext != null) - { - return new GlRenderTarget(GrContext, glSurface); - } if (surface is IFramebufferPlatformSurface framebufferSurface) - { return new FramebufferRenderTarget(framebufferSurface); - } } throw new NotSupportedException( @@ -151,5 +156,116 @@ namespace Avalonia.Skia { return new WriteableBitmapImpl(size, dpi, format); } + + private static readonly SKPaint s_paint = new SKPaint + { + TextEncoding = SKTextEncoding.GlyphId, + IsAntialias = true, + IsStroke = false, + SubpixelText = true + }; + + private static readonly SKTextBlobBuilder s_textBlobBuilder = new SKTextBlobBuilder(); + + /// + public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) + { + var count = glyphRun.GlyphIndices.Length; + + var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl; + + var typeface = glyphTypeface.Typeface; + + s_paint.TextSize = (float)glyphRun.FontRenderingEmSize; + s_paint.Typeface = typeface; + + + SKTextBlob textBlob; + + width = 0; + + var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight); + + if (glyphRun.GlyphOffsets.IsEmpty) + { + if (glyphTypeface.IsFixedPitch) + { + s_textBlobBuilder.AddRun(s_paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span); + + textBlob = s_textBlobBuilder.Build(); + + width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length; + } + else + { + var buffer = s_textBlobBuilder.AllocateHorizontalRun(s_paint, count, 0); + + var positions = buffer.GetPositionSpan(); + + for (var i = 0; i < count; i++) + { + positions[i] = (float)width; + + if (glyphRun.GlyphAdvances.IsEmpty) + { + width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; + } + else + { + width += glyphRun.GlyphAdvances[i]; + } + } + + buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span); + + textBlob = s_textBlobBuilder.Build(); + } + } + else + { + var buffer = s_textBlobBuilder.AllocatePositionedRun(s_paint, count); + + var glyphPositions = buffer.GetPositionSpan(); + + var currentX = 0.0; + + for (var i = 0; i < count; i++) + { + var glyphOffset = glyphRun.GlyphOffsets[i]; + + glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y); + + if (glyphRun.GlyphAdvances.IsEmpty) + { + currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; + } + else + { + currentX += glyphRun.GlyphAdvances[i]; + } + } + + buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span); + + width = currentX; + + textBlob = s_textBlobBuilder.Build(); + } + + return new GlyphRunImpl(textBlob); + + } + + public IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap() + { + if (_skiaGpu is IOpenGlAwareSkiaGpu glAware) + return glAware.CreateOpenGlTextureBitmap(); + if (_skiaGpu == null) + throw new PlatformNotSupportedException("GPU acceleration is not available"); + throw new PlatformNotSupportedException( + "Current GPU acceleration backend does not support OpenGL integration"); + } + + public bool SupportsIndividualRoundRects => true; } } diff --git a/src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs b/src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..f6aabfae39 --- /dev/null +++ b/src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")] +[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests")] diff --git a/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs b/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs index a873e8e2df..93d453e8f0 100644 --- a/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs @@ -1,6 +1,3 @@ -// 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 SkiaSharp; namespace Avalonia.Skia diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 17448127b0..7aea90e61e 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -1,117 +1,60 @@ -// 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 System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; - using Avalonia.Media; - +using Avalonia.Media.Fonts; using SkiaSharp; namespace Avalonia.Skia { internal class SKTypefaceCollection { - private readonly ConcurrentDictionary> _fontFamilies = - new ConcurrentDictionary>(); + private readonly ConcurrentDictionary _typefaces = + new ConcurrentDictionary(); - public void AddTypeFace(SKTypeface typeface) + public void AddTypeface(FontKey key, SKTypeface typeface) { - var key = new FontKey((SKFontStyleWeight)typeface.FontWeight, typeface.FontSlant); - - if (!_fontFamilies.TryGetValue(typeface.FamilyName, out var fontFamily)) - { - fontFamily = new ConcurrentDictionary(); - - _fontFamilies.TryAdd(typeface.FamilyName, fontFamily); - } - - fontFamily.TryAdd(key, typeface); + _typefaces.TryAdd(key, typeface); } - public SKTypeface GetTypeFace(Typeface typeface) + public SKTypeface Get(Typeface typeface) { - var styleSlant = SKFontStyleSlant.Upright; + var key = new FontKey(typeface.FontFamily.Name, typeface.Weight, typeface.Style); - switch (typeface.Style) - { - case FontStyle.Italic: - styleSlant = SKFontStyleSlant.Italic; - break; - - case FontStyle.Oblique: - styleSlant = SKFontStyleSlant.Oblique; - break; - } + return GetNearestMatch(_typefaces, key); + } - if (!_fontFamilies.TryGetValue(typeface.FontFamily.Name, out var fontFamily)) + private static SKTypeface GetNearestMatch(IDictionary typefaces, FontKey key) + { + if (typefaces.ContainsKey(key)) { - return TypefaceCache.GetTypeface(TypefaceCache.DefaultFamilyName, typeface.Style, typeface.Weight); + return typefaces[key]; } - var weight = (SKFontStyleWeight)typeface.Weight; - - var key = new FontKey(weight, styleSlant); - - return fontFamily.GetOrAdd(key, GetFallback(fontFamily, key)); - } - - private static SKTypeface GetFallback(IDictionary fontFamily, FontKey key) - { - var keys = fontFamily.Keys.Where( - x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Slant == key.Slant).ToArray(); + var keys = typefaces.Keys.Where( + x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Style == key.Style).ToArray(); if (!keys.Any()) { - keys = fontFamily.Keys.Where( - x => x.Weight == key.Weight && (x.Slant >= key.Slant || x.Slant < key.Slant)).ToArray(); + keys = typefaces.Keys.Where( + x => x.Weight == key.Weight && (x.Style >= key.Style || x.Style < key.Style)).ToArray(); if (!keys.Any()) { - keys = fontFamily.Keys.Where( + keys = typefaces.Keys.Where( x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && - (x.Slant >= key.Slant || x.Slant < key.Slant)).ToArray(); + (x.Style >= key.Style || x.Style < key.Style)).ToArray(); } } - key = keys.FirstOrDefault(); - - fontFamily.TryGetValue(key, out var typeface); - - return typeface; - } - - private struct FontKey - { - public readonly SKFontStyleSlant Slant; - public readonly SKFontStyleWeight Weight; - - public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant) - { - Slant = slant; - Weight = weight; - } - - public override int GetHashCode() + if (keys.Length == 0) { - var hash = 17; - hash = (hash * 31) + (int)Slant; - hash = (hash * 31) + (int)Weight; - - return hash; + return null; } - public override bool Equals(object other) - { - return other is FontKey key && this.Equals(key); - } + key = keys[0]; - private bool Equals(FontKey other) - { - return Slant == other.Slant && - Weight == other.Weight; - } + return typefaces[key]; } } } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index ab8ee85a54..a9aed80a04 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -1,6 +1,4 @@ -// 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 System; using System.Collections.Concurrent; using Avalonia.Media; using Avalonia.Media.Fonts; @@ -11,11 +9,11 @@ namespace Avalonia.Skia { internal static class SKTypefaceCollectionCache { - private static readonly ConcurrentDictionary s_cachedCollections; + private static readonly ConcurrentDictionary s_cachedCollections; static SKTypefaceCollectionCache() { - s_cachedCollections = new ConcurrentDictionary(); + s_cachedCollections = new ConcurrentDictionary(); } /// @@ -25,7 +23,7 @@ namespace Avalonia.Skia /// public static SKTypefaceCollection GetOrAddTypefaceCollection(FontFamily fontFamily) { - return s_cachedCollections.GetOrAdd(fontFamily.Key, x => CreateCustomFontCollection(fontFamily)); + return s_cachedCollections.GetOrAdd(fontFamily, x => CreateCustomFontCollection(fontFamily)); } /// @@ -45,9 +43,20 @@ namespace Avalonia.Skia { var assetStream = assetLoader.Open(asset); + if (assetStream == null) throw new InvalidOperationException("Asset could not be loaded."); + var typeface = SKTypeface.FromStream(assetStream); - typeFaceCollection.AddTypeFace(typeface); + if(typeface == null) throw new InvalidOperationException("Typeface could not be loaded."); + + if (typeface.FamilyName != fontFamily.Name) + { + continue; + } + + var key = new FontKey(fontFamily.Name, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant); + + typeFaceCollection.AddTypeface(key, typeface); } return typeFaceCollection; diff --git a/src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs b/src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs index 102f1f92aa..014298ce83 100644 --- a/src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs @@ -1,6 +1,3 @@ -// 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.Controls; using Avalonia.Skia; diff --git a/src/Skia/Avalonia.Skia/SkiaOptions.cs b/src/Skia/Avalonia.Skia/SkiaOptions.cs index bac1849be8..cbe0b5ef42 100644 --- a/src/Skia/Avalonia.Skia/SkiaOptions.cs +++ b/src/Skia/Avalonia.Skia/SkiaOptions.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Skia; @@ -14,6 +11,11 @@ namespace Avalonia /// /// Custom gpu factory to use. Can be used to customize behavior of Skia renderer. /// - public Func CustomGpuFactory { get; set; } + public Func CustomGpuFactory { get; set; } + + /// + /// The maximum number of bytes for video memory to store textures and resources. + /// + public long? MaxGpuResourceSizeBytes { get; set; } } } diff --git a/src/Skia/Avalonia.Skia/SkiaPlatform.cs b/src/Skia/Avalonia.Skia/SkiaPlatform.cs index f16e967f42..9a5725e06f 100644 --- a/src/Skia/Avalonia.Skia/SkiaPlatform.cs +++ b/src/Skia/Avalonia.Skia/SkiaPlatform.cs @@ -1,6 +1,3 @@ -// 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.Platform; namespace Avalonia.Skia @@ -21,10 +18,12 @@ namespace Avalonia.Skia public static void Initialize(SkiaOptions options) { var customGpu = options.CustomGpuFactory?.Invoke(); - var renderInterface = new PlatformRenderInterface(customGpu); + var renderInterface = new PlatformRenderInterface(customGpu, options.MaxGpuResourceSizeBytes); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(renderInterface); + .Bind().ToConstant(renderInterface) + .Bind().ToConstant(new FontManagerImpl()) + .Bind().ToConstant(new TextShaperImpl()); } /// diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index 9196ace4d8..ec7e0a67ed 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs @@ -1,25 +1,60 @@ -// 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 System; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Visuals.Media.Imaging; using SkiaSharp; namespace Avalonia.Skia { public static class SkiaSharpExtensions { + public static SKFilterQuality ToSKFilterQuality(this BitmapInterpolationMode interpolationMode) + { + switch (interpolationMode) + { + case BitmapInterpolationMode.LowQuality: + return SKFilterQuality.Low; + case BitmapInterpolationMode.MediumQuality: + return SKFilterQuality.Medium; + case BitmapInterpolationMode.HighQuality: + return SKFilterQuality.High; + case BitmapInterpolationMode.Default: + return SKFilterQuality.None; + default: + throw new ArgumentOutOfRangeException(nameof(interpolationMode), interpolationMode, null); + } + } + public static SKPoint ToSKPoint(this Point p) { return new SKPoint((float)p.X, (float)p.Y); } + + public static SKPoint ToSKPoint(this Vector p) + { + return new SKPoint((float)p.X, (float)p.Y); + } public static SKRect ToSKRect(this Rect r) { 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/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index 2764c65c6f..86450690e6 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs @@ -1,6 +1,3 @@ -// 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.Media; using Avalonia.Platform; using SkiaSharp; diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index 9340c9add4..588f7bee6c 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -1,6 +1,3 @@ -// 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 System; using System.IO; using System.Reactive.Disposables; diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs new file mode 100644 index 0000000000..7a0823a223 --- /dev/null +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -0,0 +1,148 @@ +using System; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.Platform; +using Avalonia.Utility; +using HarfBuzzSharp; +using Buffer = HarfBuzzSharp.Buffer; + +namespace Avalonia.Skia +{ + internal class TextShaperImpl : ITextShaperImpl + { + public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat) + { + using (var buffer = new Buffer()) + { + buffer.ContentType = ContentType.Unicode; + + var breakCharPosition = text.Length - 1; + + var codepoint = Codepoint.ReadAt(text, breakCharPosition, out var count); + + if (codepoint.IsBreakChar) + { + var breakCharCount = 1; + + if (text.Length > 1) + { + var previousCodepoint = Codepoint.ReadAt(text, breakCharPosition - count, out _); + + if (codepoint == '\r' && previousCodepoint == '\n' + || codepoint == '\n' && previousCodepoint == '\r') + { + breakCharCount = 2; + } + } + + if (breakCharPosition != text.Start) + { + buffer.AddUtf16(text.Buffer.Span.Slice(0, text.Length - breakCharCount)); + } + + var cluster = buffer.GlyphInfos.Length > 0 ? + buffer.GlyphInfos[buffer.Length - 1].Cluster + 1 : + (uint)text.Start; + + switch (breakCharCount) + { + case 1: + buffer.Add('\u200C', cluster); + break; + case 2: + buffer.Add('\u200C', cluster); + buffer.Add('\u200D', cluster); + break; + } + } + else + { + buffer.AddUtf16(text.Buffer.Span); + } + + buffer.GuessSegmentProperties(); + + var glyphTypeface = textFormat.Typeface.GlyphTypeface; + + var font = ((GlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font; + + font.Shape(buffer); + + font.GetScale(out var scaleX, out _); + + var textScale = textFormat.FontRenderingEmSize / scaleX; + + var bufferLength = buffer.Length; + + var glyphInfos = buffer.GetGlyphInfoSpan(); + + var glyphPositions = buffer.GetGlyphPositionSpan(); + + var glyphIndices = new ushort[bufferLength]; + + var clusters = new ushort[bufferLength]; + + double[] glyphAdvances = null; + + Vector[] glyphOffsets = null; + + for (var i = 0; i < bufferLength; i++) + { + glyphIndices[i] = (ushort)glyphInfos[i].Codepoint; + + clusters[i] = (ushort)(text.Start + glyphInfos[i].Cluster); + + if (!glyphTypeface.IsFixedPitch) + { + SetAdvance(glyphPositions, i, textScale, ref glyphAdvances); + } + + SetOffset(glyphPositions, i, textScale, ref glyphOffsets); + } + + return new GlyphRun(glyphTypeface, textFormat.FontRenderingEmSize, + new ReadOnlySlice(glyphIndices), + new ReadOnlySlice(glyphAdvances), + new ReadOnlySlice(glyphOffsets), + text, + new ReadOnlySlice(clusters)); + } + } + + private static void SetOffset(ReadOnlySpan glyphPositions, int index, double textScale, + ref Vector[] offsetBuffer) + { + var position = glyphPositions[index]; + + if (position.XOffset == 0 && position.YOffset == 0) + { + return; + } + + if (offsetBuffer == null) + { + offsetBuffer = new Vector[glyphPositions.Length]; + } + + var offsetX = position.XOffset * textScale; + + var offsetY = position.YOffset * textScale; + + offsetBuffer[index] = new Vector(offsetX, offsetY); + } + + private static void SetAdvance(ReadOnlySpan glyphPositions, int index, double textScale, + ref double[] advanceBuffer) + { + if (advanceBuffer == null) + { + advanceBuffer = new double[glyphPositions.Length]; + } + + // Depends on direction of layout + // advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale; + advanceBuffer[index] = glyphPositions[index].XAdvance * textScale; + } + } +} diff --git a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs index 9826bc2ce3..64d5b58970 100644 --- a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs @@ -1,6 +1,3 @@ -// 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.Platform; using SkiaSharp; diff --git a/src/Skia/Avalonia.Skia/TypefaceCache.cs b/src/Skia/Avalonia.Skia/TypefaceCache.cs deleted file mode 100644 index 9e270114d2..0000000000 --- a/src/Skia/Avalonia.Skia/TypefaceCache.cs +++ /dev/null @@ -1,99 +0,0 @@ -// 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 System.Collections.Generic; -using Avalonia.Media; -using SkiaSharp; - -namespace Avalonia.Skia -{ - /// - /// Cache for Skia typefaces. - /// - internal static class TypefaceCache - { - public static readonly string DefaultFamilyName = CreateDefaultFamilyName(); - - private static readonly Dictionary> s_cache = - new Dictionary>(); - - struct FontKey - { - public readonly SKFontStyleSlant Slant; - public readonly SKFontStyleWeight Weight; - - public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant) - { - Slant = slant; - Weight = weight; - } - - public override int GetHashCode() - { - int hash = 17; - hash = hash * 31 + (int)Slant; - hash = hash * 31 + (int)Weight; - - return hash; - } - - public override bool Equals(object other) - { - return other is FontKey ? Equals((FontKey)other) : false; - } - - public bool Equals(FontKey other) - { - return Slant == other.Slant && - Weight == other.Weight; - } - - // Equals and GetHashCode ommitted - } - - private static string CreateDefaultFamilyName() - { - var defaultTypeface = SKTypeface.CreateDefault(); - - return defaultTypeface.FamilyName; - } - - private static SKTypeface GetTypeface(string name, FontKey key) - { - var familyKey = name; - - if (!s_cache.TryGetValue(familyKey, out var entry)) - { - s_cache[familyKey] = entry = new Dictionary(); - } - - if (!entry.TryGetValue(key, out var typeface)) - { - typeface = SKTypeface.FromFamilyName(familyKey, key.Weight, SKFontStyleWidth.Normal, key.Slant) ?? - GetTypeface(DefaultFamilyName, key); - - entry[key] = typeface; - } - - return typeface; - } - - public static SKTypeface GetTypeface(string name, FontStyle style, FontWeight weight) - { - var skStyle = SKFontStyleSlant.Upright; - - switch (style) - { - case FontStyle.Italic: - skStyle = SKFontStyleSlant.Italic; - break; - - case FontStyle.Oblique: - skStyle = SKFontStyleSlant.Oblique; - break; - } - - return GetTypeface(name, new FontKey((SKFontStyleWeight)weight, skStyle)); - } - } -} diff --git a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs index fea21cde58..9bdd30f40b 100644 --- a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs @@ -1,6 +1,3 @@ -// 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 System; using System.IO; using System.Threading; diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj index 458d8f9cbb..cda95d2ebb 100644 --- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj +++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj @@ -3,6 +3,7 @@ netstandard2.0 true Avalonia.Direct2D1 + true @@ -14,6 +15,7 @@ + diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 5ab9a8f74d..c4c0541d53 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -1,7 +1,5 @@ -// 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 System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using Avalonia.Controls; @@ -9,7 +7,12 @@ using Avalonia.Controls.Platform.Surfaces; using Avalonia.Direct2D1.Media; using Avalonia.Direct2D1.Media.Imaging; using Avalonia.Media; +using Avalonia.Media.Imaging; using Avalonia.Platform; +using Avalonia.Visuals.Media.Imaging; +using SharpDX.DirectWrite; +using GlyphRun = Avalonia.Media.GlyphRun; +using TextAlignment = Avalonia.Media.TextAlignment; namespace Avalonia { @@ -41,20 +44,6 @@ namespace Avalonia.Direct2D1 public static SharpDX.DXGI.Device1 DxgiDevice { get; private set; } - public IEnumerable InstalledFontNames - { - get - { - var cache = Direct2D1FontCollectionCache.s_installedFontCollection; - var length = cache.FontFamilyCount; - for (int i = 0; i < length; i++) - { - var names = cache.GetFontFamily(i).FamilyNames; - yield return names.GetString(0); - } - } - } - private static readonly object s_initLock = new object(); private static bool s_initialized = false; @@ -119,7 +108,10 @@ namespace Avalonia.Direct2D1 public static void Initialize() { InitializeDirect2D(); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(s_instance); + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(s_instance) + .Bind().ToConstant(new FontManagerImpl()) + .Bind().ToConstant(new TextShaperImpl()); SharpDX.Configuration.EnableReleaseOnFinalizer = true; } @@ -131,6 +123,7 @@ namespace Avalonia.Direct2D1 public IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, @@ -139,6 +132,7 @@ namespace Avalonia.Direct2D1 return new FormattedTextImpl( text, typeface, + fontSize, textAlignment, wrapping, constraint, @@ -187,19 +181,90 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateRectangleGeometry(Rect rect) => new RectangleGeometryImpl(rect); public IStreamGeometryImpl CreateStreamGeometry() => new StreamGeometryImpl(); + /// public IBitmapImpl LoadBitmap(string fileName) { return new WicBitmapImpl(fileName); } + /// public IBitmapImpl LoadBitmap(Stream stream) { return new WicBitmapImpl(stream); } + /// + public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + return new WicBitmapImpl(stream, width, true, interpolationMode); + } + + /// + public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + return new WicBitmapImpl(stream, height, false, interpolationMode); + } + + /// + public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + // https://github.com/sharpdx/SharpDX/issues/959 blocks implementation. + throw new NotImplementedException(); + } + + /// public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride) { return new WicBitmapImpl(format, data, size, dpi, stride); } + + public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) + { + var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl; + + var glyphCount = glyphRun.GlyphIndices.Length; + + var run = new SharpDX.DirectWrite.GlyphRun + { + FontFace = glyphTypeface.FontFace, + FontSize = (float)glyphRun.FontRenderingEmSize + }; + + var indices = new short[glyphCount]; + + for (var i = 0; i < glyphCount; i++) + { + indices[i] = (short)glyphRun.GlyphIndices[i]; + } + + run.Indices = indices; + + run.Advances = new float[glyphCount]; + + width = 0; + + for (var i = 0; i < glyphCount; i++) + { + run.Advances[i] = (float)glyphRun.GlyphAdvances[i]; + width += run.Advances[i]; + } + + run.Offsets = new GlyphOffset[glyphCount]; + + for (var i = 0; i < glyphCount; i++) + { + var offset = glyphRun.GlyphOffsets[i]; + + run.Offsets[i] = new GlyphOffset + { + AdvanceOffset = (float)offset.X, + AscenderOffset = (float)offset.Y + }; + } + + return new GlyphRunImpl(run); + } + + public bool SupportsIndividualRoundRects => false; } } diff --git a/src/Windows/Avalonia.Direct2D1/Disposable.cs b/src/Windows/Avalonia.Direct2D1/Disposable.cs index bd5070626b..63dfeb2e0b 100644 --- a/src/Windows/Avalonia.Direct2D1/Disposable.cs +++ b/src/Windows/Avalonia.Direct2D1/Disposable.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Direct2D1 diff --git a/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs b/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs index 6e6cf47254..22c998df93 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs @@ -1,6 +1,3 @@ -// 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 SharpDX; using SharpDX.Direct2D1; using SharpDX.DirectWrite; diff --git a/src/Windows/Avalonia.Direct2D1/Media/BrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/BrushImpl.cs index 5b0781161f..ad609a0810 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/BrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/BrushImpl.cs @@ -1,6 +1,3 @@ -// 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 System; namespace Avalonia.Direct2D1.Media diff --git a/src/Windows/Avalonia.Direct2D1/Media/BrushWrapper.cs b/src/Windows/Avalonia.Direct2D1/Media/BrushWrapper.cs index 6343b38c0e..e9acf996c1 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/BrushWrapper.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/BrushWrapper.cs @@ -1,6 +1,3 @@ -// 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.Media; using SharpDX; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs index d93a59d384..78bf25d607 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs @@ -1,62 +1,62 @@ using System.Collections.Concurrent; using Avalonia.Media; using Avalonia.Media.Fonts; +using SharpDX.DirectWrite; +using FontFamily = Avalonia.Media.FontFamily; +using FontStyle = SharpDX.DirectWrite.FontStyle; +using FontWeight = SharpDX.DirectWrite.FontWeight; namespace Avalonia.Direct2D1.Media { internal static class Direct2D1FontCollectionCache { - private static readonly ConcurrentDictionary s_cachedCollections; - internal static readonly SharpDX.DirectWrite.FontCollection s_installedFontCollection; + private static readonly ConcurrentDictionary s_cachedCollections; + internal static readonly FontCollection InstalledFontCollection; static Direct2D1FontCollectionCache() { - s_cachedCollections = new ConcurrentDictionary(); + s_cachedCollections = new ConcurrentDictionary(); - s_installedFontCollection = Direct2D1Platform.DirectWriteFactory.GetSystemFontCollection(false); + InstalledFontCollection = Direct2D1Platform.DirectWriteFactory.GetSystemFontCollection(false); } - public static SharpDX.DirectWrite.TextFormat GetTextFormat(Typeface typeface) + public static Font GetFont(Typeface typeface) { var fontFamily = typeface.FontFamily; var fontCollection = GetOrAddFontCollection(fontFamily); - var fontFamilyName = FontFamily.Default.Name; + int index; - // Should this be cached? - foreach (var familyName in fontFamily.FamilyNames) + foreach (var name in fontFamily.FamilyNames) { - if (!fontCollection.FindFamilyName(familyName, out _)) + if (fontCollection.FindFamilyName(name, out index)) { - continue; + return fontCollection.GetFontFamily(index).GetFirstMatchingFont( + (FontWeight)typeface.Weight, + FontStretch.Normal, + (FontStyle)typeface.Style); } - - fontFamilyName = familyName; - - break; } - return new SharpDX.DirectWrite.TextFormat( - Direct2D1Platform.DirectWriteFactory, - fontFamilyName, - fontCollection, - (SharpDX.DirectWrite.FontWeight)typeface.Weight, - (SharpDX.DirectWrite.FontStyle)typeface.Style, - SharpDX.DirectWrite.FontStretch.Normal, - (float)typeface.FontSize); + InstalledFontCollection.FindFamilyName("Segoe UI", out index); + + return InstalledFontCollection.GetFontFamily(index).GetFirstMatchingFont( + (FontWeight)typeface.Weight, + FontStretch.Normal, + (FontStyle)typeface.Style); } - private static SharpDX.DirectWrite.FontCollection GetOrAddFontCollection(FontFamily fontFamily) + private static FontCollection GetOrAddFontCollection(FontFamily fontFamily) { - return fontFamily.Key == null ? s_installedFontCollection : s_cachedCollections.GetOrAdd(fontFamily.Key, CreateFontCollection); + return fontFamily.Key == null ? InstalledFontCollection : s_cachedCollections.GetOrAdd(fontFamily.Key, CreateFontCollection); } - private static SharpDX.DirectWrite.FontCollection CreateFontCollection(FontFamilyKey key) + private static FontCollection CreateFontCollection(FontFamilyKey key) { var assets = FontFamilyLoader.LoadFontAssets(key); var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, assets); - return new SharpDX.DirectWrite.FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key); + return new FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key); } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 39d801eb2f..e0de40525f 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using Avalonia.Media; @@ -109,7 +106,7 @@ namespace Avalonia.Direct2D1.Media /// The rect in the image to draw. /// The rect in the output to draw to. /// The bitmap interpolation mode. - public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { using (var d2d = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext)) { @@ -149,7 +146,7 @@ namespace Avalonia.Direct2D1.Media /// The opacity mask to draw with. /// The destination rect for the opacity mask. /// The rect in the output to draw to. - public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { using (var d2dSource = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext)) using (var sourceBrush = new BitmapBrush(_deviceContext, d2dSource.Value)) @@ -230,34 +227,69 @@ namespace Avalonia.Direct2D1.Media } } - /// - /// Draws the outline of a rectangle. - /// - /// The pen. - /// The rectangle bounds. - /// The corner radius. - public void DrawRectangle(IPen pen, Rect rect, float cornerRadius) + /// + public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rrect, BoxShadows boxShadow = default) { - using (var brush = CreateBrush(pen.Brush, rect.Size)) - using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext)) + var rc = rrect.Rect.ToDirect2D(); + var rect = rrect.Rect; + var radiusX = Math.Max(rrect.RadiiTopLeft.X, + 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 = !MathUtilities.IsZero(radiusX) || !MathUtilities.IsZero(radiusY); + + if (brush != null) { - if (brush.PlatformBrush != null) + using (var b = CreateBrush(brush, rect.Size)) { - if (cornerRadius == 0) + if (b.PlatformBrush != null) { - _deviceContext.DrawRectangle( - rect.ToDirect2D(), - brush.PlatformBrush, - (float)pen.Thickness, - d2dStroke); + if (isRounded) + { + _deviceContext.FillRoundedRectangle( + new RoundedRectangle + { + Rect = new RawRectangleF( + (float)rect.X, + (float)rect.Y, + (float)rect.Right, + (float)rect.Bottom), + RadiusX = (float)radiusX, + RadiusY = (float)radiusY + }, + b.PlatformBrush); + } + else + { + _deviceContext.FillRectangle(rc, b.PlatformBrush); + } } - else + } + } + + if (pen?.Brush != null) + { + using (var wrapper = CreateBrush(pen.Brush, rect.Size)) + using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext)) + { + if (wrapper.PlatformBrush != null) { - _deviceContext.DrawRoundedRectangle( - new RoundedRectangle { Rect = rect.ToDirect2D(), RadiusX = cornerRadius, RadiusY = cornerRadius }, - brush.PlatformBrush, - (float)pen.Thickness, - d2dStroke); + if (isRounded) + { + _deviceContext.DrawRoundedRectangle( + new RoundedRectangle { Rect = rc, RadiusX = (float)radiusX, RadiusY = (float)radiusY }, + wrapper.PlatformBrush, + (float)pen.Thickness, + d2dStroke); + } + else + { + _deviceContext.DrawRectangle( + rc, + wrapper.PlatformBrush, + (float)pen.Thickness, + d2dStroke); + } } } } @@ -287,37 +319,18 @@ namespace Avalonia.Direct2D1.Media } /// - /// Draws a filled rectangle. + /// Draws a glyph run. /// - /// The brush. - /// The rectangle bounds. - /// The corner radius. - public void FillRectangle(IBrush brush, Rect rect, float cornerRadius) + /// The foreground. + /// The glyph run. + /// + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) { - using (var b = CreateBrush(brush, rect.Size)) + using (var brush = CreateBrush(foreground, glyphRun.Bounds.Size)) { - if (b.PlatformBrush != null) - { - if (cornerRadius == 0) - { - _deviceContext.FillRectangle(rect.ToDirect2D(), b.PlatformBrush); - } - else - { - _deviceContext.FillRoundedRectangle( - new RoundedRectangle - { - Rect = new RawRectangleF( - (float)rect.X, - (float)rect.Y, - (float)rect.Right, - (float)rect.Bottom), - RadiusX = cornerRadius, - RadiusY = cornerRadius - }, - b.PlatformBrush); - } - } + var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; + + _renderTarget.DrawGlyphRun(baselineOrigin.ToSharpDX(), glyphRunImpl.GlyphRun, brush.PlatformBrush, MeasuringMode.Natural); } } @@ -346,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/EllipseGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/EllipseGeometryImpl.cs index 9440966406..97fd6b63ba 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/EllipseGeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/EllipseGeometryImpl.cs @@ -1,6 +1,3 @@ -// 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 SharpDX.Direct2D1; namespace Avalonia.Direct2D1.Media diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs new file mode 100644 index 0000000000..253a373106 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Media; +using Avalonia.Media.Fonts; +using Avalonia.Platform; +using SharpDX.DirectWrite; +using FontFamily = Avalonia.Media.FontFamily; +using FontStyle = Avalonia.Media.FontStyle; +using FontWeight = Avalonia.Media.FontWeight; + +namespace Avalonia.Direct2D1.Media +{ + internal class FontManagerImpl : IFontManagerImpl + { + public string GetDefaultFontFamilyName() + { + //ToDo: Implement a real lookup of the system's default font. + return "Segoe UI"; + } + + public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + { + var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; + + var fontFamilies = new string[familyCount]; + + for (var i = 0; i < familyCount; i++) + { + fontFamilies[i] = Direct2D1FontCollectionCache.InstalledFontCollection.GetFontFamily(i).FamilyNames.GetString(0); + } + + return fontFamilies; + } + + public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) + { + var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; + + for (var i = 0; i < familyCount; i++) + { + var font = Direct2D1FontCollectionCache.InstalledFontCollection.GetFontFamily(i) + .GetMatchingFonts((SharpDX.DirectWrite.FontWeight)fontWeight, FontStretch.Normal, + (SharpDX.DirectWrite.FontStyle)fontStyle).GetFont(0); + + if (!font.HasCharacter(codepoint)) + { + continue; + } + + var fontFamilyName = font.FontFamily.FamilyNames.GetString(0); + + fontKey = new FontKey(fontFamilyName, fontWeight, fontStyle); + + return true; + } + + fontKey = default; + + return false; + } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + return new GlyphTypefaceImpl(typeface); + } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs index b73deb1f0a..c59067d82d 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs @@ -1,6 +1,3 @@ -// 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 System.Collections.Generic; using System.Linq; using Avalonia.Media; @@ -14,6 +11,7 @@ namespace Avalonia.Direct2D1.Media public FormattedTextImpl( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, @@ -21,20 +19,26 @@ namespace Avalonia.Direct2D1.Media { Text = text; - using (var textFormat = Direct2D1FontCollectionCache.GetTextFormat(typeface)) + var font = ((GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl).DWFont; + var familyName = font.FontFamily.FamilyNames.GetString(0); + using (var textFormat = new DWrite.TextFormat( + Direct2D1Platform.DirectWriteFactory, + familyName, + font.FontFamily.FontCollection, + (DWrite.FontWeight)typeface.Weight, + (DWrite.FontStyle)typeface.Style, + DWrite.FontStretch.Normal, + (float)fontSize)) { textFormat.WordWrapping = wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap; TextLayout = new DWrite.TextLayout( - Direct2D1Platform.DirectWriteFactory, - Text ?? string.Empty, - textFormat, - (float)constraint.Width, - (float)constraint.Height) - { - TextAlignment = textAlignment.ToDirect2D() - }; + Direct2D1Platform.DirectWriteFactory, + Text ?? string.Empty, + textFormat, + (float)constraint.Width, + (float)constraint.Height) { TextAlignment = textAlignment.ToDirect2D() }; } if (spans != null) diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs index 51ca2520ad..636309ad1a 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs @@ -1,6 +1,3 @@ -// 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.Platform; using SharpDX.Direct2D1; diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs new file mode 100644 index 0000000000..0b06d5ef3e --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs @@ -0,0 +1,19 @@ +using Avalonia.Platform; + +namespace Avalonia.Direct2D1.Media +{ + internal class GlyphRunImpl : IGlyphRunImpl + { + public GlyphRunImpl(SharpDX.DirectWrite.GlyphRun glyphRun) + { + GlyphRun = glyphRun; + } + + public SharpDX.DirectWrite.GlyphRun GlyphRun { get; } + + public void Dispose() + { + GlyphRun?.Dispose(); + } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs new file mode 100644 index 0000000000..4f2ed22a25 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs @@ -0,0 +1,190 @@ +using System; +using Avalonia.Media; +using Avalonia.Platform; +using HarfBuzzSharp; +using SharpDX.DirectWrite; + +namespace Avalonia.Direct2D1.Media +{ + public class GlyphTypefaceImpl : IGlyphTypefaceImpl + { + private bool _isDisposed; + + public GlyphTypefaceImpl(Typeface typeface) + { + DWFont = Direct2D1FontCollectionCache.GetFont(typeface); + + FontFace = new FontFace(DWFont).QueryInterface(); + + Face = new Face(GetTable); + + Font = new HarfBuzzSharp.Font(Face); + + Font.SetFunctionsOpenType(); + + Font.GetScale(out var xScale, out _); + + DesignEmHeight = (short)xScale; + + if (!Font.TryGetHorizontalFontExtents(out var fontExtents)) + { + Font.TryGetVerticalFontExtents(out fontExtents); + } + + Ascent = -fontExtents.Ascender; + + Descent = -fontExtents.Descender; + + LineGap = fontExtents.LineGap; + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlinePosition)) + { + UnderlinePosition = underlinePosition; + } + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineThickness)) + { + UnderlineThickness = underlineThickness; + } + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutOffset, out var strikethroughPosition)) + { + StrikethroughPosition = strikethroughPosition; + } + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughThickness)) + { + StrikethroughThickness = strikethroughThickness; + } + + IsFixedPitch = FontFace.IsMonospacedFont; + } + + private Blob GetTable(Face face, Tag tag) + { + var dwTag = (int)SwapBytes(tag); + + if (FontFace.TryGetFontTable(dwTag, out var tableData, out _)) + { + return new Blob(tableData.Pointer, tableData.Size, MemoryMode.ReadOnly, () => { }); + } + + return null; + } + + private static uint SwapBytes(uint x) + { + x = (x >> 16) | (x << 16); + + return ((x & 0xFF00FF00) >> 8) | ((x & 0x00FF00FF) << 8); + } + + public SharpDX.DirectWrite.Font DWFont { get; } + + public FontFace1 FontFace { get; } + + public Face Face { get; } + + public HarfBuzzSharp.Font Font { get; } + + /// + public short DesignEmHeight { get; } + + /// + public int Ascent { get; } + + /// + public int Descent { get; } + + /// + public int LineGap { get; } + + //ToDo: Read font table for these values + /// + public int UnderlinePosition { get; } + + /// + public int UnderlineThickness { get; } + + /// + public int StrikethroughPosition { get; } + + /// + public int StrikethroughThickness { get; } + + /// + public bool IsFixedPitch { get; } + + /// + public ushort GetGlyph(uint codepoint) + { + if (Font.TryGetGlyph(codepoint, out var glyph)) + { + return (ushort)glyph; + } + + return 0; + } + + /// + public ushort[] GetGlyphs(ReadOnlySpan codepoints) + { + var glyphs = new ushort[codepoints.Length]; + + for (var i = 0; i < codepoints.Length; i++) + { + if (Font.TryGetGlyph(codepoints[i], out var glyph)) + { + glyphs[i] = (ushort)glyph; + } + } + + return glyphs; + } + + /// + public int GetGlyphAdvance(ushort glyph) + { + return Font.GetHorizontalGlyphAdvance(glyph); + } + + /// + public int[] GetGlyphAdvances(ReadOnlySpan glyphs) + { + var glyphIndices = new uint[glyphs.Length]; + + for (var i = 0; i < glyphs.Length; i++) + { + glyphIndices[i] = glyphs[i]; + } + + return Font.GetHorizontalGlyphAdvances(glyphIndices); + } + + private void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + + if (!disposing) + { + return; + } + + Font?.Dispose(); + Face?.Dispose(); + FontFace?.Dispose(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} + diff --git a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs index fbc6d21cb7..09b900b0c2 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs @@ -1,6 +1,3 @@ -// 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.Media; using Avalonia.Rendering.Utilities; using Avalonia.Utilities; @@ -107,7 +104,7 @@ namespace Avalonia.Direct2D1.Media context.PushClip(calc.IntermediateClip); context.Transform = calc.IntermediateTransform; - context.DrawImage(RefCountable.CreateUnownedNotClonable(bitmap), 1, rect, rect, _bitmapInterpolationMode); + context.DrawBitmap(RefCountable.CreateUnownedNotClonable(bitmap), 1, rect, rect, _bitmapInterpolationMode); context.PopClip(); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs index 1ee869ecb9..63676e30b5 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.IO; using SharpDX.WIC; using Bitmap = SharpDX.Direct2D1.Bitmap; @@ -30,7 +27,7 @@ namespace Avalonia.Direct2D1.Media _direct2DBitmap = d2DBitmap ?? throw new ArgumentNullException(nameof(d2DBitmap)); } - public override Vector Dpi => _direct2DBitmap.DotsPerInch.ToAvaloniaVector(); + public override Vector Dpi => new Vector(96, 96); public override PixelSize PixelSize => _direct2DBitmap.PixelSize.ToAvalonia(); public override void Dispose() @@ -58,3 +55,4 @@ namespace Avalonia.Direct2D1.Media } } } +; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs index 8ec368c999..3a3f9a9f7d 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs @@ -1,7 +1,4 @@ -// 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 System.IO; +using System.IO; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Utilities; @@ -58,7 +55,7 @@ namespace Avalonia.Direct2D1.Media.Imaging { using (var dc = wic.CreateDrawingContext(null)) { - dc.DrawImage( + dc.DrawBitmap( RefCountable.CreateUnownedNotClonable(this), 1, new Rect(PixelSize.ToSizeWithDpi(Dpi.X)), diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs index 176c3e0e23..743abddd1e 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs @@ -1,8 +1,6 @@ -// 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 System; using System.IO; +using System.Security.Cryptography; using Avalonia.Win32.Interop; using SharpDX.WIC; using APixelFormat = Avalonia.Platform.PixelFormat; @@ -17,6 +15,26 @@ namespace Avalonia.Direct2D1.Media { private BitmapDecoder _decoder; + private static BitmapInterpolationMode ConvertInterpolationMode(Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode interpolationMode) + { + switch (interpolationMode) + { + case Visuals.Media.Imaging.BitmapInterpolationMode.Default: + return BitmapInterpolationMode.Fant; + + case Visuals.Media.Imaging.BitmapInterpolationMode.LowQuality: + return BitmapInterpolationMode.NearestNeighbor; + + case Visuals.Media.Imaging.BitmapInterpolationMode.MediumQuality: + return BitmapInterpolationMode.Fant; + + default: + case Visuals.Media.Imaging.BitmapInterpolationMode.HighQuality: + return BitmapInterpolationMode.HighQualityCubic; + + } + } + /// /// Initializes a new instance of the class. /// @@ -26,9 +44,16 @@ namespace Avalonia.Direct2D1.Media using (BitmapDecoder decoder = new BitmapDecoder(Direct2D1Platform.ImagingFactory, fileName, DecodeOptions.CacheOnDemand)) { WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnDemand); + Dpi = new Vector(96, 96); } } + private WicBitmapImpl(Bitmap bmp) + { + WicImpl = bmp; + Dpi = new Vector(96, 96); + } + /// /// Initializes a new instance of the class. /// @@ -39,6 +64,7 @@ namespace Avalonia.Direct2D1.Media _decoder = new BitmapDecoder(Direct2D1Platform.ImagingFactory, stream, DecodeOptions.CacheOnLoad); WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, _decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnLoad); + Dpi = new Vector(96, 96); } /// @@ -61,7 +87,8 @@ namespace Avalonia.Direct2D1.Media size.Height, pixelFormat.Value.ToWic(), BitmapCreateCacheOption.CacheOnLoad); - WicImpl.SetResolution(dpi.X, dpi.Y); + + Dpi = dpi; } public WicBitmapImpl(APixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride) @@ -70,6 +97,8 @@ namespace Avalonia.Direct2D1.Media WicImpl.SetResolution(dpi.X, dpi.Y); PixelFormat = format; + Dpi = dpi; + using (var l = WicImpl.Lock(BitmapLockFlags.Write)) { for (var row = 0; row < size.Height; row++) @@ -82,15 +111,45 @@ namespace Avalonia.Direct2D1.Media } } - public override Vector Dpi + public WicBitmapImpl(Stream stream, int decodeSize, bool horizontal, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode interpolationMode) { - get + _decoder = new BitmapDecoder(Direct2D1Platform.ImagingFactory, stream, DecodeOptions.CacheOnLoad); + + var frame = _decoder.GetFrame(0); + + // now scale that to the size that we want + var realScale = horizontal ? ((double)frame.Size.Height / frame.Size.Width) : ((double)frame.Size.Width / frame.Size.Height); + + PixelSize desired; + + if (horizontal) + { + desired = new PixelSize(decodeSize, (int)(realScale * decodeSize)); + } + else + { + desired = new PixelSize((int)(realScale * decodeSize), decodeSize); + } + + if (frame.Size.Width != desired.Width || frame.Size.Height != desired.Height) + { + using (var scaler = new BitmapScaler(Direct2D1Platform.ImagingFactory)) + { + scaler.Initialize(frame, desired.Width, desired.Height, ConvertInterpolationMode(interpolationMode)); + + WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, scaler, BitmapCreateCacheOption.CacheOnLoad); + } + } + else { - WicImpl.GetResolution(out double x, out double y); - return new Vector(x, y); + WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, frame, BitmapCreateCacheOption.CacheOnLoad); } + + Dpi = new Vector(96, 96); } + public override Vector Dpi { get; } + public override PixelSize PixelSize => WicImpl.Size.ToAvalonia(); protected APixelFormat? PixelFormat { get; } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs index 88e669357f..e8e27f3f5d 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Platform; using Avalonia.Rendering; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs index 68f0f0e30d..4c906d62ad 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using Avalonia.Platform; using SharpDX.WIC; using PixelFormat = Avalonia.Platform.PixelFormat; diff --git a/src/Windows/Avalonia.Direct2D1/Media/LineGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/LineGeometryImpl.cs index 6b73fce309..4e020d4ae6 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/LineGeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/LineGeometryImpl.cs @@ -1,6 +1,3 @@ -// 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 SharpDX.Direct2D1; namespace Avalonia.Direct2D1.Media diff --git a/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs index 5369f84f46..0e63d4cc03 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs @@ -1,6 +1,3 @@ -// 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 System.Linq; using Avalonia.Media; diff --git a/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs index 45932a4760..1fca6d4e33 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs @@ -1,6 +1,3 @@ -// 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 System.Linq; using Avalonia.Media; diff --git a/src/Windows/Avalonia.Direct2D1/Media/RectangleGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/RectangleGeometryImpl.cs index 194de4dd14..8e5dc7a672 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/RectangleGeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/RectangleGeometryImpl.cs @@ -1,6 +1,3 @@ -// 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 SharpDX.Direct2D1; namespace Avalonia.Direct2D1.Media diff --git a/src/Windows/Avalonia.Direct2D1/Media/SolidColorBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/SolidColorBrushImpl.cs index 966bd19741..f93b4a2e08 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/SolidColorBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/SolidColorBrushImpl.cs @@ -1,6 +1,3 @@ -// 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.Media; namespace Avalonia.Direct2D1.Media diff --git a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs index fd2f14fc58..e1f7aad1b2 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Logging; using Avalonia.Media; @@ -85,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/StreamGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs index a07d5c1c72..9104be64b2 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs @@ -1,6 +1,3 @@ -// 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.Platform; using SharpDX.Direct2D1; diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs new file mode 100644 index 0000000000..2d2865e2b9 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -0,0 +1,116 @@ +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.Platform; +using Avalonia.Utility; +using HarfBuzzSharp; +using Buffer = HarfBuzzSharp.Buffer; + +namespace Avalonia.Direct2D1.Media +{ + internal class TextShaperImpl : ITextShaperImpl + { + public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat) + { + using (var buffer = new Buffer()) + { + buffer.ContentType = ContentType.Unicode; + + var breakCharPosition = text.Length - 1; + + var codepoint = Codepoint.ReadAt(text, breakCharPosition, out var count); + + if (codepoint.IsBreakChar) + { + var breakCharCount = 1; + + if (text.Length > 1) + { + var previousCodepoint = Codepoint.ReadAt(text, breakCharPosition - count, out _); + + if (codepoint == '\r' && previousCodepoint == '\n' + || codepoint == '\n' && previousCodepoint == '\r') + { + breakCharCount = 2; + } + } + + if (breakCharPosition != text.Start) + { + buffer.AddUtf16(text.Buffer.Span.Slice(0, text.Length - breakCharCount)); + } + + var cluster = buffer.GlyphInfos.Length > 0 ? + buffer.GlyphInfos[buffer.Length - 1].Cluster + 1 : + (uint)text.Start; + + switch (breakCharCount) + { + case 1: + buffer.Add('\u200C', cluster); + break; + case 2: + buffer.Add('\u200C', cluster); + buffer.Add('\u200D', cluster); + break; + } + } + else + { + buffer.AddUtf16(text.Buffer.Span); + } + + buffer.GuessSegmentProperties(); + + var glyphTypeface = textFormat.Typeface.GlyphTypeface; + + var font = ((GlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font; + + font.Shape(buffer); + + font.GetScale(out var scaleX, out _); + + var textScale = textFormat.FontRenderingEmSize / scaleX; + + var len = buffer.Length; + + var info = buffer.GetGlyphInfoSpan(); + + var pos = buffer.GetGlyphPositionSpan(); + + var glyphIndices = new ushort[len]; + + var clusters = new ushort[len]; + + var glyphAdvances = new double[len]; + + var glyphOffsets = new Vector[len]; + + for (var i = 0; i < len; i++) + { + glyphIndices[i] = (ushort)info[i].Codepoint; + + clusters[i] = (ushort)(text.Start + info[i].Cluster); + + var advanceX = pos[i].XAdvance * textScale; + // Depends on direction of layout + //var advanceY = pos[i].YAdvance * textScale; + + glyphAdvances[i] = advanceX; + + var offsetX = pos[i].XOffset * textScale; + var offsetY = pos[i].YOffset * textScale; + + glyphOffsets[i] = new Vector(offsetX, offsetY); + } + + return new GlyphRun(glyphTypeface, textFormat.FontRenderingEmSize, + new ReadOnlySlice(glyphIndices), + new ReadOnlySlice(glyphAdvances), + new ReadOnlySlice(glyphOffsets), + text, + new ReadOnlySlice(clusters)); + } + } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs index c00a826d0c..fe274701bf 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs @@ -1,6 +1,3 @@ -// 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.Platform; using SharpDX.Direct2D1; diff --git a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs index 065895859d..18304ba211 100644 --- a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs +++ b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Linq; using SharpDX; diff --git a/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs b/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs index a09d5c2d1c..5d53fd4aae 100644 --- a/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs +++ b/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs @@ -1,10 +1,11 @@ -// 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 System.Reflection; +using System.Runtime.CompilerServices; using Avalonia.Platform; using Avalonia.Direct2D1; [assembly: ExportRenderingSubsystem(OperatingSystemType.WinNT, 1, "Direct2D1", typeof(Direct2D1Platform), nameof(Direct2D1Platform.Initialize), typeof(Direct2DChecker))] +[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")] +[assembly: InternalsVisibleTo("Avalonia.Direct2D1.UnitTests")] + diff --git a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs index a21b5571c6..4433c8d50a 100644 --- a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs @@ -1,6 +1,3 @@ -// 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.Direct2D1.Media; using Avalonia.Direct2D1.Media.Imaging; using Avalonia.Platform; diff --git a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs index ffa0f5c8bc..5f4d06dab0 100644 --- a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs @@ -1,7 +1,4 @@ -// 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.Direct2D1.Media; +using Avalonia.Direct2D1.Media; using Avalonia.Direct2D1.Media.Imaging; using Avalonia.Platform; using Avalonia.Rendering; diff --git a/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs b/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs index fe626f4d38..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, InputModifiers.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 71c398481b..f8366abb81 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -5,6 +5,7 @@ using System.Windows; using System.Windows.Input; using System.Windows.Interop; using System.Windows.Media; +using Avalonia.Controls; using Avalonia.Controls.Embedding; using Avalonia.Input; using Avalonia.Input.Raw; @@ -17,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; @@ -212,17 +213,17 @@ namespace Avalonia.Win32.Interop.Wpf protected override void OnMouseLeave(MouseEventArgs e) => MouseEvent(RawPointerEventType.LeaveWindow, e); protected override void OnKeyDown(KeyEventArgs e) - => _ttl.Input?.Invoke(new RawKeyEventArgs(_keyboard, (uint) e.Timestamp, RawKeyEventType.KeyDown, + => _ttl.Input?.Invoke(new RawKeyEventArgs(_keyboard, (uint) e.Timestamp, _inputRoot, RawKeyEventType.KeyDown, (Key) e.Key, GetModifiers(null))); protected override void OnKeyUp(KeyEventArgs e) - => _ttl.Input?.Invoke(new RawKeyEventArgs(_keyboard, (uint)e.Timestamp, RawKeyEventType.KeyUp, + => _ttl.Input?.Invoke(new RawKeyEventArgs(_keyboard, (uint)e.Timestamp, _inputRoot, RawKeyEventType.KeyUp, (Key)e.Key, GetModifiers(null))); protected override void OnTextInput(TextCompositionEventArgs e) - => _ttl.Input?.Invoke(new RawTextInputEventArgs(_keyboard, (uint) e.Timestamp, e.Text)); + => _ttl.Input?.Invoke(new RawTextInputEventArgs(_keyboard, (uint) e.Timestamp, _inputRoot, e.Text)); void ITopLevelImpl.SetCursor(IPlatformHandle cursor) { @@ -236,8 +237,11 @@ namespace Avalonia.Win32.Interop.Wpf Action ITopLevelImpl.Paint { get; set; } Action ITopLevelImpl.Resized { get; set; } Action ITopLevelImpl.ScalingChanged { get; set; } + + Action ITopLevelImpl.TransparencyLevelChanged { get; set; } + Action ITopLevelImpl.Closed { get; set; } - public new event Action LostFocus; + public new Action LostFocus { get; set; } internal Vector GetScaling() { @@ -248,5 +252,9 @@ namespace Avalonia.Win32.Interop.Wpf } public IPopupImpl CreatePopup() => null; + + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { } + + public WindowTransparencyLevel TransparencyLevel { get; private set; } } } 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 ad000f11eb..7d9e0a8bd2 100644 --- a/src/Windows/Avalonia.Win32/ClipboardImpl.cs +++ b/src/Windows/Avalonia.Win32/ClipboardImpl.cs @@ -1,28 +1,30 @@ -// 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 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) @@ -40,10 +42,6 @@ namespace Avalonia.Win32 UnmanagedMethods.GlobalUnlock(hText); return rv; } - finally - { - UnmanagedMethods.CloseClipboard(); - } } public async Task SetTextAsync(string text) @@ -53,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) + { + 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) { - UnmanagedMethods.CloseClipboard(); + 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/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index b45138c27a..8c40eaa7b8 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using Avalonia.Input; diff --git a/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs b/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs index 538a51c01e..e08f1b28be 100644 --- a/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs +++ b/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs @@ -1,7 +1,4 @@ -// 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 System; +using System; using System.ComponentModel; using System.Runtime.InteropServices; using Avalonia.Platform; @@ -9,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( @@ -25,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/Input/KeyInterop.cs b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs index 11c0a6dca9..f5b2d462ab 100644 --- a/src/Windows/Avalonia.Win32/Input/KeyInterop.cs +++ b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs @@ -1,8 +1,4 @@ -// 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 System.Collections.Generic; -using System.Text; using Avalonia.Input; using Avalonia.Win32.Interop; @@ -211,7 +207,7 @@ namespace Avalonia.Win32.Input { 31, Key.ImeModeChange }, { 32, Key.Space }, { 33, Key.PageUp }, - { 34, Key.Next }, + { 34, Key.PageDown }, { 35, Key.End }, { 36, Key.Home }, { 37, Key.Left }, @@ -364,17 +360,80 @@ namespace Avalonia.Win32.Input { 254, Key.OemClear }, }; - public static Key KeyFromVirtualKey(int virtualKey) + /// + /// Indicates whether the key is an extended key, such as the right-hand ALT and CTRL keys. + /// According to https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keydown. + /// + private static bool IsExtended(int keyData) + { + const int extendedMask = 1 << 24; + + return (keyData & extendedMask) != 0; + } + + private static int GetVirtualKey(int virtualKey, int keyData) { - Key result; - s_keyFromVirtualKey.TryGetValue(virtualKey, out result); + // Adapted from https://github.com/dotnet/wpf/blob/master/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/InterOp/HwndKeyboardInputProvider.cs. + + if (virtualKey == (int)UnmanagedMethods.VirtualKeyStates.VK_SHIFT) + { + // Bits from 16 to 23 represent scan code. + const int scanCodeMask = 0xFF0000; + + var scanCode = (keyData & scanCodeMask) >> 16; + + virtualKey = (int)UnmanagedMethods.MapVirtualKey((uint)scanCode, (uint)UnmanagedMethods.MapVirtualKeyMapTypes.MAPVK_VSC_TO_VK_EX); + + if (virtualKey == 0) + { + virtualKey = (int)UnmanagedMethods.VirtualKeyStates.VK_LSHIFT; + } + } + + if (virtualKey == (int)UnmanagedMethods.VirtualKeyStates.VK_MENU) + { + bool isRight = IsExtended(keyData); + + if (isRight) + { + virtualKey = (int)UnmanagedMethods.VirtualKeyStates.VK_RMENU; + } + else + { + virtualKey = (int)UnmanagedMethods.VirtualKeyStates.VK_LMENU; + } + } + + if (virtualKey == (int)UnmanagedMethods.VirtualKeyStates.VK_CONTROL) + { + bool isRight = IsExtended(keyData); + + if (isRight) + { + virtualKey = (int)UnmanagedMethods.VirtualKeyStates.VK_RCONTROL; + } + else + { + virtualKey = (int)UnmanagedMethods.VirtualKeyStates.VK_LCONTROL; + } + } + + return virtualKey; + } + + public static Key KeyFromVirtualKey(int virtualKey, int keyData) + { + virtualKey = GetVirtualKey(virtualKey, keyData); + + s_keyFromVirtualKey.TryGetValue(virtualKey, out var result); + return result; } public static int VirtualKeyFromKey(Key key) { - int result; - s_virtualKeyFromKey.TryGetValue(key, out result); + s_virtualKeyFromKey.TryGetValue(key, out var result); + return result; } } diff --git a/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs index fda5483b00..1258bb0109 100644 --- a/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs +++ b/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs @@ -1,6 +1,3 @@ -// 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 System.Text; using Avalonia.Controls; using Avalonia.Input; @@ -47,7 +44,7 @@ namespace Avalonia.Win32.Input public void WindowActivated(Window window) { - SetFocusedElement(window, NavigationMethod.Unspecified, InputModifiers.None); + SetFocusedElement(window, NavigationMethod.Unspecified, KeyModifiers.None); } public string StringFromVirtualKey(uint virtualKey) diff --git a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs index e7c379ad89..b1064ae25d 100644 --- a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs +++ b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Controls; using Avalonia.Input; @@ -11,19 +8,11 @@ namespace Avalonia.Win32.Input { class WindowsMouseDevice : MouseDevice { - public static WindowsMouseDevice Instance { get; } = new WindowsMouseDevice(); - public WindowsMouseDevice() : base(new WindowsMousePointer()) { } - public WindowImpl CurrentWindow - { - get; - set; - } - class WindowsMousePointer : Pointer { public WindowsMousePointer() : base(Pointer.GetNextFreeId(),PointerType.Mouse, true) diff --git a/src/Windows/Avalonia.Win32/Interop/TaskBarList.cs b/src/Windows/Avalonia.Win32/Interop/TaskBarList.cs new file mode 100644 index 0000000000..1b01ebbe7f --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/TaskBarList.cs @@ -0,0 +1,55 @@ +using System; +using System.Runtime.InteropServices; +using static Avalonia.Win32.Interop.UnmanagedMethods; + +namespace Avalonia.Win32.Interop +{ + internal class TaskBarList + { + private static IntPtr s_taskBarList; + private static HrInit s_hrInitDelegate; + private static MarkFullscreenWindow s_markFullscreenWindowDelegate; + + /// + /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc + /// + /// Fullscreen state. + public static unsafe void MarkFullscreen(IntPtr hwnd, bool fullscreen) + { + if (s_taskBarList == IntPtr.Zero) + { + Guid clsid = ShellIds.TaskBarList; + Guid iid = ShellIds.ITaskBarList2; + + int result = CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out s_taskBarList); + + if (s_taskBarList != IntPtr.Zero) + { + var ptr = (ITaskBarList2VTable**)s_taskBarList.ToPointer(); + + if (s_hrInitDelegate is null) + { + s_hrInitDelegate = Marshal.GetDelegateForFunctionPointer((*ptr)->HrInit); + } + + if (s_hrInitDelegate(s_taskBarList) != HRESULT.S_OK) + { + s_taskBarList = IntPtr.Zero; + } + } + } + + if (s_taskBarList != IntPtr.Zero) + { + var ptr = (ITaskBarList2VTable**)s_taskBarList.ToPointer(); + + if (s_markFullscreenWindowDelegate is null) + { + s_markFullscreenWindowDelegate = Marshal.GetDelegateForFunctionPointer((*ptr)->MarkFullscreenWindow); + } + + s_markFullscreenWindowDelegate(s_taskBarList, hwnd, fullscreen); + } + } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index d37bec3334..b3b38db1ab 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -18,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); @@ -241,6 +238,170 @@ namespace Avalonia.Win32.Interop MK_XBUTTON2 = 0x0040 } + public enum VirtualKeyStates : int + { + VK_LBUTTON = 0x01, + VK_RBUTTON = 0x02, + VK_CANCEL = 0x03, + VK_MBUTTON = 0x04, + VK_XBUTTON1 = 0x05, + VK_XBUTTON2 = 0x06, + VK_BACK = 0x08, + VK_TAB = 0x09, + VK_CLEAR = 0x0C, + VK_RETURN = 0x0D, + VK_SHIFT = 0x10, + VK_CONTROL = 0x11, + VK_MENU = 0x12, + VK_PAUSE = 0x13, + VK_CAPITAL = 0x14, + VK_KANA = 0x15, + VK_HANGEUL = 0x15, + VK_HANGUL = 0x15, + VK_JUNJA = 0x17, + VK_FINAL = 0x18, + VK_HANJA = 0x19, + VK_KANJI = 0x19, + VK_ESCAPE = 0x1B, + VK_CONVERT = 0x1C, + VK_NONCONVERT = 0x1D, + VK_ACCEPT = 0x1E, + VK_MODECHANGE = 0x1F, + VK_SPACE = 0x20, + VK_PRIOR = 0x21, + VK_NEXT = 0x22, + VK_END = 0x23, + VK_HOME = 0x24, + VK_LEFT = 0x25, + VK_UP = 0x26, + VK_RIGHT = 0x27, + VK_DOWN = 0x28, + VK_SELECT = 0x29, + VK_PRINT = 0x2A, + VK_EXECUTE = 0x2B, + VK_SNAPSHOT = 0x2C, + VK_INSERT = 0x2D, + VK_DELETE = 0x2E, + VK_HELP = 0x2F, + VK_LWIN = 0x5B, + VK_RWIN = 0x5C, + VK_APPS = 0x5D, + VK_SLEEP = 0x5F, + VK_NUMPAD0 = 0x60, + VK_NUMPAD1 = 0x61, + VK_NUMPAD2 = 0x62, + VK_NUMPAD3 = 0x63, + VK_NUMPAD4 = 0x64, + VK_NUMPAD5 = 0x65, + VK_NUMPAD6 = 0x66, + VK_NUMPAD7 = 0x67, + VK_NUMPAD8 = 0x68, + VK_NUMPAD9 = 0x69, + VK_MULTIPLY = 0x6A, + VK_ADD = 0x6B, + VK_SEPARATOR = 0x6C, + VK_SUBTRACT = 0x6D, + VK_DECIMAL = 0x6E, + VK_DIVIDE = 0x6F, + VK_F1 = 0x70, + VK_F2 = 0x71, + VK_F3 = 0x72, + VK_F4 = 0x73, + VK_F5 = 0x74, + VK_F6 = 0x75, + VK_F7 = 0x76, + VK_F8 = 0x77, + VK_F9 = 0x78, + VK_F10 = 0x79, + VK_F11 = 0x7A, + VK_F12 = 0x7B, + VK_F13 = 0x7C, + VK_F14 = 0x7D, + VK_F15 = 0x7E, + VK_F16 = 0x7F, + VK_F17 = 0x80, + VK_F18 = 0x81, + VK_F19 = 0x82, + VK_F20 = 0x83, + VK_F21 = 0x84, + VK_F22 = 0x85, + VK_F23 = 0x86, + VK_F24 = 0x87, + VK_NUMLOCK = 0x90, + VK_SCROLL = 0x91, + VK_OEM_NEC_EQUAL = 0x92, + VK_OEM_FJ_JISHO = 0x92, + VK_OEM_FJ_MASSHOU = 0x93, + VK_OEM_FJ_TOUROKU = 0x94, + VK_OEM_FJ_LOYA = 0x95, + VK_OEM_FJ_ROYA = 0x96, + VK_LSHIFT = 0xA0, + VK_RSHIFT = 0xA1, + VK_LCONTROL = 0xA2, + VK_RCONTROL = 0xA3, + VK_LMENU = 0xA4, + VK_RMENU = 0xA5, + VK_BROWSER_BACK = 0xA6, + VK_BROWSER_FORWARD = 0xA7, + VK_BROWSER_REFRESH = 0xA8, + VK_BROWSER_STOP = 0xA9, + VK_BROWSER_SEARCH = 0xAA, + VK_BROWSER_FAVORITES = 0xAB, + VK_BROWSER_HOME = 0xAC, + VK_VOLUME_MUTE = 0xAD, + VK_VOLUME_DOWN = 0xAE, + VK_VOLUME_UP = 0xAF, + VK_MEDIA_NEXT_TRACK = 0xB0, + VK_MEDIA_PREV_TRACK = 0xB1, + VK_MEDIA_STOP = 0xB2, + VK_MEDIA_PLAY_PAUSE = 0xB3, + VK_LAUNCH_MAIL = 0xB4, + VK_LAUNCH_MEDIA_SELECT = 0xB5, + VK_LAUNCH_APP1 = 0xB6, + VK_LAUNCH_APP2 = 0xB7, + VK_OEM_1 = 0xBA, + VK_OEM_PLUS = 0xBB, + VK_OEM_COMMA = 0xBC, + VK_OEM_MINUS = 0xBD, + VK_OEM_PERIOD = 0xBE, + VK_OEM_2 = 0xBF, + VK_OEM_3 = 0xC0, + VK_OEM_4 = 0xDB, + VK_OEM_5 = 0xDC, + VK_OEM_6 = 0xDD, + VK_OEM_7 = 0xDE, + VK_OEM_8 = 0xDF, + VK_OEM_AX = 0xE1, + VK_OEM_102 = 0xE2, + VK_ICO_HELP = 0xE3, + VK_ICO_00 = 0xE4, + VK_PROCESSKEY = 0xE5, + VK_ICO_CLEAR = 0xE6, + VK_PACKET = 0xE7, + VK_OEM_RESET = 0xE9, + VK_OEM_JUMP = 0xEA, + VK_OEM_PA1 = 0xEB, + VK_OEM_PA2 = 0xEC, + VK_OEM_PA3 = 0xED, + VK_OEM_WSCTRL = 0xEE, + VK_OEM_CUSEL = 0xEF, + VK_OEM_ATTN = 0xF0, + VK_OEM_FINISH = 0xF1, + VK_OEM_COPY = 0xF2, + VK_OEM_AUTO = 0xF3, + VK_OEM_ENLW = 0xF4, + VK_OEM_BACKTAB = 0xF5, + VK_ATTN = 0xF6, + VK_CRSEL = 0xF7, + VK_EXSEL = 0xF8, + VK_EREOF = 0xF9, + VK_PLAY = 0xFA, + VK_ZOOM = 0xFB, + VK_NONAME = 0xFC, + VK_PA1 = 0xFD, + VK_OEM_CLEAR = 0xFE + } + public enum WindowActivate { WA_INACTIVE, @@ -299,6 +460,7 @@ namespace Avalonia.Win32.Interop WS_SIZEFRAME = 0x40000, WS_SYSMENU = 0x80000, WS_TABSTOP = 0x10000, + WS_THICKFRAME = 0x40000, WS_VISIBLE = 0x10000000, WS_VSCROLL = 0x200000, WS_EX_DLGMODALFRAME = 0x00000001, @@ -581,6 +743,14 @@ namespace Avalonia.Win32.Interop WM_DISPATCH_WORK_ITEM = WM_USER, } + public enum MapVirtualKeyMapTypes : uint + { + MAPVK_VK_TO_VSC = 0x00, + MAPVK_VSC_TO_VK = 0x01, + MAPVK_VK_TO_CHAR = 0x02, + MAPVK_VSC_TO_VK_EX = 0x03, + } + public enum BitmapCompressionMode : uint { BI_RGB = 0, @@ -756,6 +926,9 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool GetKeyboardState(byte[] lpKeyState); + [DllImport("user32.dll", EntryPoint = "MapVirtualKeyW")] + public static extern uint MapVirtualKey(uint uCode, uint uMapType); + [DllImport("user32.dll", EntryPoint = "GetMessageW")] public static extern sbyte GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); @@ -828,6 +1001,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); @@ -878,6 +1055,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)] @@ -974,7 +1153,10 @@ namespace Avalonia.Win32.Interop internal static extern int CoCreateInstance(ref Guid clsid, IntPtr ignore1, int ignore2, ref Guid iid, [MarshalAs(UnmanagedType.IUnknown), Out] out object pUnkOuter); - + [DllImport("ole32.dll", PreserveSig = true)] + internal static extern int CoCreateInstance(ref Guid clsid, + IntPtr ignore1, int ignore2, ref Guid iid, [Out] out IntPtr pUnkOuter); + [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string pszPath, IntPtr pbc, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppv); @@ -993,6 +1175,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); @@ -1031,6 +1219,9 @@ namespace Avalonia.Win32.Interop [DllImport("shcore.dll")] public static extern long GetDpiForMonitor(IntPtr hmonitor, MONITOR_DPI_TYPE dpiType, out uint dpiX, out uint dpiY); + [DllImport("gdi32.dll")] + public static extern int GetDeviceCaps(IntPtr hdc, DEVICECAP nIndex); + [DllImport("shcore.dll")] public static extern void GetScaleFactorForMonitor(IntPtr hMon, out uint pScale); @@ -1095,7 +1286,10 @@ namespace Avalonia.Win32.Interop [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] public static extern HRESULT RegisterDragDrop(IntPtr hwnd, IDropTarget target); - + + [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] + public static extern HRESULT RevokeDragDrop(IntPtr hwnd); + [DllImport("ole32.dll", EntryPoint = "OleInitialize")] public static extern HRESULT OleInitialize(IntPtr val); @@ -1117,7 +1311,123 @@ namespace Avalonia.Win32.Interop [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true, PreserveSig = false)] internal static extern void DoDragDrop(IOleDataObject dataObject, IDropSource dropSource, int allowedEffects, out int finalEffect); + [DllImport("dwmapi.dll")] + public static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins); + + [DllImport("dwmapi.dll")] + public static extern int DwmIsCompositionEnabled(out bool enabled); + + [DllImport("dwmapi.dll")] + public static extern void DwmEnableBlurBehindWindow(IntPtr hwnd, ref DWM_BLURBEHIND blurBehind); + + [Flags] + public enum DWM_BB + { + Enable = 1, + BlurRegion = 2, + TransitionMaximized = 4 + } + + [StructLayout(LayoutKind.Sequential)] + public struct DWM_BLURBEHIND + { + public DWM_BB dwFlags; + public bool fEnable; + public IntPtr hRgnBlur; + public bool fTransitionOnMaximized; + + public DWM_BLURBEHIND(bool enabled) + { + fEnable = enabled ? true : false; + hRgnBlur = IntPtr.Zero; + fTransitionOnMaximized = false; + dwFlags = DWM_BB.Enable; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct RTL_OSVERSIONINFOEX + { + internal uint dwOSVersionInfoSize; + internal uint dwMajorVersion; + internal uint dwMinorVersion; + internal uint dwBuildNumber; + internal uint dwPlatformId; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + internal string szCSDVersion; + } + + [DllImport("ntdll")] + private static extern int RtlGetVersion(out RTL_OSVERSIONINFOEX lpVersionInformation); + + internal static Version RtlGetVersion() + { + RTL_OSVERSIONINFOEX v = new RTL_OSVERSIONINFOEX(); + v.dwOSVersionInfoSize = (uint)Marshal.SizeOf(v); + if (RtlGetVersion(out v) == 0) + { + return new Version((int)v.dwMajorVersion, (int)v.dwMinorVersion, (int)v.dwBuildNumber, (int)v.dwPlatformId); + } + else + { + throw new Exception("RtlGetVersion failed!"); + } + } + + [DllImport("user32.dll")] + internal static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data); + + [StructLayout(LayoutKind.Sequential)] + internal struct WindowCompositionAttributeData + { + public WindowCompositionAttribute Attribute; + public IntPtr Data; + public int SizeOfData; + } + + internal enum WindowCompositionAttribute + { + // ... + WCA_ACCENT_POLICY = 19 + // ... + } + + internal enum AccentState + { + ACCENT_DISABLED = 0, + ACCENT_ENABLE_GRADIENT = 1, + ACCENT_ENABLE_TRANSPARENTGRADIENT = 2, + ACCENT_ENABLE_BLURBEHIND = 3, + ACCENT_ENABLE_ACRYLIC = 4, //1703 and above + ACCENT_ENABLE_HOSTBACKDROP = 5, // RS5 1809 + ACCENT_INVALID_STATE = 6 + } + + internal enum AccentFlags + { + DrawLeftBorder = 0x20, + DrawTopBorder = 0x40, + DrawRightBorder = 0x80, + DrawBottomBorder = 0x100, + } + + [StructLayout(LayoutKind.Sequential)] + internal struct AccentPolicy + { + public AccentState AccentState; + public int AccentFlags; + public int GradientColor; + public int AnimationId; + } + [StructLayout(LayoutKind.Sequential)] + internal struct MARGINS + { + public int cxLeftWidth; + public int cxRightWidth; + public int cyTopHeight; + public int cyBottomHeight; + } public enum MONITOR { @@ -1147,6 +1457,12 @@ namespace Avalonia.Win32.Interop } } + public enum DEVICECAP + { + HORZRES = 8, + DESKTOPHORZRES = 118 + } + public enum PROCESS_DPI_AWARENESS { PROCESS_DPI_UNAWARE = 0, @@ -1448,6 +1764,8 @@ namespace Avalonia.Win32.Interop public static readonly Guid SaveFileDialog = Guid.Parse("C0B4E2F3-BA21-4773-8DBA-335EC946EB8B"); public static readonly Guid IFileDialog = Guid.Parse("42F85136-DB7E-439C-85F1-E4075D135FC8"); public static readonly Guid IShellItem = Guid.Parse("43826D1E-E718-42EE-BC55-A1E261C37BFE"); + public static readonly Guid TaskBarList = Guid.Parse("56FDF344-FD6D-11D0-958A-006097C9A090"); + public static readonly Guid ITaskBarList2 = Guid.Parse("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf"); } [ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] @@ -1680,6 +1998,22 @@ namespace Avalonia.Win32.Interop [MarshalAs(UnmanagedType.LPWStr)] public string pszSpec; } + + public delegate void MarkFullscreenWindow(IntPtr This, IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fullscreen); + public delegate HRESULT HrInit(IntPtr This); + + public struct ITaskBarList2VTable + { + public IntPtr IUnknown1; + public IntPtr IUnknown2; + public IntPtr IUnknown3; + public IntPtr HrInit; + public IntPtr AddTab; + public IntPtr DeleteTab; + public IntPtr ActivateTab; + public IntPtr SetActiveAlt; + public IntPtr MarkFullscreenWindow; + } } [Flags] 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/OleContext.cs b/src/Windows/Avalonia.Win32/OleContext.cs index d454c797fa..c6e04a29b4 100644 --- a/src/Windows/Avalonia.Win32/OleContext.cs +++ b/src/Windows/Avalonia.Win32/OleContext.cs @@ -7,9 +7,9 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32 { - class OleContext + internal class OleContext { - private static OleContext fCurrent; + private static OleContext s_current; internal static OleContext Current { @@ -18,13 +18,12 @@ namespace Avalonia.Win32 if (!IsValidOleThread()) return null; - if (fCurrent == null) - fCurrent = new OleContext(); - return fCurrent; + if (s_current == null) + s_current = new OleContext(); + return s_current; } } - private OleContext() { UnmanagedMethods.HRESULT res = UnmanagedMethods.OleInitialize(IntPtr.Zero); @@ -43,9 +42,21 @@ namespace Avalonia.Win32 internal bool RegisterDragDrop(IPlatformHandle hwnd, IDropTarget target) { if (hwnd?.HandleDescriptor != "HWND" || target == null) + { return false; + } return UnmanagedMethods.RegisterDragDrop(hwnd.Handle, target) == UnmanagedMethods.HRESULT.S_OK; } + + internal bool UnregisterDragDrop(IPlatformHandle hwnd) + { + if (hwnd?.HandleDescriptor != "HWND") + { + return false; + } + + return UnmanagedMethods.RevokeDragDrop(hwnd.Handle) == UnmanagedMethods.HRESULT.S_OK; + } } } diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index 8ac0f25598..37d047689c 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -6,15 +6,15 @@ using IDataObject = Avalonia.Input.IDataObject; namespace Avalonia.Win32 { - class OleDropTarget : IDropTarget + internal class OleDropTarget : IDropTarget { - private readonly IInputElement _target; + private readonly IInputRoot _target; private readonly ITopLevelImpl _tl; private readonly IDragDropDevice _dragDevice; private IDataObject _currentDrag = null; - public OleDropTarget(ITopLevelImpl tl, IInputElement target) + public OleDropTarget(ITopLevelImpl tl, IInputRoot target) { _dragDevice = AvaloniaLocator.Current.GetService(); _tl = tl; diff --git a/src/Windows/Avalonia.Win32/PlatformConstants.cs b/src/Windows/Avalonia.Win32/PlatformConstants.cs index ba89c5bee6..af1eca29be 100644 --- a/src/Windows/Avalonia.Win32/PlatformConstants.cs +++ b/src/Windows/Avalonia.Win32/PlatformConstants.cs @@ -1,6 +1,3 @@ -// 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. - namespace Avalonia.Win32 { static class PlatformConstants diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index c9aa1ce4e7..cd25b32ed9 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -1,6 +1,3 @@ -// 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 System; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Platform; @@ -10,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) @@ -34,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; } @@ -52,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: @@ -59,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)); } @@ -71,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/Properties/AssemblyInfo.cs b/src/Windows/Avalonia.Win32/Properties/AssemblyInfo.cs index 38ec891f76..30a3f71cad 100644 --- a/src/Windows/Avalonia.Win32/Properties/AssemblyInfo.cs +++ b/src/Windows/Avalonia.Win32/Properties/AssemblyInfo.cs @@ -1,6 +1,3 @@ -// 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.Platform; using Avalonia.Win32; diff --git a/src/Windows/Avalonia.Win32/ScreenImpl.cs b/src/Windows/Avalonia.Win32/ScreenImpl.cs index e77aa07bcd..442794f0f0 100644 --- a/src/Windows/Avalonia.Win32/ScreenImpl.cs +++ b/src/Windows/Avalonia.Win32/ScreenImpl.cs @@ -1,16 +1,14 @@ -// 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 System; +using System; using System.Collections.Generic; using Avalonia.Platform; +using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32 { public class ScreenImpl : IScreenImpl { - public int ScreenCount + public int ScreenCount { get => GetSystemMetrics(SystemMetric.SM_CMONITORS); } @@ -28,19 +26,35 @@ namespace Avalonia.Win32 (IntPtr monitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr data) => { MONITORINFO monitorInfo = MONITORINFO.Create(); - if (GetMonitorInfo(monitor,ref monitorInfo)) + if (GetMonitorInfo(monitor, ref monitorInfo)) { - GetDpiForMonitor(monitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var x, out _); + var dpi = 1.0; + + var shcore = LoadLibrary("shcore.dll"); + var method = GetProcAddress(shcore, nameof(GetDpiForMonitor)); + if (method != IntPtr.Zero) + { + GetDpiForMonitor(monitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var x, out _); + dpi = (double)x; + } + else + { + var hdc = GetDC(IntPtr.Zero); + + double virtW = GetDeviceCaps(hdc, DEVICECAP.HORZRES); + double physW = GetDeviceCaps(hdc, DEVICECAP.DESKTOPHORZRES); + + dpi = (96d * physW / virtW); + + ReleaseDC(IntPtr.Zero, hdc); + } RECT bounds = monitorInfo.rcMonitor; RECT workingArea = monitorInfo.rcWork; - PixelRect avaloniaBounds = new PixelRect(bounds.left, bounds.top, bounds.right - bounds.left, - bounds.bottom - bounds.top); - PixelRect avaloniaWorkArea = - new PixelRect(workingArea.left, workingArea.top, workingArea.right - workingArea.left, - workingArea.bottom - workingArea.top); + PixelRect avaloniaBounds = bounds.ToPixelRect(); + PixelRect avaloniaWorkArea = workingArea.ToPixelRect(); screens[index] = - new WinScreen((double)x / 96.0d, avaloniaBounds, avaloniaWorkArea, monitorInfo.dwFlags == 1, + new WinScreen(dpi / 96.0d, avaloniaBounds, avaloniaWorkArea, monitorInfo.dwFlags == 1, monitor); index++; } diff --git a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs index c452290afc..209b9e022a 100644 --- a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs +++ b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs @@ -5,7 +5,6 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; -using Avalonia.Platform; using Avalonia.Win32.Interop; namespace Avalonia.Win32 @@ -16,16 +15,16 @@ namespace Avalonia.Win32 private const UnmanagedMethods.FOS DefaultDialogOptions = UnmanagedMethods.FOS.FOS_FORCEFILESYSTEM | UnmanagedMethods.FOS.FOS_NOVALIDATE | UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT; - public unsafe Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + public unsafe Task ShowFileDialogAsync(FileDialog dialog, Window parent) { - var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero; + var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero; return Task.Factory.StartNew(() => { var result = Array.Empty(); Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog; Guid iid = UnmanagedMethods.ShellIds.IFileDialog; - UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk); + UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out object unk); var frm = (UnmanagedMethods.IFileDialog)unk; var openDialog = dialog as OpenFileDialog; @@ -57,11 +56,11 @@ namespace Avalonia.Win32 frm.SetFileTypes((uint)filters.Count, filters.ToArray()); frm.SetFileTypeIndex(0); - if (dialog.InitialDirectory != null) + if (dialog.Directory != null) { UnmanagedMethods.IShellItem directoryShellItem; Guid riid = UnmanagedMethods.ShellIds.IShellItem; - if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.InitialDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) + if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.Directory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) { frm.SetFolder(directoryShellItem); frm.SetDefaultFolder(directoryShellItem); @@ -98,38 +97,38 @@ namespace Avalonia.Win32 }); } - public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + public Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) { return Task.Factory.StartNew(() => { string result = string.Empty; - var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero; + var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero; Guid clsid = UnmanagedMethods.ShellIds.OpenFileDialog; - Guid iid = UnmanagedMethods.ShellIds.IFileDialog; + Guid iid = UnmanagedMethods.ShellIds.IFileDialog; - UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk); + UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out object unk); var frm = (UnmanagedMethods.IFileDialog)unk; uint options; frm.GetOptions(out options); options |= (uint)(UnmanagedMethods.FOS.FOS_PICKFOLDERS | DefaultDialogOptions); frm.SetOptions(options); - if (dialog.InitialDirectory != null) + if (dialog.Directory != null) { UnmanagedMethods.IShellItem directoryShellItem; Guid riid = UnmanagedMethods.ShellIds.IShellItem; - if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.InitialDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) + if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.Directory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) { frm.SetFolder(directoryShellItem); } } - if (dialog.DefaultDirectory != null) + if (dialog.Directory != null) { UnmanagedMethods.IShellItem directoryShellItem; Guid riid = UnmanagedMethods.ShellIds.IShellItem; - if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.DefaultDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) + if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.Directory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) { frm.SetDefaultFolder(directoryShellItem); } diff --git a/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs b/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs new file mode 100644 index 0000000000..69fbe30068 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs @@ -0,0 +1,201 @@ +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 Hide() + { + UnmanagedMethods.SetWindowPos(_holder.Handle, IntPtr.Zero, + -100, -100, 1, 1, + UnmanagedMethods.SetWindowPosFlags.SWP_HIDEWINDOW | + UnmanagedMethods.SetWindowPosFlags.SWP_NOACTIVATE); + } + + public unsafe void ShowInBounds(TransformedBounds transformedBounds) + { + CheckDisposed(); + if (_attachedTo == null) + throw new InvalidOperationException("The control isn't currently attached to a toplevel"); + var bounds = transformedBounds.Bounds.TransformToAABB(transformedBounds.Transform) * + new Vector(_attachedTo.Window.Scaling, _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 ac3fa021f1..b7bb0e19ba 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -1,6 +1,3 @@ -// 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 System; using System.Collections.Generic; using System.ComponentModel; @@ -39,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; } } @@ -61,6 +58,11 @@ namespace Avalonia.Win32 CreateMessageWindow(); } + /// + /// Gets the actual WindowsVersion. Same as the info returned from RtlGetVersion. + /// + public static Version WindowsVersion { get; } = RtlGetVersion(); + public static bool UseDeferredRendering => Options.UseDeferredRendering; internal static bool UseOverlayPopups => Options.OverlayPopups; public static Win32PlatformOptions Options { get; private set; } @@ -207,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/Win32TypeExtensions.cs b/src/Windows/Avalonia.Win32/Win32TypeExtensions.cs new file mode 100644 index 0000000000..8193611f6d --- /dev/null +++ b/src/Windows/Avalonia.Win32/Win32TypeExtensions.cs @@ -0,0 +1,13 @@ +using static Avalonia.Win32.Interop.UnmanagedMethods; + +namespace Avalonia.Win32 +{ + internal static class Win32TypeExtensions + { + public static PixelRect ToPixelRect(this RECT rect) + { + return new PixelRect(rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top); + } + } +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs new file mode 100644 index 0000000000..391abdfc73 --- /dev/null +++ b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs @@ -0,0 +1,537 @@ +// 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 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 WndProc(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) + { + 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); + } + + 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); + + _maxTrackSize = mmi.ptMaxTrackSize; + + 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; + } + + public INativeControlHostImpl NativeControlHost => _nativeControlHost; + + protected virtual bool ShouldTakeFocusOnClick => true; + } +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index e8c3177ec5..36398eb810 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -1,14 +1,12 @@ -// 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 System; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Media; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; @@ -18,42 +16,67 @@ using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32 { - public class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo + /// + /// Window implementation for Win32 platform. + /// + public partial class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo, + ITopLevelImplWithNativeControlHost { private static readonly List s_instances = new List(); - private static readonly IntPtr DefaultCursor = UnmanagedMethods.LoadCursor( + private static readonly IntPtr DefaultCursor = LoadCursor( IntPtr.Zero, new IntPtr((int)UnmanagedMethods.Cursor.IDC_ARROW)); - private UnmanagedMethods.WndProc _wndProcDelegate; + private static readonly Dictionary s_edgeLookup = + new Dictionary + { + { WindowEdge.East, HitTestValues.HTRIGHT }, + { WindowEdge.North, HitTestValues.HTTOP }, + { WindowEdge.NorthEast, HitTestValues.HTTOPRIGHT }, + { WindowEdge.NorthWest, HitTestValues.HTTOPLEFT }, + { WindowEdge.South, HitTestValues.HTBOTTOM }, + { WindowEdge.SouthEast, HitTestValues.HTBOTTOMRIGHT }, + { WindowEdge.SouthWest, HitTestValues.HTBOTTOMLEFT }, + { WindowEdge.West, HitTestValues.HTLEFT } + }; + + private SavedWindowInfo _savedWindowInfo; + private bool _isFullScreenActive; + +#if USE_MANAGED_DRAG + private readonly ManagedWindowResizeDragHelper _managedDrag; +#endif + + private const WindowStyles WindowStateMask = (WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE); + private readonly TouchDevice _touchDevice; + private readonly MouseDevice _mouseDevice; + private readonly ManagedDeferredRendererLock _rendererLock; + private readonly FramebufferManager _framebuffer; + private readonly IGlPlatformSurface _gl; + + private Win32NativeControlHost _nativeControlHost; + private WndProc _wndProcDelegate; private string _className; private IntPtr _hwnd; private bool _multitouch; - private TouchDevice _touchDevice = new TouchDevice(); private IInputRoot _owner; - private ManagedDeferredRendererLock _rendererLock = new ManagedDeferredRendererLock(); + private WindowProperties _windowProperties; private bool _trackingMouse; - private bool _decorated = true; - private bool _resizable = true; - private bool _topmost = false; - private bool _taskbarIcon = true; + private bool _topmost; private double _scaling = 1; private WindowState _showWindowState; private WindowState _lastWindowState; - private FramebufferManager _framebuffer; - private IGlPlatformSurface _gl; private OleDropTarget _dropTarget; private Size _minSize; private Size _maxSize; + private POINT _maxTrackSize; private WindowImpl _parent; - private readonly List _disabledBy = new List(); - -#if USE_MANAGED_DRAG - private readonly ManagedWindowResizeDragHelper _managedDrag; -#endif public WindowImpl() { + _touchDevice = new TouchDevice(); + _mouseDevice = new WindowsMouseDevice(); + #if USE_MANAGED_DRAG _managedDrag = new ManagedWindowResizeDragHelper(this, capture => { @@ -63,12 +86,25 @@ namespace Avalonia.Win32 UnmanagedMethods.ReleaseCapture(); }); #endif + + _windowProperties = new WindowProperties + { + ShowInTaskbar = false, + IsResizable = true, + Decorations = SystemDecorations.Full + }; + _rendererLock = new ManagedDeferredRendererLock(); + + CreateWindow(); _framebuffer = new FramebufferManager(_hwnd); + if (Win32GlManager.EglFeature != null) - _gl = new EglGlPlatformSurface((EglDisplay)Win32GlManager.EglFeature.Display, - Win32GlManager.EglFeature.DeferredContext, this); + _gl = new EglGlPlatformSurface(Win32GlManager.EglFeature.DeferredContext, this); + + Screen = new ScreenImpl(); + _nativeControlHost = new Win32NativeControlHost(this); s_instances.Add(this); } @@ -91,19 +127,23 @@ namespace Avalonia.Win32 public Action PositionChanged { get; set; } public Action WindowStateChanged { get; set; } + + public Action LostFocus { get; set; } + + public Action TransparencyLevelChanged { get; set; } public Thickness BorderThickness { get { - if (_decorated) + if (HasFullDecorations) { - var style = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE); - var exStyle = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_EXSTYLE); + var style = GetStyle(); + var exStyle = GetExtendedStyle(); var padding = new RECT(); - if (UnmanagedMethods.AdjustWindowRectEx(ref padding, style, false, exStyle)) + if (AdjustWindowRectEx(ref padding, (uint)style, false, (uint)exStyle)) { return new Thickness(-padding.left, -padding.top, padding.right, padding.bottom); } @@ -119,16 +159,156 @@ namespace Avalonia.Win32 } } + public double Scaling => _scaling; + public Size ClientSize { get { - UnmanagedMethods.RECT rect; - UnmanagedMethods.GetClientRect(_hwnd, out rect); + GetClientRect(_hwnd, out var rect); + return new Size(rect.right, rect.bottom) / Scaling; } } + public IScreenImpl Screen { get; } + + public IPlatformHandle Handle { get; private set; } + + public virtual Size MaxAutoSizeHint => new Size(_maxTrackSize.X / Scaling, _maxTrackSize.Y / Scaling); + + public IMouseDevice MouseDevice => _mouseDevice; + + public WindowState WindowState + { + get + { + var placement = default(WINDOWPLACEMENT); + GetWindowPlacement(_hwnd, ref placement); + + return placement.ShowCmd switch + { + ShowWindowCommand.Maximize => WindowState.Maximized, + ShowWindowCommand.Minimize => WindowState.Minimized, + _ => WindowState.Normal + }; + } + + set + { + if (IsWindowVisible(_hwnd)) + { + ShowWindow(value); + } + else + { + _showWindowState = value; + } + } + } + + public WindowTransparencyLevel TransparencyLevel { get; private set; } + + protected IntPtr Hwnd => _hwnd; + + public void SetTransparencyLevelHint (WindowTransparencyLevel transparencyLevel) + { + TransparencyLevel = EnableBlur(transparencyLevel); + } + + private WindowTransparencyLevel EnableBlur(WindowTransparencyLevel transparencyLevel) + { + bool canUseTransparency = false; + bool canUseAcrylic = false; + + if (Win32Platform.WindowsVersion.Major >= 10) + { + canUseTransparency = true; + + if (Win32Platform.WindowsVersion.Major > 10 || Win32Platform.WindowsVersion.Build >= 19628) + { + canUseAcrylic = true; + } + } + + if (!canUseTransparency || DwmIsCompositionEnabled(out var compositionEnabled) != 0 || !compositionEnabled) + { + return WindowTransparencyLevel.None; + } + + var accent = new AccentPolicy(); + var accentStructSize = Marshal.SizeOf(accent); + + if(transparencyLevel == WindowTransparencyLevel.AcrylicBlur && !canUseAcrylic) + { + transparencyLevel = WindowTransparencyLevel.Blur; + } + + switch (transparencyLevel) + { + default: + case WindowTransparencyLevel.None: + accent.AccentState = AccentState.ACCENT_DISABLED; + break; + + case WindowTransparencyLevel.Transparent: + accent.AccentState = AccentState.ACCENT_ENABLE_TRANSPARENTGRADIENT; + break; + + case WindowTransparencyLevel.Blur: + accent.AccentState = AccentState.ACCENT_ENABLE_BLURBEHIND; + break; + + case WindowTransparencyLevel.AcrylicBlur: + case (WindowTransparencyLevel.AcrylicBlur + 1): // hack-force acrylic. + accent.AccentState = AccentState.ACCENT_ENABLE_ACRYLIC; + transparencyLevel = WindowTransparencyLevel.AcrylicBlur; + break; + } + + accent.AccentFlags = 2; + accent.GradientColor = 0x00FFFFFF; + + var accentPtr = Marshal.AllocHGlobal(accentStructSize); + Marshal.StructureToPtr(accent, accentPtr, false); + + var data = new WindowCompositionAttributeData(); + data.Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY; + data.SizeOfData = accentStructSize; + data.Data = accentPtr; + + SetWindowCompositionAttribute(_hwnd, ref data); + + Marshal.FreeHGlobal(accentPtr); + + return transparencyLevel; + } + + public IEnumerable Surfaces => new object[] { Handle, _gl, _framebuffer }; + + public PixelPoint Position + { + get + { + GetWindowRect(_hwnd, out var rc); + + return new PixelPoint(rc.left, rc.top); + } + set + { + SetWindowPos( + Handle.Handle, + IntPtr.Zero, + value.X, + value.Y, + 0, + 0, + SetWindowPosFlags.SWP_NOSIZE | SetWindowPosFlags.SWP_NOACTIVATE); + } + } + + private bool HasFullDecorations => _windowProperties.Decorations == SystemDecorations.Full; + public void Move(PixelPoint point) => Position = point; public void SetMinMaxSize(Size minSize, Size maxSize) @@ -137,12 +317,6 @@ namespace Avalonia.Win32 _maxSize = maxSize; } - public IScreenImpl Screen - { - get; - } = new ScreenImpl(); - - public IRenderer CreateRenderer(IRenderRoot root) { var loop = AvaloniaLocator.Current.GetService(); @@ -160,148 +334,70 @@ namespace Avalonia.Win32 { int requestedClientWidth = (int)(value.Width * Scaling); int requestedClientHeight = (int)(value.Height * Scaling); - UnmanagedMethods.RECT clientRect; - UnmanagedMethods.GetClientRect(_hwnd, out clientRect); - + + GetClientRect(_hwnd, out var clientRect); + // do comparison after scaling to avoid rounding issues if (requestedClientWidth != clientRect.Width || requestedClientHeight != clientRect.Height) { - UnmanagedMethods.RECT windowRect; - UnmanagedMethods.GetWindowRect(_hwnd, out windowRect); + GetWindowRect(_hwnd, out var windowRect); - UnmanagedMethods.SetWindowPos( + SetWindowPos( _hwnd, IntPtr.Zero, 0, 0, requestedClientWidth + (windowRect.Width - clientRect.Width), requestedClientHeight + (windowRect.Height - clientRect.Height), - UnmanagedMethods.SetWindowPosFlags.SWP_RESIZE); - } - } - - public double Scaling => _scaling; - - public IPlatformHandle Handle - { - get; - private set; - } - - - void UpdateEnabled() - { - EnableWindow(_hwnd, _disabledBy.Count == 0); - } - - public Size MaxClientSize - { - get - { - return (new Size( - UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CXMAXTRACK), - UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CYMAXTRACK)) - - BorderThickness) / Scaling; - } - } - - public IMouseDevice MouseDevice => WindowsMouseDevice.Instance; - - public WindowState WindowState - { - get - { - var placement = default(UnmanagedMethods.WINDOWPLACEMENT); - UnmanagedMethods.GetWindowPlacement(_hwnd, ref placement); - - switch (placement.ShowCmd) - { - case UnmanagedMethods.ShowWindowCommand.Maximize: - return WindowState.Maximized; - case UnmanagedMethods.ShowWindowCommand.Minimize: - return WindowState.Minimized; - default: - return WindowState.Normal; - } - } - - set - { - if (UnmanagedMethods.IsWindowVisible(_hwnd)) - { - ShowWindow(value); - } - else - { - _showWindowState = value; - } + SetWindowPosFlags.SWP_RESIZE); } } - public IEnumerable Surfaces => new object[] - { - Handle, _gl, _framebuffer - }; - public void Activate() { - UnmanagedMethods.SetActiveWindow(_hwnd); + SetActiveWindow(_hwnd); } public IPopupImpl CreatePopup() => Win32Platform.UseOverlayPopups ? null : new PopupImpl(this); public void Dispose() { - if (_hwnd != IntPtr.Zero) + if (_dropTarget != null) { - UnmanagedMethods.DestroyWindow(_hwnd); - _hwnd = IntPtr.Zero; + OleContext.Current?.UnregisterDragDrop(Handle); + _dropTarget = null; } - if (_className != null) - { - UnmanagedMethods.UnregisterClass(_className, UnmanagedMethods.GetModuleHandle(null)); - _className = null; - } - } - public void Hide() - { - if (_parent != null) + if (_hwnd != IntPtr.Zero) { - _parent._disabledBy.Remove(this); - _parent.UpdateEnabled(); - _parent = null; + DestroyWindow(_hwnd); + _hwnd = IntPtr.Zero; } - UnmanagedMethods.ShowWindow(_hwnd, UnmanagedMethods.ShowWindowCommand.Hide); - } - public void SetSystemDecorations(bool value) - { - if (value == _decorated) + if (_className != null) { - return; + UnregisterClass(_className, GetModuleHandle(null)); + _className = null; } - - UpdateWMStyles(()=> _decorated = value); } public void Invalidate(Rect rect) { - var f = Scaling; - var r = new UnmanagedMethods.RECT + var scaling = Scaling; + var r = new RECT { - left = (int)Math.Floor(rect.X * f), - top = (int)Math.Floor(rect.Y * f), - right = (int)Math.Ceiling(rect.Right * f), - bottom = (int)Math.Ceiling(rect.Bottom * f), + left = (int)Math.Floor(rect.X * scaling), + top = (int)Math.Floor(rect.Y * scaling), + right = (int)Math.Ceiling(rect.Right * scaling), + bottom = (int)Math.Ceiling(rect.Bottom * scaling), }; - UnmanagedMethods.InvalidateRect(_hwnd, ref r, false); + InvalidateRect(_hwnd, ref r, false); } public Point PointToClient(PixelPoint point) { - var p = new UnmanagedMethods.POINT { X = (int)point.X, Y = (int)point.Y }; + var p = new POINT { X = point.X, Y = point.Y }; UnmanagedMethods.ScreenToClient(_hwnd, ref p); return new Point(p.X, p.Y) / Scaling; } @@ -309,8 +405,8 @@ namespace Avalonia.Win32 public PixelPoint PointToScreen(Point point) { point *= Scaling; - var p = new UnmanagedMethods.POINT { X = (int)point.X, Y = (int)point.Y }; - UnmanagedMethods.ClientToScreen(_hwnd, ref p); + var p = new POINT { X = (int)point.X, Y = (int)point.Y }; + ClientToScreen(_hwnd, ref p); return new PixelPoint(p.X, p.Y); } @@ -320,482 +416,152 @@ namespace Avalonia.Win32 CreateDropTarget(); } - public void SetTitle(string title) + public void Hide() { - UnmanagedMethods.SetWindowText(_hwnd, title); + UnmanagedMethods.ShowWindow(_hwnd, ShowWindowCommand.Hide); } public virtual void Show() { - SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, IntPtr.Zero); + SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, _parent != null ? _parent._hwnd : IntPtr.Zero); ShowWindow(_showWindowState); } - public void BeginMoveDrag() + public Action GotInputWhenDisabled { get; set; } + + public void SetParent(IWindowImpl parent) { - WindowsMouseDevice.Instance.Capture(null); - UnmanagedMethods.DefWindowProc(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN, - new IntPtr((int)UnmanagedMethods.HitTestValues.HTCAPTION), IntPtr.Zero); + _parent = (WindowImpl)parent; + SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, _parent._hwnd); } - static readonly Dictionary EdgeDic = new Dictionary + public void SetEnabled(bool enable) => EnableWindow(_hwnd, enable); + + public void BeginMoveDrag(PointerPressedEventArgs e) { - {WindowEdge.East, UnmanagedMethods.HitTestValues.HTRIGHT}, - {WindowEdge.North, UnmanagedMethods.HitTestValues.HTTOP }, - {WindowEdge.NorthEast, UnmanagedMethods.HitTestValues.HTTOPRIGHT }, - {WindowEdge.NorthWest, UnmanagedMethods.HitTestValues.HTTOPLEFT }, - {WindowEdge.South, UnmanagedMethods.HitTestValues.HTBOTTOM }, - {WindowEdge.SouthEast, UnmanagedMethods.HitTestValues.HTBOTTOMRIGHT }, - {WindowEdge.SouthWest, UnmanagedMethods.HitTestValues.HTBOTTOMLEFT }, - {WindowEdge.West, UnmanagedMethods.HitTestValues.HTLEFT} - }; + _mouseDevice.Capture(null); + DefWindowProc(_hwnd, (int)WindowsMessage.WM_NCLBUTTONDOWN, + new IntPtr((int)HitTestValues.HTCAPTION), IntPtr.Zero); + e.Pointer.Capture(null); + } - public void BeginResizeDrag(WindowEdge edge) + public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) { #if USE_MANAGED_DRAG - _managedDrag.BeginResizeDrag(edge, ScreenToClient(MouseDevice.Position)); + _managedDrag.BeginResizeDrag(edge, ScreenToClient(MouseDevice.Position.ToPoint(_scaling))); #else - WindowsMouseDevice.Instance.Capture(null); - UnmanagedMethods.DefWindowProc(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN, - new IntPtr((int)EdgeDic[edge]), IntPtr.Zero); + _mouseDevice.Capture(null); + DefWindowProc(_hwnd, (int)WindowsMessage.WM_NCLBUTTONDOWN, + new IntPtr((int)s_edgeLookup[edge]), IntPtr.Zero); #endif } - public PixelPoint Position - { - get - { - UnmanagedMethods.RECT rc; - UnmanagedMethods.GetWindowRect(_hwnd, out rc); - return new PixelPoint(rc.left, rc.top); - } - set - { - UnmanagedMethods.SetWindowPos( - Handle.Handle, - IntPtr.Zero, - value.X, - value.Y, - 0, - 0, - UnmanagedMethods.SetWindowPosFlags.SWP_NOSIZE | UnmanagedMethods.SetWindowPosFlags.SWP_NOACTIVATE); - - } - } - - public void ShowDialog(IWindowImpl parent) + public void SetTitle(string title) { - _parent = (WindowImpl)parent; - _parent._disabledBy.Add(this); - _parent.UpdateEnabled(); - SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, ((WindowImpl)parent)._hwnd); - ShowWindow(_showWindowState); + SetWindowText(_hwnd, title); } public void SetCursor(IPlatformHandle cursor) { var hCursor = cursor?.Handle ?? DefaultCursor; - UnmanagedMethods.SetClassLong(_hwnd, UnmanagedMethods.ClassLongIndex.GCLP_HCURSOR, hCursor); + SetClassLong(_hwnd, ClassLongIndex.GCLP_HCURSOR, hCursor); if (_owner.IsPointerOver) + { UnmanagedMethods.SetCursor(hCursor); + } } - protected virtual IntPtr CreateWindowOverride(ushort atom) + public void SetIcon(IWindowIconImpl icon) { - return 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); + var impl = (IconImpl)icon; + var hIcon = impl?.HIcon ?? IntPtr.Zero; + PostMessage(_hwnd, (int)WindowsMessage.WM_SETICON, + new IntPtr((int)Icons.ICON_BIG), hIcon); } - bool ShouldIgnoreTouchEmulatedMessage() - { - if (!_multitouch) - return false; - var marker = 0xFF515700L; - var info = GetMessageExtraInfo().ToInt64(); - return (info & marker) == marker; - } - - [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) + public void ShowTaskbarIcon(bool value) { - bool unicode = UnmanagedMethods.IsWindowUnicode(hWnd); - - const double wheelDelta = 120.0; - uint timestamp = unchecked((uint)UnmanagedMethods.GetMessageTime()); + var newWindowProperties = _windowProperties; - RawInputEventArgs e = null; - - WindowsMouseDevice.Instance.CurrentWindow = this; - - switch ((UnmanagedMethods.WindowsMessage)msg) - { - case UnmanagedMethods.WindowsMessage.WM_ACTIVATE: - var wa = (UnmanagedMethods.WindowActivate)(ToInt32(wParam) & 0xffff); - - switch (wa) - { - case UnmanagedMethods.WindowActivate.WA_ACTIVE: - case UnmanagedMethods.WindowActivate.WA_CLICKACTIVE: - Activated?.Invoke(); - break; - - case UnmanagedMethods.WindowActivate.WA_INACTIVE: - Deactivated?.Invoke(); - break; - } + newWindowProperties.ShowInTaskbar = value; - return IntPtr.Zero; - - case WindowsMessage.WM_NCCALCSIZE: - if (ToInt32(wParam) == 1 && !_decorated) - { - return IntPtr.Zero; - } - break; - - case UnmanagedMethods.WindowsMessage.WM_CLOSE: - bool? preventClosing = Closing?.Invoke(); - if (preventClosing == true) - { - return IntPtr.Zero; - } - break; - - case UnmanagedMethods.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(); - if (_parent != null) - { - _parent._disabledBy.Remove(this); - _parent.UpdateEnabled(); - } - //Free other resources - Dispose(); - return IntPtr.Zero; - - case UnmanagedMethods.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 UnmanagedMethods.WindowsMessage.WM_KEYDOWN: - case UnmanagedMethods.WindowsMessage.WM_SYSKEYDOWN: - e = new RawKeyEventArgs( - WindowsKeyboardDevice.Instance, - timestamp, - RawKeyEventType.KeyDown, - KeyInterop.KeyFromVirtualKey(ToInt32(wParam)), WindowsKeyboardDevice.Instance.Modifiers); - break; - - case UnmanagedMethods.WindowsMessage.WM_MENUCHAR: - // mute the system beep - return (IntPtr)((Int32)UnmanagedMethods.MenuCharParam.MNC_CLOSE << 16); - - case UnmanagedMethods.WindowsMessage.WM_KEYUP: - case UnmanagedMethods.WindowsMessage.WM_SYSKEYUP: - e = new RawKeyEventArgs( - WindowsKeyboardDevice.Instance, - timestamp, - RawKeyEventType.KeyUp, - KeyInterop.KeyFromVirtualKey(ToInt32(wParam)), WindowsKeyboardDevice.Instance.Modifiers); - break; - case UnmanagedMethods.WindowsMessage.WM_CHAR: - // Ignore control chars - if (ToInt32(wParam) >= 32) - { - e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, - new string((char)ToInt32(wParam), 1)); - } - - break; - - case UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN: - case UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN: - case UnmanagedMethods.WindowsMessage.WM_MBUTTONDOWN: - if(ShouldIgnoreTouchEmulatedMessage()) - break; - e = new RawPointerEventArgs( - WindowsMouseDevice.Instance, - timestamp, - _owner, - msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN - ? RawPointerEventType.LeftButtonDown - : msg == (int)UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN - ? RawPointerEventType.RightButtonDown - : RawPointerEventType.MiddleButtonDown, - DipFromLParam(lParam), GetMouseModifiers(wParam)); - break; - - case UnmanagedMethods.WindowsMessage.WM_LBUTTONUP: - case UnmanagedMethods.WindowsMessage.WM_RBUTTONUP: - case UnmanagedMethods.WindowsMessage.WM_MBUTTONUP: - if(ShouldIgnoreTouchEmulatedMessage()) - break; - e = new RawPointerEventArgs( - WindowsMouseDevice.Instance, - timestamp, - _owner, - msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONUP - ? RawPointerEventType.LeftButtonUp - : msg == (int)UnmanagedMethods.WindowsMessage.WM_RBUTTONUP - ? RawPointerEventType.RightButtonUp - : RawPointerEventType.MiddleButtonUp, - DipFromLParam(lParam), GetMouseModifiers(wParam)); - break; - - case UnmanagedMethods.WindowsMessage.WM_MOUSEMOVE: - if(ShouldIgnoreTouchEmulatedMessage()) - break; - if (!_trackingMouse) - { - var tm = new UnmanagedMethods.TRACKMOUSEEVENT - { - cbSize = Marshal.SizeOf(), - dwFlags = 2, - hwndTrack = _hwnd, - dwHoverTime = 0, - }; - - UnmanagedMethods.TrackMouseEvent(ref tm); - } - - e = new RawPointerEventArgs( - WindowsMouseDevice.Instance, - timestamp, - _owner, - RawPointerEventType.Move, - DipFromLParam(lParam), GetMouseModifiers(wParam)); - - break; - - case UnmanagedMethods.WindowsMessage.WM_MOUSEWHEEL: - e = new RawMouseWheelEventArgs( - WindowsMouseDevice.Instance, - timestamp, - _owner, - PointToClient(PointFromLParam(lParam)), - new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta), GetMouseModifiers(wParam)); - break; - - case UnmanagedMethods.WindowsMessage.WM_MOUSEHWHEEL: - e = new RawMouseWheelEventArgs( - WindowsMouseDevice.Instance, - timestamp, - _owner, - PointToClient(PointFromLParam(lParam)), - new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0), GetMouseModifiers(wParam)); - break; - - case UnmanagedMethods.WindowsMessage.WM_MOUSELEAVE: - _trackingMouse = false; - e = new RawPointerEventArgs( - WindowsMouseDevice.Instance, - timestamp, - _owner, - RawPointerEventType.LeaveWindow, - new Point(), WindowsKeyboardDevice.Instance.Modifiers); - break; - - case UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN: - case UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN: - case UnmanagedMethods.WindowsMessage.WM_NCMBUTTONDOWN: - e = new RawPointerEventArgs( - WindowsMouseDevice.Instance, - timestamp, - _owner, - msg == (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN - ? RawPointerEventType.NonClientLeftButtonDown - : msg == (int)UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN - ? RawPointerEventType.RightButtonDown - : RawPointerEventType.MiddleButtonDown, - new Point(0, 0), 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.HasFlag(TouchInputFlags.TOUCHEVENTF_UP) ? - RawPointerEventType.TouchEnd : - touchInput.Flags.HasFlag(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 (!_decorated) - { - return IntPtr.Zero; - } - break; - - case WindowsMessage.WM_NCACTIVATE: - if (!_decorated) - { - return new IntPtr(1); - } - break; - - case UnmanagedMethods.WindowsMessage.WM_PAINT: - using (_rendererLock.Lock()) - { - UnmanagedMethods.PAINTSTRUCT ps; - if (UnmanagedMethods.BeginPaint(_hwnd, out 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)); - UnmanagedMethods.EndPaint(_hwnd, ref ps); - } - } - - return IntPtr.Zero; - - case UnmanagedMethods.WindowsMessage.WM_SIZE: - using (_rendererLock.Lock()) - { - // Do nothing here, just block until the pending frame render is completed on the render thread - } - var size = (UnmanagedMethods.SizeCommand)wParam; - - if (Resized != null && - (size == UnmanagedMethods.SizeCommand.Restored || - size == UnmanagedMethods.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 UnmanagedMethods.WindowsMessage.WM_MOVE: - PositionChanged?.Invoke(new PixelPoint((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16))); - return IntPtr.Zero; - - case UnmanagedMethods.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); + UpdateWindowProperties(newWindowProperties); + } - if (!Double.IsInfinity(_maxSize.Width) && _maxSize.Width > 0) - mmi.ptMaxTrackSize.X = (int)((_maxSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right); + public void CanResize(bool value) + { + var newWindowProperties = _windowProperties; - if (!Double.IsInfinity(_maxSize.Height) && _maxSize.Height > 0) - mmi.ptMaxTrackSize.Y = (int)((_maxSize.Height * Scaling) + BorderThickness.Top + BorderThickness.Bottom); + newWindowProperties.IsResizable = value; - Marshal.StructureToPtr(mmi, lParam, true); - return IntPtr.Zero; + UpdateWindowProperties(newWindowProperties); + } - case UnmanagedMethods.WindowsMessage.WM_DISPLAYCHANGE: - (Screen as ScreenImpl)?.InvalidateScreensCache(); - return IntPtr.Zero; - } + public void SetSystemDecorations(SystemDecorations value) + { + var newWindowProperties = _windowProperties; -#if USE_MANAGED_DRAG + newWindowProperties.Decorations = value; - if (_managedDrag.PreprocessInputEvent(ref e)) - return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); -#endif + UpdateWindowProperties(newWindowProperties); + } - if (e != null && Input != null) + public void SetTopmost(bool value) + { + if (value == _topmost) { - Input(e); - - if (e.Handled) - { - return IntPtr.Zero; - } + return; } - using (_rendererLock.Lock()) - return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); + IntPtr hWndInsertAfter = value ? WindowPosZOrder.HWND_TOPMOST : WindowPosZOrder.HWND_NOTOPMOST; + SetWindowPos(_hwnd, + hWndInsertAfter, + 0, 0, 0, 0, + SetWindowPosFlags.SWP_NOMOVE | SetWindowPosFlags.SWP_NOSIZE | SetWindowPosFlags.SWP_NOACTIVATE); + + _topmost = value; } - static RawInputModifiers GetMouseModifiers(IntPtr wParam) + protected virtual IntPtr CreateWindowOverride(ushort atom) { - var keys = (UnmanagedMethods.ModifierKeys)ToInt32(wParam); - var modifiers = WindowsKeyboardDevice.Instance.Modifiers; - if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_LBUTTON)) - modifiers |= RawInputModifiers.LeftMouseButton; - if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_RBUTTON)) - modifiers |= RawInputModifiers.RightMouseButton; - if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_MBUTTON)) - modifiers |= RawInputModifiers.MiddleMouseButton; - return modifiers; + return CreateWindowEx( + 0, + atom, + null, + (int)WindowStyles.WS_OVERLAPPEDWINDOW | (int) WindowStyles.WS_CLIPCHILDREN, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero); } private void CreateWindow() { // Ensure that the delegate doesn't get garbage collected by storing it as a field. - _wndProcDelegate = new UnmanagedMethods.WndProc(WndProc); + _wndProcDelegate = WndProc; - _className = "Avalonia-" + Guid.NewGuid(); + _className = $"Avalonia-{Guid.NewGuid().ToString()}"; - UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX + // Unique DC helps with performance when using Gpu based rendering + const ClassStyles windowClassStyle = ClassStyles.CS_OWNDC | ClassStyles.CS_HREDRAW | ClassStyles.CS_VREDRAW; + + var wndClassEx = new WNDCLASSEX { - cbSize = Marshal.SizeOf(), - style = (int)(ClassStyles.CS_OWNDC | ClassStyles.CS_HREDRAW | ClassStyles.CS_VREDRAW), // Unique DC helps with performance when using Gpu based rendering + cbSize = Marshal.SizeOf(), + style = (int)windowClassStyle, lpfnWndProc = _wndProcDelegate, - hInstance = UnmanagedMethods.GetModuleHandle(null), + hInstance = GetModuleHandle(null), hCursor = DefaultCursor, hbrBackground = IntPtr.Zero, lpszClassName = _className }; - ushort atom = UnmanagedMethods.RegisterClassEx(ref wndClassEx); + ushort atom = RegisterClassEx(ref wndClassEx); if (atom == 0) { @@ -811,23 +577,24 @@ namespace Avalonia.Win32 Handle = new PlatformHandle(_hwnd, PlatformConstants.WindowHandleType); - _multitouch = Win32Platform.Options.EnableMultitouch ?? false; + _multitouch = Win32Platform.Options.EnableMultitouch ?? true; + if (_multitouch) - RegisterTouchWindow(_hwnd, 0); - - if (UnmanagedMethods.ShCoreAvailable) { - uint dpix, dpiy; + RegisterTouchWindow(_hwnd, 0); + } - var monitor = UnmanagedMethods.MonitorFromWindow( + if (ShCoreAvailable) + { + var monitor = MonitorFromWindow( _hwnd, - UnmanagedMethods.MONITOR.MONITOR_DEFAULTTONEAREST); + MONITOR.MONITOR_DEFAULTTONEAREST); - if (UnmanagedMethods.GetDpiForMonitor( - monitor, - UnmanagedMethods.MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, - out dpix, - out dpiy) == 0) + if (GetDpiForMonitor( + monitor, + MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, + out var dpix, + out var dpiy) == 0) { _scaling = dpix / 96.0; } @@ -836,49 +603,106 @@ namespace Avalonia.Win32 private void CreateDropTarget() { - OleDropTarget odt = new OleDropTarget(this, _owner); + var odt = new OleDropTarget(this, _owner); + if (OleContext.Current?.RegisterDragDrop(Handle, odt) ?? false) + { _dropTarget = odt; + } } - private Point DipFromLParam(IntPtr lParam) + /// + /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc + /// Method must only be called from inside UpdateWindowProperties. + /// + /// + private void SetFullScreen(bool fullscreen) { - return new Point((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)) / Scaling; - } + if (fullscreen) + { + GetWindowRect(_hwnd, out var windowRect); + _savedWindowInfo.WindowRect = windowRect; - private PixelPoint PointFromLParam(IntPtr lParam) - { - return new PixelPoint((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)); - } + var current = GetStyle(); + var currentEx = GetExtendedStyle(); - private Point ScreenToClient(Point point) - { - var p = new UnmanagedMethods.POINT { X = (int)point.X, Y = (int)point.Y }; - UnmanagedMethods.ScreenToClient(_hwnd, ref p); - return new Point(p.X, p.Y); + _savedWindowInfo.Style = current; + _savedWindowInfo.ExStyle = currentEx; + + // Set new window style and size. + SetStyle(current & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME), false); + SetExtendedStyle(currentEx & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE), false); + + // On expand, if we're given a window_rect, grow to it, otherwise do + // not resize. + MONITORINFO monitor_info = MONITORINFO.Create(); + GetMonitorInfo(MonitorFromWindow(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST), ref monitor_info); + + var window_rect = monitor_info.rcMonitor.ToPixelRect(); + + SetWindowPos(_hwnd, IntPtr.Zero, window_rect.X, window_rect.Y, + window_rect.Width, window_rect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); + + _isFullScreenActive = true; + } + else + { + // Reset original window style and size. The multiple window size/moves + // here are ugly, but if SetWindowPos() doesn't redraw, the taskbar won't be + // repainted. Better-looking methods welcome. + _isFullScreenActive = false; + + var windowStates = GetWindowStateStyles(); + SetStyle((_savedWindowInfo.Style & ~WindowStateMask) | windowStates, false); + SetExtendedStyle(_savedWindowInfo.ExStyle, false); + + // On restore, resize to the previous saved rect size. + var new_rect = _savedWindowInfo.WindowRect.ToPixelRect(); + + SetWindowPos(_hwnd, IntPtr.Zero, new_rect.X, new_rect.Y, new_rect.Width, + new_rect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); + + UpdateWindowProperties(_windowProperties, true); + } + + TaskBarList.MarkFullscreen(_hwnd, fullscreen); } private void ShowWindow(WindowState state) { - UnmanagedMethods.ShowWindowCommand command; + ShowWindowCommand command; + + var newWindowProperties = _windowProperties; switch (state) { case WindowState.Minimized: + newWindowProperties.IsFullScreen = false; command = ShowWindowCommand.Minimize; break; case WindowState.Maximized: + newWindowProperties.IsFullScreen = false; command = ShowWindowCommand.Maximize; break; case WindowState.Normal: + newWindowProperties.IsFullScreen = false; command = ShowWindowCommand.Restore; break; + case WindowState.FullScreen: + newWindowProperties.IsFullScreen = true; + UpdateWindowProperties(newWindowProperties); + return; + default: throw new ArgumentException("Invalid WindowState."); } + UpdateWindowProperties(newWindowProperties); + UnmanagedMethods.ShowWindow(_hwnd, command); if (state == WindowState.Maximized) @@ -898,12 +722,10 @@ namespace Avalonia.Win32 if (monitor != IntPtr.Zero) { - MONITORINFO monitorInfo = MONITORINFO.Create(); + var monitorInfo = MONITORINFO.Create(); if (GetMonitorInfo(monitor, ref monitorInfo)) { - RECT rcMonitorArea = monitorInfo.rcMonitor; - var x = monitorInfo.rcWork.left; var y = monitorInfo.rcWork.top; var cx = Math.Abs(monitorInfo.rcWork.right - x); @@ -912,145 +734,197 @@ namespace Avalonia.Win32 SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW); } } - } + } - public void SetIcon(IWindowIconImpl icon) + private WindowStyles GetWindowStateStyles() { - var impl = (IconImpl)icon; - var hIcon = impl.HIcon; - UnmanagedMethods.PostMessage(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_SETICON, - new IntPtr((int)UnmanagedMethods.Icons.ICON_BIG), hIcon); + return GetStyle() & WindowStateMask; } - private static int ToInt32(IntPtr ptr) + private WindowStyles GetStyle() { - if (IntPtr.Size == 4) return ptr.ToInt32(); - - return (int)(ptr.ToInt64() & 0xffffffff); + if (_isFullScreenActive) + { + return _savedWindowInfo.Style; + } + else + { + return (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE); + } } - - public void ShowTaskbarIcon(bool value) + private WindowStyles GetExtendedStyle() { - if (_taskbarIcon == value) + if (_isFullScreenActive) { - return; + return _savedWindowInfo.ExStyle; } + else + { + return (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE); + } + } - _taskbarIcon = value; - - var style = (UnmanagedMethods.WindowStyles)UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_EXSTYLE); - - style &= ~(UnmanagedMethods.WindowStyles.WS_VISIBLE); + private void SetStyle(WindowStyles style, bool save = true) + { + if (save) + { + _savedWindowInfo.Style = style; + } - style |= UnmanagedMethods.WindowStyles.WS_EX_TOOLWINDOW; + if (!_isFullScreenActive) + { + SetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE, (uint)style); + } + } - if (value) - style |= UnmanagedMethods.WindowStyles.WS_EX_APPWINDOW; - else - style &= ~(UnmanagedMethods.WindowStyles.WS_EX_APPWINDOW); + private void SetExtendedStyle(WindowStyles style, bool save = true) + { + if (save) + { + _savedWindowInfo.ExStyle = style; + } - WINDOWPLACEMENT windowPlacement = UnmanagedMethods.WINDOWPLACEMENT.Default; - if (UnmanagedMethods.GetWindowPlacement(_hwnd, ref windowPlacement)) + if (!_isFullScreenActive) { - //Toggle to make the styles stick - UnmanagedMethods.ShowWindow(_hwnd, ShowWindowCommand.Hide); - UnmanagedMethods.SetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_EXSTYLE, (uint)style); - UnmanagedMethods.ShowWindow(_hwnd, windowPlacement.ShowCmd); + SetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE, (uint)style); } } - private void UpdateWMStyles(Action change) + private void UpdateWindowProperties(WindowProperties newProperties, bool forceChanges = false) { - var oldDecorated = _decorated; - - var oldThickness = BorderThickness; + var oldProperties = _windowProperties; - change(); + // Calling SetWindowPos will cause events to be sent and we need to respond + // according to the new values already. + _windowProperties = newProperties; - var style = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE); + if ((oldProperties.ShowInTaskbar != newProperties.ShowInTaskbar) || forceChanges) + { + var exStyle = GetExtendedStyle(); - const WindowStyles controlledFlags = WindowStyles.WS_OVERLAPPEDWINDOW; + if (newProperties.ShowInTaskbar) + { + exStyle |= WindowStyles.WS_EX_APPWINDOW; + } + else + { + exStyle &= ~WindowStyles.WS_EX_APPWINDOW; + } - style = style | controlledFlags ^ controlledFlags; + SetExtendedStyle(exStyle); - style |= WindowStyles.WS_OVERLAPPEDWINDOW; + // TODO: To hide non-owned window from taskbar we need to parent it to a hidden window. + // Otherwise it will still show in the taskbar. + } - if (!_decorated) + if ((oldProperties.IsResizable != newProperties.IsResizable) || forceChanges) { - style ^= (WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU); + var style = GetStyle(); + + if (newProperties.IsResizable) + { + style |= WindowStyles.WS_SIZEFRAME; + } + else + { + style &= ~WindowStyles.WS_SIZEFRAME; + } + + SetStyle(style); } - if (!_resizable) + if (oldProperties.IsFullScreen != newProperties.IsFullScreen) { - style ^= (WindowStyles.WS_SIZEFRAME); + SetFullScreen(newProperties.IsFullScreen); } - GetClientRect(_hwnd, out var oldClientRect); - var oldClientRectOrigin = new UnmanagedMethods.POINT(); - ClientToScreen(_hwnd, ref oldClientRectOrigin); - oldClientRect.Offset(oldClientRectOrigin); - - - SetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE, (uint)style); - - UnmanagedMethods.GetWindowRect(_hwnd, out var windowRect); - bool frameUpdated = false; - if (oldDecorated != _decorated) + if ((oldProperties.Decorations != newProperties.Decorations) || forceChanges) { - var newRect = oldClientRect; - if (_decorated) - AdjustWindowRectEx(ref newRect, (uint)style, false, - GetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE)); - SetWindowPos(_hwnd, IntPtr.Zero, newRect.left, newRect.top, newRect.Width, newRect.Height, - SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); - frameUpdated = true; - } + var style = GetStyle(); - if (!frameUpdated) - SetWindowPos(_hwnd, IntPtr.Zero, 0, 0, 0, 0, - SetWindowPosFlags.SWP_FRAMECHANGED | SetWindowPosFlags.SWP_NOZORDER | - SetWindowPosFlags.SWP_NOACTIVATE - | SetWindowPosFlags.SWP_NOMOVE | SetWindowPosFlags.SWP_NOSIZE); - } + const WindowStyles fullDecorationFlags = WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU; - public void CanResize(bool value) - { - if (value == _resizable) - { - return; - } + if (newProperties.Decorations == SystemDecorations.Full) + { + style |= fullDecorationFlags; + } + else + { + style &= ~fullDecorationFlags; + } - UpdateWMStyles(()=> _resizable = value); - } + SetStyle(style); - public void SetTopmost(bool value) - { - if (value == _topmost) - { - return; - } + if (!_isFullScreenActive) + { + var margin = newProperties.Decorations == SystemDecorations.BorderOnly ? 1 : 0; - IntPtr hWndInsertAfter = value ? WindowPosZOrder.HWND_TOPMOST : WindowPosZOrder.HWND_NOTOPMOST; - UnmanagedMethods.SetWindowPos(_hwnd, - hWndInsertAfter, - 0, 0, 0, 0, - SetWindowPosFlags.SWP_NOMOVE | SetWindowPosFlags.SWP_NOSIZE | SetWindowPosFlags.SWP_NOACTIVATE); + var margins = new MARGINS + { + cyBottomHeight = margin, + cxRightWidth = margin, + cxLeftWidth = margin, + cyTopHeight = margin + }; - _topmost = value; + DwmExtendFrameIntoClientArea(_hwnd, ref margins); + + GetClientRect(_hwnd, out var oldClientRect); + var oldClientRectOrigin = new POINT(); + ClientToScreen(_hwnd, ref oldClientRectOrigin); + oldClientRect.Offset(oldClientRectOrigin); + + var newRect = oldClientRect; + + if (newProperties.Decorations == SystemDecorations.Full) + { + AdjustWindowRectEx(ref newRect, (uint)style, false, (uint)GetExtendedStyle()); + } + + SetWindowPos(_hwnd, IntPtr.Zero, newRect.left, newRect.top, newRect.Width, newRect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | + SetWindowPosFlags.SWP_FRAMECHANGED); + } + } } +#if USE_MANAGED_DRAG + private Point ScreenToClient(Point point) + { + var p = new UnmanagedMethods.POINT { X = (int)point.X, Y = (int)point.Y }; + UnmanagedMethods.ScreenToClient(_hwnd, ref p); + return new Point(p.X, p.Y); + } +#endif + PixelSize EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Size { get { - RECT rect; - GetClientRect(_hwnd, out rect); + GetClientRect(_hwnd, out var rect); + return new PixelSize( Math.Max(1, rect.right - rect.left), Math.Max(1, rect.bottom - rect.top)); } } + IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle; + + private struct SavedWindowInfo + { + public WindowStyles Style { get; set; } + public WindowStyles ExStyle { get; set; } + public RECT WindowRect { get; set; } + }; + + private struct WindowProperties + { + public bool ShowInTaskbar; + public bool IsResizable; + public SystemDecorations Decorations; + public bool IsFullScreen; + } } } 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 65a6c15971..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) { @@ -20,14 +20,8 @@ namespace Avalonia.iOS return Disposable.Empty; } - public void SetSystemDecorations(bool enabled) + 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 new file mode 100644 index 0000000000..784f40fe1f --- /dev/null +++ b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs @@ -0,0 +1,404 @@ +using System; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.UnitTests; +using Moq; +using Xunit; + +namespace Avalonia.Animation.UnitTests +{ + public class AnimatableTests + { + [Fact] + public void Transition_Is_Not_Applied_When_Not_Attached_To_Visual_Tree() + { + var target = CreateTarget(); + var control = new Control + { + Transitions = new Transitions { target.Object }, + }; + + control.Opacity = 0.5; + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5), + Times.Never); + } + + [Fact] + public void Transition_Is_Not_Applied_To_Initial_Style() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var target = CreateTarget(); + var control = new Control + { + Transitions = new Transitions { target.Object }, + }; + + var root = new TestRoot + { + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(Visual.OpacityProperty, 0.8), + } + } + } + }; + + root.Child = control; + + Assert.Equal(0.8, control.Opacity); + + target.Verify(x => x.Apply( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + } + + [Fact] + public void Transition_Is_Applied_When_Local_Value_Changes() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + + control.Opacity = 0.5; + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5)); + } + + [Fact] + public void Transition_Is_Not_Applied_When_Animated_Value_Changes() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + + control.SetValue(Visual.OpacityProperty, 0.5, BindingPriority.Animation); + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5), + Times.Never); + } + + [Fact] + public void Transition_Is_Not_Applied_When_StyleTrigger_Changes_With_LocalValue_Present() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + + control.SetValue(Visual.OpacityProperty, 0.5); + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5)); + target.ResetCalls(); + + control.SetValue(Visual.OpacityProperty, 0.8, BindingPriority.StyleTrigger); + + target.Verify(x => x.Apply( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Fact] + public void Transition_Is_Disposed_When_Local_Value_Changes() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + var sub = new Mock(); + + target.Setup(x => x.Apply(control, It.IsAny(), 1.0, 0.5)).Returns(sub.Object); + + control.Opacity = 0.5; + sub.ResetCalls(); + control.Opacity = 0.4; + + sub.Verify(x => x.Dispose()); + } + + [Fact] + public void New_Transition_Is_Applied_When_Local_Value_Changes() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + + target.Setup(x => x.Property).Returns(Visual.OpacityProperty); + target.Setup(x => x.Apply(control, It.IsAny(), 1.0, 0.5)) + .Callback(() => + { + control.SetValue(Visual.OpacityProperty, 0.9, BindingPriority.Animation); + }) + .Returns(Mock.Of()); + + control.Opacity = 0.5; + + Assert.Equal(0.9, control.Opacity); + target.ResetCalls(); + + control.Opacity = 0.4; + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 0.9, + 0.4)); + } + + [Fact] + public void Transition_Is_Not_Applied_When_Removed_From_Visual_Tree() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + + control.Opacity = 0.5; + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5)); + target.ResetCalls(); + + var root = (TestRoot)control.Parent; + root.Child = null; + control.Opacity = 0.8; + + target.Verify(x => x.Apply( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Fact] + public void Animation_Is_Cancelled_When_Transition_Removed() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + var sub = new Mock(); + + target.Setup(x => x.Apply( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).Returns(sub.Object); + + control.Opacity = 0.5; + control.Transitions.RemoveAt(0); + + sub.Verify(x => x.Dispose()); + } + + [Fact] + public void Animation_Is_Cancelled_When_New_Style_Activates() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var target = CreateTarget(); + var control = CreateStyledControl(target.Object); + var sub = new Mock(); + + target.Setup(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5)).Returns(sub.Object); + + control.Opacity = 0.5; + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5), + Times.Once); + + control.Classes.Add("foo"); + + sub.Verify(x => x.Dispose()); + } + } + + [Fact] + public void Transition_From_Style_Trigger_Is_Applied() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var target = CreateTransition(Control.WidthProperty); + var control = CreateStyledControl(transition2: target.Object); + var sub = new Mock(); + + control.Classes.Add("foo"); + control.Width = 100; + + target.Verify(x => x.Apply( + control, + It.IsAny(), + double.NaN, + 100.0), + Times.Once); + } + } + + [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); + } + + private static Control CreateControl(ITransition transition) + { + var control = new Control + { + Transitions = new Transitions { transition }, + }; + + var root = new TestRoot(control); + return control; + } + + private static Control CreateStyledControl( + ITransition transition1 = null, + ITransition transition2 = null) + { + transition1 = transition1 ?? CreateTarget().Object; + transition2 = transition2 ?? CreateTransition(Control.WidthProperty).Object; + + var control = new Control + { + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter + { + Property = Control.TransitionsProperty, + Value = new Transitions { transition1 }, + } + } + }, + new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter + { + Property = Control.TransitionsProperty, + Value = new Transitions { transition2 }, + } + } + } + } + }; + + var root = new TestRoot(control); + return control; + } + + private static Mock CreateTransition(AvaloniaProperty property) + { + var target = new Mock(); + var sub = new Mock(); + + target.Setup(x => x.Property).Returns(property); + target.Setup(x => x.Apply( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).Returns(sub.Object); + + return target; + } + } +} diff --git a/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs b/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs index f7a8774689..fe718ec32b 100644 --- a/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs +++ b/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs @@ -14,6 +14,55 @@ namespace Avalonia.Animation.UnitTests { public class AnimationIterationTests { + [Fact] + public void Check_KeyTime_Correctly_Converted_To_Cue() + { + var keyframe1 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 100d), + }, + KeyTime = TimeSpan.FromSeconds(0.5) + }; + + var keyframe2 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 0d), + }, + KeyTime = TimeSpan.FromSeconds(0) + }; + + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(1), + Children = + { + keyframe2, + keyframe1 + } + }; + + var border = new Border() + { + Height = 100d, + Width = 100d + }; + + var clock = new TestClock(); + var animationRun = animation.RunAsync(border, clock); + + clock.Step(TimeSpan.Zero); + Assert.Equal(border.Width, 0d); + + clock.Step(TimeSpan.FromSeconds(1)); + Assert.Equal(border.Width, 100d); + + } + + [Fact] public void Check_Initial_Inter_and_Trailing_Delay_Values() { diff --git a/tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj b/tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj index ef515ce155..dd50eff2b6 100644 --- a/tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj +++ b/tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj @@ -1,6 +1,6 @@  - netcoreapp2.0;net47 + netcoreapp3.1;net47 Library true diff --git a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs new file mode 100644 index 0000000000..df7c0693e1 --- /dev/null +++ b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs @@ -0,0 +1,145 @@ +using System; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.Styling; +using Xunit; + +namespace Avalonia.Animation.UnitTests +{ + public class KeySplineTests + { + [Theory] + [InlineData("1,2 3,4")] + [InlineData("1 2 3 4")] + [InlineData("1 2,3 4")] + [InlineData("1,2,3,4")] + public void Can_Parse_KeySpline_Via_TypeConverter(string input) + { + var conv = new KeySplineTypeConverter(); + + var keySpline = (KeySpline)conv.ConvertFrom(input); + + Assert.Equal(1, keySpline.ControlPointX1); + Assert.Equal(2, keySpline.ControlPointY1); + Assert.Equal(3, keySpline.ControlPointX2); + Assert.Equal(4, keySpline.ControlPointY2); + } + + [Theory] + [InlineData(0.00)] + [InlineData(0.50)] + [InlineData(1.00)] + public void KeySpline_X_Values_In_Range_Do_Not_Throw(double input) + { + var keySpline = new KeySpline(); + keySpline.ControlPointX1 = input; // no exception will be thrown -- test will fail if exception thrown + keySpline.ControlPointX2 = input; // no exception will be thrown -- test will fail if exception thrown + } + + [Theory] + [InlineData(-0.01)] + [InlineData(1.01)] + public void KeySpline_X_Values_Cannot_Be_Out_Of_Range(double input) + { + var keySpline = new KeySpline(); + Assert.Throws(() => keySpline.ControlPointX1 = input); + Assert.Throws(() => keySpline.ControlPointX2 = input); + } + + /* + 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 + 2) Add the following xaml somewhere: +