diff --git a/.gitignore b/.gitignore index 6ee6057d5f..2b2c9c3d0d 100644 --- a/.gitignore +++ b/.gitignore @@ -195,3 +195,4 @@ Logs/ ModuleCache.noindex/ Build/Intermediates.noindex/ info.plist +build-intermediate diff --git a/.gitmodules b/.gitmodules index 2d2a9ac497..22c56307b0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github"] path = src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github url = https://github.com/AvaloniaUI/Portable.Xaml.git +[submodule "nukebuild/Numerge"] + path = nukebuild/Numerge + url = https://github.com/kekekeks/Numerge.git diff --git a/.nuke b/.nuke new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b0c0c807cb..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,24 +0,0 @@ -language: csharp -os: - - linux -dist: trusty -osx_image: xcode8.3 -env: - global: - - DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 - - DOTNET_CLI_TELEMETRY_OPTOUT=1 -mono: - - 5.2.0 -dotnet: 2.1.200 -script: - - sudo apt-get update - - sudo apt-get install castxml - - ./build.sh --target "Travis" --configuration "Release" -notifications: - email: false - webhooks: - urls: - - https://webhooks.gitter.im/e/98f653320ef2b7506c05 - on_success: change - on_failure: always - on_start: never diff --git a/Avalonia.sln b/Avalonia.sln index 7fa5f7736a..3ed931933a 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.27130.2027 MinimumVisualStudioVersion = 10.0.40219.1 @@ -147,6 +147,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\Splat.props = build\Splat.props build\System.Memory.props = build\System.Memory.props build\XUnit.props = build\XUnit.props + build\BuildTargets.targets = build\BuildTargets.targets EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6FAF79-58B4-482F-9122-0668C346364C}" @@ -188,6 +189,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia", "packages\Avalon EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Desktop", "src\Avalonia.Desktop\Avalonia.Desktop.csproj", "{3C471044-3640-45E3-B1B2-16D2FF8399EE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Build.Tasks", "src\Avalonia.Build.Tasks\Avalonia.Build.Tasks.csproj", "{BF28998D-072C-439A-AFBB-2FE5021241E0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "nukebuild\_build.csproj", "{3F00BC43-5095-477F-93D8-E65B08179A00}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Animation.UnitTests", "tests\Avalonia.Animation.UnitTests\Avalonia.Animation.UnitTests.csproj", "{AF227847-E65C-4BE9-BCE9-B551357788E0}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -213,6 +220,10 @@ Global Release|iPhoneSimulator = Release|iPhoneSimulator EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3F00BC43-5095-477F-93D8-E65B08179A00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F00BC43-5095-477F-93D8-E65B08179A00}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F00BC43-5095-477F-93D8-E65B08179A00}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F00BC43-5095-477F-93D8-E65B08179A00}.Release|Any CPU.Build.0 = Release|Any CPU {B09B78D8-9B26-48B0-9149-D64A2F120F3F}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {B09B78D8-9B26-48B0-9149-D64A2F120F3F}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {B09B78D8-9B26-48B0-9149-D64A2F120F3F}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -1687,6 +1698,54 @@ Global {3C471044-3640-45E3-B1B2-16D2FF8399EE}.Release|iPhone.Build.0 = Release|Any CPU {3C471044-3640-45E3-B1B2-16D2FF8399EE}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {3C471044-3640-45E3-B1B2-16D2FF8399EE}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.AppStore|iPhone.Build.0 = Debug|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.Debug|iPhone.Build.0 = Debug|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|Any CPU.Build.0 = Release|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|iPhone.ActiveCfg = Release|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|iPhone.Build.0 = Release|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|iPhone.Build.0 = Debug|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|iPhone.Build.0 = Debug|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|Any CPU.Build.0 = Release|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|iPhone.ActiveCfg = Release|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|iPhone.Build.0 = Release|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1740,6 +1799,7 @@ Global {E1240B49-7B4B-4371-A00E-068778C5CF0B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {D49233F8-F29C-47DD-9975-C4C9E4502720} = {E870DCD7-F46A-498D-83FC-D0FD13E0A11C} {3C471044-3640-45E3-B1B2-16D2FF8399EE} = {E870DCD7-F46A-498D-83FC-D0FD13E0A11C} + {AF227847-E65C-4BE9-BCE9-B551357788E0} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/Avalonia.sln.DotSettings b/Avalonia.sln.DotSettings index d61208c358..1361172fff 100644 --- a/Avalonia.sln.DotSettings +++ b/Avalonia.sln.DotSettings @@ -35,4 +35,5 @@ <Policy Inspect="True" Prefix="s_" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="False" Prefix="T" Suffix="" Style="AaBb" /> - <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> \ No newline at end of file + <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> + True \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 50476c81f1..1f26df9bbc 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - $(MSBuildThisFileDirectory)artifacts/nuget + $(MSBuildThisFileDirectory)build-intermediate/nuget diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 484fb4586f..0000000000 --- a/appveyor.yml +++ /dev/null @@ -1,23 +0,0 @@ -os: Visual Studio 2017 -skip_branch_with_pr: true -configuration: -- Release -environment: - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 - DOTNET_CLI_TELEMETRY_OPTOUT: 1 - NUGET_API_URL: https://www.nuget.org/api/v2/package - MYGET_API_KEY: - secure: OtVfyN3ErqQrDTnWH2HDfJDlCiu/i4/X4wFmK3ZXXP7HmCiXYPSbTjMPwwdOxRaK - MYGET_API_URL: https://www.myget.org/F/avalonia-ci/api/v2/package -init: -- ps: if (Test-Path env:nuget_address) {[System.IO.File]::AppendAllText("C:\Windows\System32\drivers\etc\hosts", "`n$($env:nuget_address)`tapi.nuget.org")} -before_build: -- git submodule update --init -build_script: -- ps: .\build.ps1 -Target "AppVeyor" -Configuration "$env:configuration" - -test: off -artifacts: - - path: artifacts\nuget\*.nupkg - - path: artifacts\zip\*.zip - - path: artifacts\inspectcode.xml diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f6929f8dee..8c5380e65e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -11,19 +11,18 @@ jobs: sudo apt-get install castxml - task: CmdLine@2 - displayName: 'Install Cake' + displayName: 'Install Nuke' inputs: script: | - dotnet tool install -g Cake.Tool --version 0.30.0 - + dotnet tool install --global Nuke.GlobalTool --version 0.12.3 - task: CmdLine@2 - displayName: 'Run Cake' + displayName: 'Run Nuke' inputs: script: | export PATH="$PATH:$HOME/.dotnet/tools" dotnet --info printenv - dotnet cake build.cake -target="Azure-Linux" -configuration="Release" + nuke --target CiAzureLinux --configuration=Release - task: PublishTestResults@2 inputs: @@ -55,13 +54,13 @@ jobs: script: brew install castxml - task: CmdLine@2 - displayName: 'Install Cake' + displayName: 'Install Nuke' inputs: script: | - dotnet tool install -g Cake.Tool --version 0.30.0 + dotnet tool install --global Nuke.GlobalTool --version 0.12.3 - task: CmdLine@2 - displayName: 'Run Cake' + displayName: 'Run Nuke' inputs: script: | export COREHOST_TRACE=0 @@ -72,7 +71,7 @@ jobs: export PATH="$PATH:$HOME/.dotnet/tools" dotnet --info printenv - dotnet cake build.cake -target="Azure-OSX" -configuration="Release" + nuke --target CiAzureOSX --configuration Release - task: PublishTestResults@2 inputs: @@ -97,17 +96,17 @@ jobs: vmImage: 'vs2017-win2016' steps: - task: CmdLine@2 - displayName: 'Install Cake' + displayName: 'Install Nuke' inputs: script: | - dotnet tool install -g Cake.Tool --version 0.30.0 + dotnet tool install --global Nuke.GlobalTool --version 0.12.3 - task: CmdLine@2 - displayName: 'Run Cake' + displayName: 'Run Nuke' inputs: script: | set PATH=%PATH%;%USERPROFILE%\.dotnet\tools - dotnet cake build.cake -target="Azure-Windows" -configuration="Release" + nuke --target CiAzureWindows --configuration Release - task: PublishTestResults@2 inputs: diff --git a/build.cake b/build.cake deleted file mode 100644 index f10a12c4e6..0000000000 --- a/build.cake +++ /dev/null @@ -1,312 +0,0 @@ -/////////////////////////////////////////////////////////////////////////////// -// TOOLS -/////////////////////////////////////////////////////////////////////////////// - -#tool "nuget:?package=NuGet.CommandLine&version=4.7.1" -#tool "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2018.2.3" -#tool "nuget:?package=xunit.runner.console&version=2.3.1" -#tool "nuget:?package=JetBrains.dotMemoryUnit&version=3.0.20171219.105559" - -/////////////////////////////////////////////////////////////////////////////// -// USINGS -/////////////////////////////////////////////////////////////////////////////// - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -/////////////////////////////////////////////////////////////////////////////// -// SCRIPTS -/////////////////////////////////////////////////////////////////////////////// - -#load "./parameters.cake" - -/////////////////////////////////////////////////////////////////////////////// -// SETUP -/////////////////////////////////////////////////////////////////////////////// - -Setup(context => -{ - var parameters = new Parameters(context); - - Information("Building version {0} of Avalonia ({1}) using version {2} of Cake.", - parameters.Version, - parameters.Configuration, - typeof(ICakeContext).Assembly.GetName().Version.ToString()); - - if (parameters.IsRunningOnAppVeyor) - { - Information("Repository Name: " + BuildSystem.AppVeyor.Environment.Repository.Name); - Information("Repository Branch: " + BuildSystem.AppVeyor.Environment.Repository.Branch); - } - Information("Target:" + context.TargetTask.Name); - Information("Configuration: " + parameters.Configuration); - Information("IsLocalBuild: " + parameters.IsLocalBuild); - Information("IsRunningOnUnix: " + parameters.IsRunningOnUnix); - Information("IsRunningOnWindows: " + parameters.IsRunningOnWindows); - Information("IsRunningOnAppVeyor: " + parameters.IsRunningOnAppVeyor); - Information("IsRunnongOnAzure:" + parameters.IsRunningOnAzure); - Information("IsPullRequest: " + parameters.IsPullRequest); - Information("IsMainRepo: " + parameters.IsMainRepo); - Information("IsMasterBranch: " + parameters.IsMasterBranch); - Information("IsReleaseBranch: " + parameters.IsReleaseBranch); - Information("IsTagged: " + parameters.IsTagged); - Information("IsReleasable: " + parameters.IsReleasable); - Information("IsMyGetRelease: " + parameters.IsMyGetRelease); - Information("IsNuGetRelease: " + parameters.IsNuGetRelease); - - return parameters; -}); - -/////////////////////////////////////////////////////////////////////////////// -// TEARDOWN -/////////////////////////////////////////////////////////////////////////////// - -Teardown((context, buildContext) => -{ - Information("Finished running tasks."); -}); - -/////////////////////////////////////////////////////////////////////////////// -// TASKS -/////////////////////////////////////////////////////////////////////////////// - -Task("Clean-Impl") - .Does(data => -{ - CleanDirectories(data.BuildDirs); - CleanDirectory(data.ArtifactsDir); - CleanDirectory(data.NugetRoot); - CleanDirectory(data.ZipRoot); - CleanDirectory(data.TestResultsRoot); -}); - -void DotNetCoreBuild(Parameters parameters) -{ - var settings = new DotNetCoreBuildSettings - { - Configuration = parameters.Configuration, - MSBuildSettings = new DotNetCoreMSBuildSettings - { - Properties = - { - { "PackageVersion", new [] { parameters.Version } } - } - } - }; - - DotNetCoreBuild(parameters.MSBuildSolution, settings); -} - -Task("Build-Impl") - .Does(data => -{ - if(data.IsRunningOnWindows) - { - MSBuild(data.MSBuildSolution, settings => { - settings.SetConfiguration(data.Configuration); - settings.SetVerbosity(Verbosity.Minimal); - settings.WithProperty("iOSRoslynPathHackRequired", "true"); - settings.WithProperty("PackageVersion", data.Version); - settings.UseToolVersion(MSBuildToolVersion.VS2017); - settings.WithRestore(); - }); - } - else - { - DotNetCoreBuild(data); - } -}); - -void RunCoreTest(string project, Parameters parameters, bool coreOnly = false) -{ - if(!project.EndsWith(".csproj")) - project = System.IO.Path.Combine(project, System.IO.Path.GetFileName(project)+".csproj"); - Information("Running tests from " + project); - var frameworks = new List(){"netcoreapp2.0"}; - foreach(var fw in frameworks) - { - if(!fw.StartsWith("netcoreapp") && coreOnly) - continue; - Information("Running for " + fw); - - var settings = new DotNetCoreTestSettings { - Configuration = parameters.Configuration, - Framework = fw, - NoBuild = true, - NoRestore = true - }; - - if (parameters.PublishTestResults) - { - settings.Logger = "trx"; - settings.ResultsDirectory = parameters.TestResultsRoot; - } - - DotNetCoreTest(project, settings); - } -} - -Task("Run-Unit-Tests-Impl") - .WithCriteria((context, data) => !data.SkipTests) - .Does(data => -{ - RunCoreTest("./tests/Avalonia.Base.UnitTests", data, false); - RunCoreTest("./tests/Avalonia.Controls.UnitTests", data, false); - RunCoreTest("./tests/Avalonia.Input.UnitTests", data, false); - RunCoreTest("./tests/Avalonia.Interactivity.UnitTests", data, false); - RunCoreTest("./tests/Avalonia.Layout.UnitTests", data, false); - RunCoreTest("./tests/Avalonia.Markup.UnitTests", data, false); - RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", data, false); - RunCoreTest("./tests/Avalonia.Styling.UnitTests", data, false); - RunCoreTest("./tests/Avalonia.Visuals.UnitTests", data, false); - RunCoreTest("./tests/Avalonia.Skia.UnitTests", data, false); - RunCoreTest("./tests/Avalonia.ReactiveUI.UnitTests", data, false); - if (data.IsRunningOnWindows) - { - RunCoreTest("./tests/Avalonia.Direct2D1.UnitTests", data, false); - } -}); - -Task("Run-Designer-Tests-Impl") - .WithCriteria((context, data) => !data.SkipTests) - .Does(data => -{ - RunCoreTest("./tests/Avalonia.DesignerSupport.Tests", data, false); -}); - -Task("Run-Render-Tests-Impl") - .WithCriteria((context, data) => !data.SkipTests) - .WithCriteria((context, data) => data.IsRunningOnWindows) - .Does(data => -{ - RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj", data, true); - RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj", data, true); -}); - -Task("Run-Leak-Tests-Impl") - .WithCriteria((context, data) => !data.SkipTests) - .WithCriteria((context, data) => data.IsRunningOnWindows) - .Does(() => -{ - var dotMemoryUnit = Context.Tools.Resolve("dotMemoryUnit.exe"); - var leakTestsExitCode = StartProcess(dotMemoryUnit, new ProcessSettings - { - Arguments = new ProcessArgumentBuilder() - .Append(Context.Tools.Resolve("xunit.console.x86.exe").FullPath) - .Append("--propagate-exit-code") - .Append("--") - .Append("tests\\Avalonia.LeakTests\\bin\\Release\\net461\\Avalonia.LeakTests.dll"), - Timeout = 120000 - }); - - if (leakTestsExitCode != 0) - { - throw new Exception("Leak Tests failed"); - } -}); - -Task("Zip-Files-Impl") - .Does(data => -{ - Zip(data.BinRoot, data.ZipCoreArtifacts); - - Zip(data.NugetRoot, data.ZipNuGetArtifacts); - - Zip(data.ZipSourceControlCatalogDesktopDirs, - data.ZipTargetControlCatalogDesktopDirs, - GetFiles(data.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.dll") + - GetFiles(data.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.config") + - GetFiles(data.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.so") + - GetFiles(data.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.dylib") + - GetFiles(data.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.exe")); -}); - -void DotNetCorePack(Parameters parameters) -{ - var settings = new DotNetCorePackSettings - { - Configuration = parameters.Configuration, - MSBuildSettings = new DotNetCoreMSBuildSettings - { - Properties = - { - { "PackageVersion", new [] { parameters.Version } } - } - } - }; - - DotNetCorePack(parameters.MSBuildSolution, settings); -} - -Task("Create-NuGet-Packages-Impl") - .Does(data => -{ - if(data.IsRunningOnWindows) - { - MSBuild(data.MSBuildSolution, settings => { - settings.SetConfiguration(data.Configuration); - settings.SetVerbosity(Verbosity.Minimal); - settings.WithProperty("iOSRoslynPathHackRequired", "true"); - settings.WithProperty("PackageVersion", data.Version); - settings.UseToolVersion(MSBuildToolVersion.VS2017); - settings.WithRestore(); - settings.WithTarget("Pack"); - }); - } - else - { - DotNetCorePack(data); - } -}); - -/////////////////////////////////////////////////////////////////////////////// -// TARGETS -/////////////////////////////////////////////////////////////////////////////// - -Task("Build") - .IsDependentOn("Clean-Impl") - .IsDependentOn("Build-Impl"); - -Task("Run-Tests") - .IsDependentOn("Build") - .IsDependentOn("Run-Unit-Tests-Impl") - .IsDependentOn("Run-Render-Tests-Impl") - .IsDependentOn("Run-Designer-Tests-Impl") - .IsDependentOn("Run-Leak-Tests-Impl"); - -Task("Package") - .IsDependentOn("Run-Tests") - .IsDependentOn("Create-NuGet-Packages-Impl"); - -Task("AppVeyor") - .IsDependentOn("Package") - .IsDependentOn("Zip-Files-Impl"); - -Task("Travis") - .IsDependentOn("Run-Tests"); - -Task("Azure-Linux") - .IsDependentOn("Run-Tests"); - -Task("Azure-OSX") - .IsDependentOn("Package") - .IsDependentOn("Zip-Files-Impl"); - -Task("Azure-Windows") - .IsDependentOn("Package") - .IsDependentOn("Zip-Files-Impl"); - -/////////////////////////////////////////////////////////////////////////////// -// EXECUTE -/////////////////////////////////////////////////////////////////////////////// - -var target = Context.Argument("target", "Default"); - -if (target == "Default") -{ - target = Context.IsRunningOnWindows() ? "Package" : "Run-Tests"; -} - -RunTarget(target); diff --git a/build.ps1 b/build.ps1 index 46696db2b2..57e2f80075 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,201 +1,69 @@ -########################################################################## -# This is the Cake bootstrapper script for PowerShell. -# This file was downloaded from https://github.com/cake-build/resources -# Feel free to change this file to fit your needs. -########################################################################## - -<# - -.SYNOPSIS -This is a Powershell script to bootstrap a Cake build. - -.DESCRIPTION -This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) -and execute your Cake build script with the parameters you provide. - -.PARAMETER Script -The build script to execute. -.PARAMETER Target -The build script target to run. -.PARAMETER Platform -The build platform to use. -.PARAMETER Configuration -The build configuration to use. -.PARAMETER Verbosity -Specifies the amount of information to be displayed. -.PARAMETER Experimental -Tells Cake to use the latest Roslyn release. -.PARAMETER WhatIf -Performs a dry run of the build script. -No tasks will be executed. -.PARAMETER Mono -Tells Cake to use the Mono scripting engine. -.PARAMETER SkipToolPackageRestore -Skips restoring of packages. -.PARAMETER SkipTests -Skips unit tests -.PARAMETER ScriptArgs -Remaining arguments are added here. - -.LINK -http://cakebuild.net - -#> - [CmdletBinding()] Param( - [string]$Script = "build.cake", - [string]$Target = "Default", - [ValidateSet("Any CPU", "x86", "x64", "NetCoreOnly", "iPhone")] - [string]$Platform = "Any CPU", - [ValidateSet("Release", "Debug")] - [string]$Configuration = "Release", - [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] - [string]$Verbosity = "Verbose", - [switch]$Experimental, - [Alias("DryRun","Noop")] - [switch]$WhatIf, - [switch]$Mono, - [switch]$SkipToolPackageRestore, + #[switch]$CustomParam, [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] - [string[]]$ScriptArgs + [string[]]$BuildArguments ) -[Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null -function MD5HashFile([string] $filePath) -{ - if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf)) - { - return $null - } - - [System.IO.Stream] $file = $null; - [System.Security.Cryptography.MD5] $md5 = $null; - try - { - $md5 = [System.Security.Cryptography.MD5]::Create() - $file = [System.IO.File]::OpenRead($filePath) - return [System.BitConverter]::ToString($md5.ComputeHash($file)) - } - finally - { - if ($file -ne $null) - { - $file.Dispose() - } - } -} - -Write-Host "Preparing to run build script..." - -if(!$PSScriptRoot){ - $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent -} +Write-Output "Windows PowerShell $($Host.Version)" -$TOOLS_DIR = Join-Path $PSScriptRoot "tools" -$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" -$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" -$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" -$PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" -$PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum" +Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { exit 1 } +$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent -# Should we use mono? -$UseMono = ""; -if($Mono.IsPresent) { - Write-Verbose -Message "Using the Mono based scripting engine." - $UseMono = "-mono" -} +########################################################################### +# CONFIGURATION +########################################################################### -# Should we use the new Roslyn? -$UseExperimental = ""; -if($Experimental.IsPresent -and !($Mono.IsPresent)) { - Write-Verbose -Message "Using experimental version of Roslyn." - $UseExperimental = "-experimental" -} +$BuildProjectFile = "$PSScriptRoot\nukebuild\_build.csproj" +$TempDirectory = "$PSScriptRoot\\.tmp" -# Is this a dry run? -$UseDryRun = ""; -if($WhatIf.IsPresent) { - $UseDryRun = "-dryrun" -} +$DotNetGlobalFile = "$PSScriptRoot\\global.json" +$DotNetInstallUrl = "https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.ps1" +$DotNetChannel = "Current" -# Is this a dry run? -$UseSkipTests = ""; -if($SkipTests.IsPresent) { - $UseSkipTests = "-skip-tests" -} +$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 +$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 +$env:NUGET_XMLDOC_MODE = "skip" -# Make sure tools folder exists -if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { - Write-Verbose -Message "Creating tools directory..." - New-Item -Path $TOOLS_DIR -Type directory | out-null -} +########################################################################### +# EXECUTION +########################################################################### -# Make sure that packages.config exist. -if (!(Test-Path $PACKAGES_CONFIG)) { - Write-Verbose -Message "Downloading packages.config..." - try { (New-Object System.Net.WebClient).DownloadFile("http://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) } catch { - Throw "Could not download packages.config." - } +function ExecSafe([scriptblock] $cmd) { + & $cmd + if ($LASTEXITCODE) { exit $LASTEXITCODE } } -# Try find NuGet.exe in path if not exists -if (!(Test-Path $NUGET_EXE)) { - Write-Verbose -Message "Trying to find nuget.exe in PATH..." - $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_) } - $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1 - if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { - Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." - $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName +# If global.json exists, load expected version +if (Test-Path $DotNetGlobalFile) { + $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) + if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { + $DotNetVersion = $DotNetGlobal.sdk.version } } -# Try download NuGet.exe if not exists -if (!(Test-Path $NUGET_EXE)) { - Write-Verbose -Message "Downloading NuGet.exe..." - try { - (New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE) - } catch { - Throw "Could not download NuGet.exe." - } +# If dotnet is installed locally, and expected version is not set or installation matches the expected version +if ((Get-Command "dotnet" -ErrorAction SilentlyContinue) -ne $null -and ` + (!(Test-Path variable:DotNetVersion) -or $(& dotnet --version) -eq $DotNetVersion)) { + $env:DOTNET_EXE = (Get-Command "dotnet").Path } - -# Save nuget.exe path to environment to be available to child processed -$ENV:NUGET_EXE = $NUGET_EXE - -# Restore tools from NuGet? -if(-Not $SkipToolPackageRestore.IsPresent) { - Push-Location - Set-Location $TOOLS_DIR - - # Check for changes in packages.config and remove installed tools if true. - [string] $md5Hash = MD5HashFile($PACKAGES_CONFIG) - if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or - ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { - Write-Verbose -Message "Missing or changed package.config hash..." - Remove-Item * -Recurse -Exclude packages.config,nuget.exe +else { + $DotNetDirectory = "$TempDirectory\dotnet-win" + $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" + + # Download install script + $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" + md -force $TempDirectory > $null + (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) + + # Install by channel or version + if (!(Test-Path variable:DotNetVersion)) { + ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } + } else { + ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } } - - Write-Verbose -Message "Restoring tools from NuGet..." - $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" - - if ($LASTEXITCODE -ne 0) { - Throw "An error occured while restoring NuGet tools." - } - else - { - $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII" - } - Write-Verbose -Message ($NuGetOutput | out-string) - Pop-Location } -# Make sure that Cake has been installed. -if (!(Test-Path $CAKE_EXE)) { - Throw "Could not find Cake.exe at $CAKE_EXE" -} +Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)" -# Start Cake -Write-Host "Running build script..." -Invoke-Expression "& `"$CAKE_EXE`" `"$Script`" -target=`"$Target`" -platform=`"$Platform`" -configuration=`"$Configuration`" -verbosity=`"$Verbosity`" $UseSkipTests $UseMono $UseDryRun $UseExperimental $ScriptArgs" -exit $LASTEXITCODE \ No newline at end of file +ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile -- $BuildArguments } diff --git a/build.sh b/build.sh index 206a55d171..40b1c225a6 100755 --- a/build.sh +++ b/build.sh @@ -1,105 +1,72 @@ #!/usr/bin/env bash -########################################################################## -# This is the Cake bootstrapper script for Linux and OS X. -# This file was downloaded from https://github.com/cake-build/resources -# Feel free to change this file to fit your needs. -########################################################################## +echo $(bash --version 2>&1 | head -n 1) -# Define directories. -SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) -TOOLS_DIR=$SCRIPT_DIR/tools -NUGET_EXE=$TOOLS_DIR/nuget.exe -CAKE_EXE=$TOOLS_DIR/Cake/Cake.exe -PACKAGES_CONFIG=$TOOLS_DIR/packages.config -PACKAGES_CONFIG_MD5=$TOOLS_DIR/packages.config.md5sum - -# Define md5sum or md5 depending on Linux/OSX -MD5_EXE= -if [[ "$(uname -s)" == "Darwin" ]]; then - MD5_EXE="md5 -r" -else - MD5_EXE="md5sum" -fi - -# Define default arguments. -SCRIPT="build.cake" -TARGET="Default" -CONFIGURATION="Release" -PLATFORM="Any CPU" -VERBOSITY="verbose" -DRYRUN= -SKIP_TESTS= -SHOW_VERSION=false -SCRIPT_ARGUMENTS=() - -# Parse arguments. +#CUSTOMPARAM=0 +BUILD_ARGUMENTS=() for i in "$@"; do - case $1 in - -s|--script) SCRIPT="$2"; shift ;; - -t|--target) TARGET="$2"; shift ;; - -p|--platform) PLATFORM="$2"; shift ;; - -c|--configuration) CONFIGURATION="$2"; shift ;; - --skip-tests) SKIP_TESTS="-skip-tests"; shift ;; - -v|--verbosity) VERBOSITY="$2"; shift ;; - -d|--dryrun) DRYRUN="-dryrun" ;; - --version) SHOW_VERSION=true ;; - --) shift; SCRIPT_ARGUMENTS+=("$@"); break ;; - *) SCRIPT_ARGUMENTS+=("$1") ;; + case $(echo $1 | awk '{print tolower($0)}') in + # -custom-param) CUSTOMPARAM=1;; + *) BUILD_ARGUMENTS+=("$1") ;; esac shift done -# Make sure the tools folder exist. -if [ ! -d "$TOOLS_DIR" ]; then - mkdir "$TOOLS_DIR" -fi +set -eo pipefail +SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) -# Make sure that packages.config exist. -if [ ! -f "$TOOLS_DIR/packages.config" ]; then - echo "Downloading packages.config..." - curl -Lsfo "$TOOLS_DIR/packages.config" http://cakebuild.net/download/bootstrapper/packages - if [ $? -ne 0 ]; then - echo "An error occured while downloading packages.config." - exit 1 - fi -fi +########################################################################### +# CONFIGURATION +########################################################################### -# Download NuGet if it does not exist. -if [ ! -f "$NUGET_EXE" ]; then - echo "Downloading NuGet..." - curl -Lsfo "$NUGET_EXE" https://dist.nuget.org/win-x86-commandline/latest/nuget.exe - if [ $? -ne 0 ]; then - echo "An error occured while downloading nuget.exe." - exit 1 - fi -fi +BUILD_PROJECT_FILE="$SCRIPT_DIR/nukebuild/_build.csproj" +TEMP_DIRECTORY="$SCRIPT_DIR//.tmp" -# Restore tools from NuGet. -pushd "$TOOLS_DIR" >/dev/null -if [ ! -f $PACKAGES_CONFIG_MD5 ] || [ "$( cat $PACKAGES_CONFIG_MD5 | sed 's/\r$//' )" != "$( $MD5_EXE $PACKAGES_CONFIG | awk '{ print $1 }' )" ]; then - find . -type d ! -name . | xargs rm -rf -fi +DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" +DOTNET_INSTALL_URL="https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.sh" +DOTNET_CHANNEL="Current" -mono "$NUGET_EXE" install -ExcludeVersion -if [ $? -ne 0 ]; then - echo "Could not restore NuGet packages." - exit 1 -fi +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 +export NUGET_XMLDOC_MODE="skip" -$MD5_EXE $PACKAGES_CONFIG | awk '{ print $1 }' >| $PACKAGES_CONFIG_MD5 +########################################################################### +# EXECUTION +########################################################################### -popd >/dev/null +function FirstJsonValue { + perl -nle 'print $1 if m{"'$1'": "([^"\-]+)",?}' <<< ${@:2} +} -# Make sure that Cake has been installed. -if [ ! -f "$CAKE_EXE" ]; then - echo "Could not find Cake.exe at '$CAKE_EXE'." - exit 1 +# If global.json exists, load expected version +if [ -f "$DOTNET_GLOBAL_FILE" ]; then + DOTNET_VERSION=$(FirstJsonValue "version" $(cat "$DOTNET_GLOBAL_FILE")) + if [ "$DOTNET_VERSION" == "" ]; then + unset DOTNET_VERSION + fi fi -# Start Cake -if $SHOW_VERSION; then - exec mono "$CAKE_EXE" -version +# If dotnet is installed locally, and expected version is not set or installation matches the expected version +if [[ -x "$(command -v dotnet)" && (-z ${DOTNET_VERSION+x} || $(dotnet --version) == "$DOTNET_VERSION") ]]; then + export DOTNET_EXE="$(command -v dotnet)" else - exec mono "$CAKE_EXE" $SCRIPT -verbosity=$VERBOSITY -platform="$PLATFORM" -configuration="$CONFIGURATION" -target=$TARGET $DRYRUN $SKIP_TESTS "${SCRIPT_ARGUMENTS[@]}" + DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" + export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" + + # Download install script + DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" + mkdir -p "$TEMP_DIRECTORY" + curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" + chmod +x "$DOTNET_INSTALL_FILE" + + # Install by channel or version + if [ -z ${DOTNET_VERSION+x} ]; then + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path + else + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path + fi fi + +echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" + +"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]} diff --git a/build/BuildTargets.targets b/build/BuildTargets.targets new file mode 100644 index 0000000000..08ec039ec7 --- /dev/null +++ b/build/BuildTargets.targets @@ -0,0 +1,8 @@ + + + $(MSBuildThisFileDirectory)\..\src\Avalonia.Build.Tasks\bin\$(Configuration)\netstandard2.0\Avalonia.Build.Tasks.dll + true + + + + diff --git a/build/Magick.NET-Q16-AnyCPU.props b/build/Magick.NET-Q16-AnyCPU.props index 4e600a1c11..21d9cdcb1f 100644 --- a/build/Magick.NET-Q16-AnyCPU.props +++ b/build/Magick.NET-Q16-AnyCPU.props @@ -1,5 +1,5 @@ - + diff --git a/build/ReferenceCoreLibraries.props b/build/ReferenceCoreLibraries.props index bd9d6ad843..cffc2e0324 100644 --- a/build/ReferenceCoreLibraries.props +++ b/build/ReferenceCoreLibraries.props @@ -1,5 +1,4 @@ - diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index 35c979a95e..a43c99e978 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/cake.config b/cake.config deleted file mode 100644 index 8089cd4084..0000000000 --- a/cake.config +++ /dev/null @@ -1,15 +0,0 @@ -; This is the default configuration file for Cake. -; This file was downloaded from https://github.com/cake-build/resources - -[Nuget] -Source=https://api.nuget.org/v3/index.json -UseInProcessClient=true -LoadDependencies=false - -[Paths] -Tools=./tools -Addins=./tools/Addins -Modules=./tools/Modules - -[Settings] -SkipVerification=false diff --git a/native/Avalonia.Native/src/OSX/gl.mm b/native/Avalonia.Native/src/OSX/gl.mm index a62098a074..083adc927d 100644 --- a/native/Avalonia.Native/src/OSX/gl.mm +++ b/native/Avalonia.Native/src/OSX/gl.mm @@ -208,9 +208,8 @@ public: virtual ~AvnGlRenderingSession() { - glFlush(); [_context flushBuffer]; - [_context setView:nil]; + [NSOpenGLContext clearCurrentContext]; CGLUnlockContext([_context CGLContextObj]); [_view unlockFocus]; } @@ -241,9 +240,8 @@ public: auto gl = _context; CGLLockContext([_context CGLContextObj]); [gl setView: _view]; + [gl update]; [gl makeCurrentContext]; - auto frame = [_view frame]; - *ret = new AvnGlRenderingSession(_window, _view, gl); return S_OK; } diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index ae00bde780..ef5dcb5b2e 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -425,7 +425,7 @@ private: [[Window parentWindow] removeChildWindow:Window]; WindowBaseImpl::Show(); - return SetWindowState(_lastWindowState); + return SetWindowState(Normal); } } @@ -1184,6 +1184,25 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } } +- (void)windowDidMiniaturize:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->WindowStateChanged(); + } +} + +- (void)windowDidDeminiaturize:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->WindowStateChanged(); + } +} - (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame { diff --git a/nukebuild/.editorconfig b/nukebuild/.editorconfig new file mode 100644 index 0000000000..d6009b3c0f --- /dev/null +++ b/nukebuild/.editorconfig @@ -0,0 +1,8 @@ +# editorconfig.org + +# top-most EditorConfig file +root = false + +# C# files +[*.cs] +dotnet_style_require_accessibility_modifiers = never diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs new file mode 100644 index 0000000000..a14842cfd3 --- /dev/null +++ b/nukebuild/Build.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using Nuke.Common; +using Nuke.Common.Git; +using Nuke.Common.ProjectModel; +using Nuke.Common.Tooling; +using Nuke.Common.Tools.DotNet; +using Nuke.Common.Tools.MSBuild; +using Nuke.Common.Utilities; +using static Nuke.Common.EnvironmentInfo; +using static Nuke.Common.IO.FileSystemTasks; +using static Nuke.Common.IO.PathConstruction; +using static Nuke.Common.Tools.MSBuild.MSBuildTasks; +using static Nuke.Common.Tools.DotNet.DotNetTasks; +using static Nuke.Common.Tools.Xunit.XunitTasks; + + +/* + Before editing this file, install support plugin for your IDE, + 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 +{ + BuildParameters Parameters { get; set; } + protected override void OnBuildInitialized() + { + Parameters = new BuildParameters(this); + Information("Building version {0} of Avalonia ({1}) using version {2} of Nuke.", + Parameters.Version, + Parameters.Configuration, + typeof(NukeBuild).Assembly.GetName().Version.ToString()); + + if (Parameters.IsLocalBuild) + { + Information("Repository Name: " + Parameters.RepositoryName); + Information("Repository Branch: " + Parameters.RepositoryBranch); + } + Information("Configuration: " + Parameters.Configuration); + Information("IsLocalBuild: " + Parameters.IsLocalBuild); + Information("IsRunningOnUnix: " + Parameters.IsRunningOnUnix); + Information("IsRunningOnWindows: " + Parameters.IsRunningOnWindows); + Information("IsRunningOnAzure:" + Parameters.IsRunningOnAzure); + Information("IsPullRequest: " + Parameters.IsPullRequest); + Information("IsMainRepo: " + Parameters.IsMainRepo); + Information("IsMasterBranch: " + Parameters.IsMasterBranch); + Information("IsReleaseBranch: " + Parameters.IsReleaseBranch); + Information("IsReleasable: " + Parameters.IsReleasable); + Information("IsMyGetRelease: " + Parameters.IsMyGetRelease); + Information("IsNuGetRelease: " + Parameters.IsNuGetRelease); + } + + Target Clean => _ => _.Executes(() => + { + DeleteDirectories(Parameters.BuildDirs); + EnsureCleanDirectories(Parameters.BuildDirs); + EnsureCleanDirectory(Parameters.ArtifactsDir); + EnsureCleanDirectory(Parameters.NugetIntermediateRoot); + EnsureCleanDirectory(Parameters.NugetRoot); + EnsureCleanDirectory(Parameters.ZipRoot); + EnsureCleanDirectory(Parameters.TestResultsRoot); + }); + + Target Compile => _ => _ + .DependsOn(Clean) + .Executes(() => + { + + if (Parameters.IsRunningOnWindows) + MSBuild(Parameters.MSBuildSolution, c => c + .SetArgumentConfigurator(a => a.Add("/r")) + .SetConfiguration(Parameters.Configuration) + .SetVerbosity(MSBuildVerbosity.Minimal) + .AddProperty("PackageVersion", Parameters.Version) + .AddProperty("iOSRoslynPathHackRequired", "true") + .SetToolsVersion(MSBuildToolsVersion._15_0) + .AddTargets("Build") + ); + + else + DotNetBuild(Parameters.MSBuildSolution, c => c + .AddProperty("PackageVersion", Parameters.Version) + .SetConfiguration(Parameters.Configuration) + ); + }); + + void RunCoreTest(string project, bool coreOnly = false) + { + if(!project.EndsWith(".csproj")) + project = System.IO.Path.Combine(project, System.IO.Path.GetFileName(project)+".csproj"); + Information("Running tests from " + project); + var frameworks = new List(){"netcoreapp2.0"}; + foreach(var fw in frameworks) + { + if(!fw.StartsWith("netcoreapp") && coreOnly) + 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; + }); + } + } + + Target RunCoreLibsTests => _ => _ + .OnlyWhen(() => !Parameters.SkipTests) + .DependsOn(Compile) + .Executes(() => + { + RunCoreTest("./tests/Avalonia.Animation.UnitTests", false); + RunCoreTest("./tests/Avalonia.Base.UnitTests", false); + RunCoreTest("./tests/Avalonia.Controls.UnitTests", false); + RunCoreTest("./tests/Avalonia.Input.UnitTests", false); + RunCoreTest("./tests/Avalonia.Interactivity.UnitTests", false); + RunCoreTest("./tests/Avalonia.Layout.UnitTests", false); + RunCoreTest("./tests/Avalonia.Markup.UnitTests", false); + RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", false); + RunCoreTest("./tests/Avalonia.Styling.UnitTests", false); + RunCoreTest("./tests/Avalonia.Visuals.UnitTests", false); + RunCoreTest("./tests/Avalonia.Skia.UnitTests", false); + RunCoreTest("./tests/Avalonia.ReactiveUI.UnitTests", false); + }); + + Target RunRenderTests => _ => _ + .OnlyWhen(() => !Parameters.SkipTests) + .DependsOn(Compile) + .Executes(() => + { + RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj", true); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj", true); + }); + + Target RunDesignerTests => _ => _ + .OnlyWhen(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) + .DependsOn(Compile) + .Executes(() => + { + RunCoreTest("./tests/Avalonia.DesignerSupport.Tests", false); + }); + + [PackageExecutable("JetBrains.dotMemoryUnit", "dotMemoryUnit.exe")] readonly Tool DotMemoryUnit; + + Target RunLeakTests => _ => _ + .OnlyWhen(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) + .DependsOn(Compile) + .Executes(() => + { + var testAssembly = "tests\\Avalonia.LeakTests\\bin\\Release\\net461\\Avalonia.LeakTests.dll"; + DotMemoryUnit( + $"{XunitPath.DoubleQuoteIfNeeded()} --propagate-exit-code -- {testAssembly}", + timeout: 120_000); + }); + + Target ZipFiles => _ => _ + .After(CreateNugetPackages, Compile, RunCoreLibsTests, Package) + .Executes(() => + { + var data = Parameters; + Zip(data.ZipCoreArtifacts, data.BinRoot); + Zip(data.ZipNuGetArtifacts, data.NugetRoot); + Zip(data.ZipTargetControlCatalogDesktopDir, + GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.dll").Concat( + GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.config")).Concat( + GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.so")).Concat( + GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.dylib")).Concat( + GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.exe"))); + }); + + Target CreateIntermediateNugetPackages => _ => _ + .DependsOn(Compile) + .After(RunTests) + .Executes(() => + { + if (Parameters.IsRunningOnWindows) + + MSBuild(Parameters.MSBuildSolution, c => c + .SetConfiguration(Parameters.Configuration) + .SetVerbosity(MSBuildVerbosity.Minimal) + .AddProperty("PackageVersion", Parameters.Version) + .AddProperty("iOSRoslynPathHackRequired", "true") + .SetToolsVersion(MSBuildToolsVersion._15_0) + .AddTargets("Pack")); + else + DotNetPack(Parameters.MSBuildSolution, c => + c.SetConfiguration(Parameters.Configuration) + .AddProperty("PackageVersion", Parameters.Version)); + }); + + Target CreateNugetPackages => _ => _ + .DependsOn(CreateIntermediateNugetPackages) + .Executes(() => + { + var config = Numerge.MergeConfiguration.LoadFile(RootDirectory / "nukebuild" / "numerge.config"); + EnsureCleanDirectory(Parameters.NugetRoot); + if(!Numerge.NugetPackageMerger.Merge(Parameters.NugetIntermediateRoot, Parameters.NugetRoot, config, + 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); + +} diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs new file mode 100644 index 0000000000..5f8ec7267e --- /dev/null +++ b/nukebuild/BuildParameters.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +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.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 NukeArgConfiguration { get; set; } + + [Parameter("skip-tests")] + public bool NukeArgSkipTests { get; set; } + + [Parameter("force-nuget-version")] + public string NukeArgForceNugetVersion { get; set; } + + public class BuildParameters + { + public string Configuration { get; } + public bool SkipTests { get; } + public string MainRepo { get; } + public string MasterBranch { get; } + public string RepositoryName { get; } + public string RepositoryBranch { get; } + public string ReleaseConfiguration { get; } + public string ReleaseBranchPrefix { get; } + public string MSBuildSolution { get; } + public bool IsLocalBuild { get; } + public bool IsRunningOnUnix { get; } + public bool IsRunningOnWindows { get; } + public bool IsRunningOnAzure { get; } + public bool IsPullRequest { get; } + public bool IsMainRepo { get; } + public bool IsMasterBranch { get; } + public bool IsReleaseBranch { get; } + public bool IsReleasable { get; } + public bool IsMyGetRelease { get; } + public bool IsNuGetRelease { get; } + public bool PublishTestResults { get; } + public string Version { get; } + public AbsolutePath ArtifactsDir { get; } + public AbsolutePath NugetIntermediateRoot { get; } + public AbsolutePath NugetRoot { get; } + public AbsolutePath ZipRoot { get; } + public AbsolutePath BinRoot { get; } + public AbsolutePath TestResultsRoot { get; } + public string DirSuffix { get; } + public List BuildDirs { get; } + public string FileZipSuffix { get; } + public AbsolutePath ZipCoreArtifacts { get; } + public AbsolutePath ZipNuGetArtifacts { get; } + public AbsolutePath ZipSourceControlCatalogDesktopDir { get; } + public AbsolutePath ZipTargetControlCatalogDesktopDir { get; } + + + public BuildParameters(Build b) + { + // ARGUMENTS + Configuration = b.NukeArgConfiguration ?? "Release"; + SkipTests = b.NukeArgSkipTests; + + // CONFIGURATION + MainRepo = "https://github.com/AvaloniaUI/Avalonia"; + MasterBranch = "refs/heads/master"; + ReleaseBranchPrefix = "refs/heads/release/"; + ReleaseConfiguration = "Release"; + MSBuildSolution = RootDirectory / "dirs.proj"; + + // PARAMETERS + IsLocalBuild = Host == HostType.Console; + IsRunningOnUnix = Environment.OSVersion.Platform == PlatformID.Unix || + Environment.OSVersion.Platform == PlatformID.MacOSX; + IsRunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + IsRunningOnAzure = Host == HostType.TeamServices || + 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); + } + IsMainRepo = + StringComparer.OrdinalIgnoreCase.Equals(MainRepo, + RepositoryName); + IsMasterBranch = StringComparer.OrdinalIgnoreCase.Equals(MasterBranch, + RepositoryBranch); + IsReleaseBranch = RepositoryBranch?.StartsWith(ReleaseBranchPrefix, StringComparison.OrdinalIgnoreCase) == + true; + + IsReleasable = StringComparer.OrdinalIgnoreCase.Equals(ReleaseConfiguration, Configuration); + IsMyGetRelease = IsReleasable; + IsNuGetRelease = IsMainRepo && IsReleasable && IsReleaseBranch; + + // VERSION + Version = b.NukeArgForceNugetVersion ?? GetVersion(); + + if (IsRunningOnAzure) + { + if (!IsNuGetRelease) + { + // Use AssemblyVersion with Build as version + Version += "-build" + Environment.GetEnvironmentVariable("BUILD_BUILDID") + "-beta"; + } + + PublishTestResults = true; + } + + // DIRECTORIES + ArtifactsDir = RootDirectory / "artifacts"; + NugetRoot = ArtifactsDir / "nuget"; + NugetIntermediateRoot = RootDirectory / "build-intermediate" / "nuget"; + ZipRoot = ArtifactsDir / "zip"; + BinRoot = ArtifactsDir / "bin"; + TestResultsRoot = ArtifactsDir / "test-results"; + BuildDirs = GlobDirectories(RootDirectory, "**bin").Concat(GlobDirectories(RootDirectory, "**obj")).ToList(); + DirSuffix = Configuration; + FileZipSuffix = Version + ".zip"; + ZipCoreArtifacts = ZipRoot / ("Avalonia-" + FileZipSuffix); + ZipNuGetArtifacts = ZipRoot / ("Avalonia-NuGet-" + FileZipSuffix); + ZipSourceControlCatalogDesktopDir = + RootDirectory / ("samples/ControlCatalog.Desktop/bin/" + DirSuffix + "/net461"); + ZipTargetControlCatalogDesktopDir = ZipRoot / ("ControlCatalog.Desktop-" + FileZipSuffix); + } + + string GetVersion() + { + var xdoc = XDocument.Load(RootDirectory / "build/SharedVersion.props"); + return xdoc.Descendants().First(x => x.Name.LocalName == "Version").Value; + } + } + +} diff --git a/nukebuild/Numerge b/nukebuild/Numerge new file mode 160000 index 0000000000..4464343aef --- /dev/null +++ b/nukebuild/Numerge @@ -0,0 +1 @@ +Subproject commit 4464343aef5c8ab7a42fcb20a483a6058199f8b8 diff --git a/nukebuild/Shims.cs b/nukebuild/Shims.cs new file mode 100644 index 0000000000..461d617643 --- /dev/null +++ b/nukebuild/Shims.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using Nuke.Common; +using Nuke.Common.IO; +using Numerge; + +public partial class Build +{ + static void Information(string info) + { + Logger.Info(info); + } + + static void Information(string info, params object[] args) + { + Logger.Info(info, args); + } + + private void Zip(PathConstruction.AbsolutePath target, params string[] paths) => Zip(target, paths.AsEnumerable()); + + private void Zip(PathConstruction.AbsolutePath target, IEnumerable paths) + { + var targetPath = target.ToString(); + bool finished = false, atLeastOneFileAdded = false; + try + { + using (var targetStream = File.Create(targetPath)) + using(var archive = new System.IO.Compression.ZipArchive(targetStream, ZipArchiveMode.Create)) + { + void AddFile(string path, string relativePath) + { + var e = archive.CreateEntry(relativePath.Replace("\\", "/"), CompressionLevel.Optimal); + using (var entryStream = e.Open()) + using (var fileStream = File.OpenRead(path)) + fileStream.CopyTo(entryStream); + atLeastOneFileAdded = true; + } + + foreach (var path in paths) + { + if (Directory.Exists(path)) + { + var dirInfo = new DirectoryInfo(path); + var rootPath = Path.GetDirectoryName(dirInfo.FullName); + foreach(var fsEntry in dirInfo.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) + { + if (fsEntry is FileInfo) + { + var relPath = Path.GetRelativePath(rootPath, fsEntry.FullName); + AddFile(fsEntry.FullName, relPath); + } + } + } + else if(File.Exists(path)) + { + var name = Path.GetFileName(path); + AddFile(path, name); + } + } + } + + finished = true; + } + finally + { + try + { + if (!finished || !atLeastOneFileAdded) + File.Delete(targetPath); + } + catch + { + //Ignore + } + } + } + + class NumergeNukeLogger : INumergeLogger + { + public void Log(NumergeLogLevel level, string message) + { + if(level == NumergeLogLevel.Error) + Logger.Error(message); + else if (level == NumergeLogLevel.Warning) + Logger.Warn(message); + else + Logger.Info(message); + } + } +} diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj new file mode 100644 index 0000000000..e02acff007 --- /dev/null +++ b/nukebuild/_build.csproj @@ -0,0 +1,37 @@ + + + + Exe + netcoreapp2.0 + false + + False + CS0649;CS0169 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nukebuild/_build.csproj.DotSettings b/nukebuild/_build.csproj.DotSettings new file mode 100644 index 0000000000..9aac7d8e8d --- /dev/null +++ b/nukebuild/_build.csproj.DotSettings @@ -0,0 +1,24 @@ + + False + Implicit + Implicit + ExpressionBody + 0 + NEXT_LINE + True + False + 120 + IF_OWNER_IS_SINGLE_LINE + WRAP_IF_LONG + False + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + True + True + True + True + True + True + True + True + True diff --git a/nukebuild/numerge.config b/nukebuild/numerge.config new file mode 100644 index 0000000000..e4e15d693d --- /dev/null +++ b/nukebuild/numerge.config @@ -0,0 +1,23 @@ +{ + "Packages": + [ + { + "Id": "Avalonia", + "MergeAll": true, + "Exclude": ["Avalonia.Remote.Protocol"], + "IncomingIncludeAssetsOverride": "", + "Merge": [ + { + "Id": "Avalonia.Build.Tasks", + "IgnoreMissingFrameworkBinaries": true, + "DoNotMergeDependencies": true + }, + { + "Id": "Avalonia.DesktopRuntime", + "IgnoreMissingFrameworkBinaries": true, + "IgnoreMissingFrameworkDependencies": true + } + ] + } + ] +} diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index dee42fb795..489cb228aa 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -1,10 +1,12 @@ - + netstandard2.0;net461;netcoreapp2.0 - + + + @@ -27,14 +29,18 @@ false None - <_PackageFiles Include="Avalonia.props"> - build/Avalonia.props - false - None - - + + + true + build\ + + + true + build\ + + diff --git a/packages/Avalonia/Avalonia.props b/packages/Avalonia/Avalonia.props index 8234dc4fde..6f21971d3d 100644 --- a/packages/Avalonia/Avalonia.props +++ b/packages/Avalonia/Avalonia.props @@ -2,5 +2,8 @@ $(MSBuildThisFileDirectory)\..\tools\netcoreapp2.0\designer\Avalonia.Designer.HostApp.dll $(MSBuildThisFileDirectory)\..\tools\net461\designer\Avalonia.Designer.HostApp.exe + $(MSBuildThisFileDirectory)\..\tools\netstandard2.0\Avalonia.Build.Tasks.dll + false + diff --git a/packages/Avalonia/Avalonia.targets b/packages/Avalonia/Avalonia.targets new file mode 100644 index 0000000000..50306f2cdc --- /dev/null +++ b/packages/Avalonia/Avalonia.targets @@ -0,0 +1,3 @@ + + + diff --git a/packages/Avalonia/AvaloniaBuildTasks.props b/packages/Avalonia/AvaloniaBuildTasks.props new file mode 100644 index 0000000000..30bafa37ee --- /dev/null +++ b/packages/Avalonia/AvaloniaBuildTasks.props @@ -0,0 +1,3 @@ + + + diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets new file mode 100644 index 0000000000..10f971cc4c --- /dev/null +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -0,0 +1,43 @@ + + + <_AvaloniaUseExternalMSBuild>$(AvaloniaUseExternalMSBuild) + <_AvaloniaUseExternalMSBuild Condition="'$(_AvaloniaForceInternalMSBuild)' == 'true'">false + + + + + + + + $(IntermediateOutputPath)/Avalonia/resources + + + + !AvaloniaResources + + + + + + + + + + + + + + diff --git a/readme.md b/readme.md index 9d113cf2ef..9280125323 100644 --- a/readme.md +++ b/readme.md @@ -2,9 +2,9 @@ # Avalonia -| Gitter Chat | Build Status (Win, Linux, OSX) | Appveyor Build Status | Open Collective | -|---|---|---|---| -| [![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) | [![Build status](https://ci.appveyor.com/api/projects/status/hubk3k0w9idyibfg/branch/master?svg=true)](https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master) | [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) | +| Gitter Chat | Build Status (Win, Linux, OSX) | Open Collective | +|---|---|---| +| [![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) | ## About diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 1f53dedc14..57c8b700df 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -23,6 +23,7 @@ namespace ControlCatalog.NetCore break; } } + if (args.Contains("--fbdev")) AppBuilder.Configure().InitializeWithLinuxFramebuffer(tl => { @@ -30,7 +31,12 @@ namespace ControlCatalog.NetCore System.Threading.ThreadPool.QueueUserWorkItem(_ => ConsoleSilencer()); }); else - BuildAvaloniaApp().Start(); + BuildAvaloniaApp().Start(AppMain, args); + } + + static void AppMain(Application app, string[] args) + { + app.Run(new MainWindow()); } /// @@ -46,4 +52,4 @@ namespace ControlCatalog.NetCore Console.ReadKey(true); } } -} \ No newline at end of file +} diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 19a22bb6ed..67220e6dd0 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -1,7 +1,9 @@ - + - - + + - + diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index d0a746f87d..463a8a8db6 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -6,10 +6,11 @@ %(Filename) - + Designer - - + + + @@ -24,4 +25,5 @@ + diff --git a/samples/ControlCatalog/DecoratedWindow.xaml b/samples/ControlCatalog/DecoratedWindow.xaml index b2f6497caa..cb6016b324 100644 --- a/samples/ControlCatalog/DecoratedWindow.xaml +++ b/samples/ControlCatalog/DecoratedWindow.xaml @@ -1,7 +1,8 @@ - + xmlns:local="clr-namespace:ControlCatalog" HasSystemDecorations="False" Name="Window"> @@ -18,7 +19,13 @@ - Hello world! + + Hello world! + + Decorated + + CanResize + @@ -30,4 +37,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index ec3bf799b4..0f7f2e80a8 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -1,33 +1,42 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:Class="ControlCatalog.MainView" + Background="{DynamicResource ThemeBackgroundBrush}" + Foreground="{DynamicResource ThemeForegroundBrush}" + FontSize="{DynamicResource FontSizeNormal}"> + + + Light + Dark + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 0be5d25a09..a498b17bdd 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -2,6 +2,7 @@ using System.Collections; using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Styling; using Avalonia.Platform; using ControlCatalog.Pages; @@ -27,6 +28,22 @@ namespace ControlCatalog }); } + var light = AvaloniaXamlLoader.Parse(@""); + var dark = AvaloniaXamlLoader.Parse(@""); + var themes = this.Find("Themes"); + themes.SelectionChanged += (sender, e) => + { + switch (themes.SelectedIndex) + { + case 0: + Styles[0] = light; + break; + case 1: + Styles[0] = dark; + break; + } + }; + Styles.Add(light); } private void InitializeComponent() diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 7029273a84..4bae60b2a6 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -1,6 +1,8 @@  + Icon="/Assets/test_icon.ico" + xmlns:local="clr-namespace:ControlCatalog" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:Class="ControlCatalog.MainWindow"> - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml index 28bdc7ac71..0ca3567970 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml @@ -1,4 +1,6 @@ - + AutoCompleteBox A control into which the user can input text diff --git a/samples/ControlCatalog/Pages/BorderPage.xaml b/samples/ControlCatalog/Pages/BorderPage.xaml index 09f591d59d..c30056d5e5 100644 --- a/samples/ControlCatalog/Pages/BorderPage.xaml +++ b/samples/ControlCatalog/Pages/BorderPage.xaml @@ -1,4 +1,6 @@ - + Border A control which decorates a child with a border and background diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml b/samples/ControlCatalog/Pages/ButtonPage.xaml index a4690e32e1..39d89590c2 100644 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml +++ b/samples/ControlCatalog/Pages/ButtonPage.xaml @@ -1,5 +1,6 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:Class="ControlCatalog.Pages.ButtonPage"> Button A button control diff --git a/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml index fba15f6e77..c3f9f65dd9 100644 --- a/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml +++ b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml @@ -1,5 +1,6 @@  + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:Class="ControlCatalog.Pages.ButtonSpinnerPage"> ButtonSpinner diff --git a/samples/ControlCatalog/Pages/CalendarPage.xaml b/samples/ControlCatalog/Pages/CalendarPage.xaml index c47fd766fb..dbd0dba37b 100644 --- a/samples/ControlCatalog/Pages/CalendarPage.xaml +++ b/samples/ControlCatalog/Pages/CalendarPage.xaml @@ -1,5 +1,6 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:Class="ControlCatalog.Pages.CalendarPage"> Calendar A calendar control for selecting dates diff --git a/samples/ControlCatalog/Pages/CanvasPage.xaml b/samples/ControlCatalog/Pages/CanvasPage.xaml index d6c138a4f7..d154e717a4 100644 --- a/samples/ControlCatalog/Pages/CanvasPage.xaml +++ b/samples/ControlCatalog/Pages/CanvasPage.xaml @@ -1,4 +1,6 @@ - + Canvas A panel which lays out its children by explicit coordinates diff --git a/samples/ControlCatalog/Pages/CarouselPage.xaml b/samples/ControlCatalog/Pages/CarouselPage.xaml index cf9b13c00c..b08ff565e3 100644 --- a/samples/ControlCatalog/Pages/CarouselPage.xaml +++ b/samples/ControlCatalog/Pages/CarouselPage.xaml @@ -1,4 +1,6 @@ - + Carousel An items control that displays its items as pages that fill the control. @@ -11,9 +13,9 @@ - - - + + + diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml b/samples/ControlCatalog/Pages/DragAndDropPage.xaml index 1f3cd3ff71..9bfcd90149 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml @@ -1,4 +1,6 @@ - + Drag+Drop Example of Drag+Drop capabilities diff --git a/samples/ControlCatalog/Pages/DropDownPage.xaml b/samples/ControlCatalog/Pages/DropDownPage.xaml index 5e2a3102e7..864d2be49c 100644 --- a/samples/ControlCatalog/Pages/DropDownPage.xaml +++ b/samples/ControlCatalog/Pages/DropDownPage.xaml @@ -1,4 +1,6 @@ - + DropDown A drop-down list. diff --git a/samples/ControlCatalog/Pages/ExpanderPage.xaml b/samples/ControlCatalog/Pages/ExpanderPage.xaml index 91440929f5..605eff4fce 100644 --- a/samples/ControlCatalog/Pages/ExpanderPage.xaml +++ b/samples/ControlCatalog/Pages/ExpanderPage.xaml @@ -1,4 +1,6 @@ - + Expander Expands to show nested content diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml b/samples/ControlCatalog/Pages/ImagePage.xaml index 78fbf90192..b44fac27cb 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml +++ b/samples/ControlCatalog/Pages/ImagePage.xaml @@ -1,4 +1,6 @@ - + Image Displays an image @@ -9,28 +11,28 @@ Spacing="16"> No Stretch - Fill - Uniform - UniformToFill - @@ -40,4 +42,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml b/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml index f7e1c08cac..446dfd7ce1 100644 --- a/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml +++ b/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml @@ -1,5 +1,6 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:Class="ControlCatalog.Pages.LayoutTransformControlPage"> Rotation @@ -23,4 +24,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index 3dd8be91c2..4783c8cfb8 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -1,4 +1,6 @@ - + ListBox Hosts a collection of ListBoxItem. diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml b/samples/ControlCatalog/Pages/MenuPage.xaml index c5aa35312c..e1a5cf2c5a 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml +++ b/samples/ControlCatalog/Pages/MenuPage.xaml @@ -1,4 +1,6 @@ - + Menu A window menu @@ -19,7 +21,7 @@ - + diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml index e263f59b8d..07e5581304 100644 --- a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml +++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml @@ -1,5 +1,6 @@  + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:Class="ControlCatalog.Pages.NumericUpDownPage"> Numeric up-down control Numeric up-down control provides a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel. diff --git a/samples/ControlCatalog/Pages/ProgressBarPage.xaml b/samples/ControlCatalog/Pages/ProgressBarPage.xaml index a40aa0ea9a..39bbf391bb 100644 --- a/samples/ControlCatalog/Pages/ProgressBarPage.xaml +++ b/samples/ControlCatalog/Pages/ProgressBarPage.xaml @@ -1,4 +1,6 @@ - + ProgressBar A progress bar control @@ -21,4 +23,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/RadioButtonPage.xaml b/samples/ControlCatalog/Pages/RadioButtonPage.xaml index 9525f6187e..bf31c40e2a 100644 --- a/samples/ControlCatalog/Pages/RadioButtonPage.xaml +++ b/samples/ControlCatalog/Pages/RadioButtonPage.xaml @@ -1,5 +1,6 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:Class="ControlCatalog.Pages.RadioButtonPage"> RadioButton Allows the selection of a single option of many @@ -37,4 +38,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/SliderPage.xaml b/samples/ControlCatalog/Pages/SliderPage.xaml index 6db71b5fcc..58f7b881fe 100644 --- a/samples/ControlCatalog/Pages/SliderPage.xaml +++ b/samples/ControlCatalog/Pages/SliderPage.xaml @@ -1,4 +1,6 @@ - + Slider A control that lets the user select from a range of values by moving a Thumb control along a Track. @@ -18,4 +20,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/TabControlPage.xaml b/samples/ControlCatalog/Pages/TabControlPage.xaml new file mode 100644 index 0000000000..01ddc0ddca --- /dev/null +++ b/samples/ControlCatalog/Pages/TabControlPage.xaml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + This is the first page in the TabControl. + + + + + + This is the second page in the TabControl. + + + + + You should not see this. + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tab Placement: + + Left + Bottom + Right + Top + + + + + diff --git a/samples/ControlCatalog/Pages/TabControlPage.xaml.cs b/samples/ControlCatalog/Pages/TabControlPage.xaml.cs new file mode 100644 index 0000000000..a38a3ab4cb --- /dev/null +++ b/samples/ControlCatalog/Pages/TabControlPage.xaml.cs @@ -0,0 +1,80 @@ +using System; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +using ReactiveUI; + +namespace ControlCatalog.Pages +{ + using System.Collections.Generic; + + public class TabControlPage : UserControl + { + public TabControlPage() + { + InitializeComponent(); + + DataContext = new PageViewModel + { + Tabs = new[] + { + new TabItemViewModel + { + Header = "Arch", + Text = "This is the first templated tab page.", + Image = LoadBitmap("avares://ControlCatalog/Assets/delicate-arch-896885_640.jpg"), + }, + new TabItemViewModel + { + Header = "Leaf", + Text = "This is the second templated tab page.", + Image = LoadBitmap("avares://ControlCatalog/Assets/maple-leaf-888807_640.jpg"), + }, + new TabItemViewModel + { + Header = "Disabled", + Text = "You should not see this.", + IsEnabled = false, + }, + }, + TabPlacement = Dock.Top, + }; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private IBitmap LoadBitmap(string uri) + { + var assets = AvaloniaLocator.Current.GetService(); + return new Bitmap(assets.Open(new Uri(uri))); + } + + private class PageViewModel : ReactiveObject + { + private Dock _tabPlacement; + + public TabItemViewModel[] Tabs { get; set; } + + public Dock TabPlacement + { + get { return _tabPlacement; } + set { this.RaiseAndSetIfChanged(ref _tabPlacement, value); } + } + } + + private class TabItemViewModel + { + public string Header { get; set; } + public string Text { get; set; } + public IBitmap Image { get; set; } + public bool IsEnabled { get; set; } = true; + } + } +} diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml b/samples/ControlCatalog/Pages/TextBoxPage.xaml index 5f94e15cec..0c0a4d705b 100644 --- a/samples/ControlCatalog/Pages/TextBoxPage.xaml +++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml @@ -1,4 +1,6 @@ - + TextBox A control into which the user can input text @@ -33,11 +35,20 @@ Text="Multiline TextBox with no TextWrapping. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." /> + resm fonts + + + res fonts + + + + + - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/ToolTipPage.xaml b/samples/ControlCatalog/Pages/ToolTipPage.xaml index ad832b9b82..cbe1e3059c 100644 --- a/samples/ControlCatalog/Pages/ToolTipPage.xaml +++ b/samples/ControlCatalog/Pages/ToolTipPage.xaml @@ -1,4 +1,6 @@ - + ToolTip @@ -38,4 +40,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml index 1ab49dbb30..f8f3cd5848 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml @@ -1,4 +1,6 @@ - + TreeView Displays a hierachical tree of data. @@ -16,4 +18,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/ViewboxPage.xaml b/samples/ControlCatalog/Pages/ViewboxPage.xaml new file mode 100644 index 0000000000..e78cf2bc22 --- /dev/null +++ b/samples/ControlCatalog/Pages/ViewboxPage.xaml @@ -0,0 +1,66 @@ + + + + F1 M 16.6309,18.6563C 17.1309, + 8.15625 29.8809,14.1563 29.8809, + 14.1563C 30.8809,11.1563 34.1308, + 11.4063 34.1308,11.4063C 33.5,12 + 34.6309,13.1563 34.6309,13.1563C + 32.1309,13.1562 31.1309,14.9062 + 31.1309,14.9062C 41.1309,23.9062 + 32.6309,27.9063 32.6309,27.9062C + 24.6309,24.9063 21.1309,22.1562 + 16.6309,18.6563 Z M 16.6309,19.9063C + 21.6309,24.1563 25.1309,26.1562 + 31.6309,28.6562C 31.6309,28.6562 + 26.3809,39.1562 18.3809,36.1563C + 18.3809,36.1563 18,38 16.3809,36.9063C + 15,36 16.3809,34.9063 16.3809,34.9063C + 16.3809,34.9063 10.1309,30.9062 16.6309,19.9063 Z + + + + + + Viewbox + A control used to scale single child. + + + None + Fill + Uniform + UniformToFill + + + Hello World! + + + Hello World! + + + Hello World! + + + Hello World! + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs b/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs new file mode 100644 index 0000000000..1b5f4bc7f4 --- /dev/null +++ b/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class ViewboxPage : UserControl + { + public ViewboxPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index cc3c31d13a..f97a3064e7 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -1,52 +1,67 @@ - + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:Class="ControlCatalog.SideBar"> + - - - - - + + + + + diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index fc5b1ce94d..41164c7780 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -24,9 +24,6 @@ - - - @@ -38,4 +35,4 @@ - \ No newline at end of file + diff --git a/samples/RenderDemo/Pages/AnimationsPage.xaml b/samples/RenderDemo/Pages/AnimationsPage.xaml index 1646708797..b5a8ff6dea 100644 --- a/samples/RenderDemo/Pages/AnimationsPage.xaml +++ b/samples/RenderDemo/Pages/AnimationsPage.xaml @@ -43,7 +43,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + @@ -120,6 +150,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + diff --git a/samples/RenderDemo/Pages/ClippingPage.xaml b/samples/RenderDemo/Pages/ClippingPage.xaml index 2bdecfc0ef..ca1b297363 100644 --- a/samples/RenderDemo/Pages/ClippingPage.xaml +++ b/samples/RenderDemo/Pages/ClippingPage.xaml @@ -8,7 +8,7 @@ xmlns="https://github.com/avaloniaui"> + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" > + - - - - - + + + + + diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 516f383b92..4b0f76c5d5 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Reactive.Linq; using Avalonia.Collections; using Avalonia.Data; +using Avalonia.Animation.Animators; namespace Avalonia.Animation { diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index d7efc69e10..ae6deb585c 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -7,68 +7,209 @@ using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Threading.Tasks; +using Avalonia.Animation.Animators; using Avalonia.Animation.Easings; using Avalonia.Collections; +using Avalonia.Data; +using Avalonia.Metadata; namespace Avalonia.Animation { /// /// Tracks the progress of an animation. /// - public class Animation : AvaloniaList, IAnimation + public class Animation : AvaloniaObject, IAnimation { + /// + /// Defines the property. + /// + public static readonly DirectProperty DurationProperty = + AvaloniaProperty.RegisterDirect( + nameof(_duration), + o => o._duration, + (o, v) => o._duration = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty IterationCountProperty = + AvaloniaProperty.RegisterDirect( + nameof(_iterationCount), + o => o._iterationCount, + (o, v) => o._iterationCount = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty PlaybackDirectionProperty = + AvaloniaProperty.RegisterDirect( + nameof(_playbackDirection), + o => o._playbackDirection, + (o, v) => o._playbackDirection = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty FillModeProperty = + AvaloniaProperty.RegisterDirect( + nameof(_fillMode), + o => o._fillMode, + (o, v) => o._fillMode = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty EasingProperty = + AvaloniaProperty.RegisterDirect( + nameof(_easing), + o => o._easing, + (o, v) => o._easing = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty DelayProperty = + AvaloniaProperty.RegisterDirect( + nameof(_delay), + o => o._delay, + (o, v) => o._delay = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty DelayBetweenIterationsProperty = + AvaloniaProperty.RegisterDirect( + nameof(_delayBetweenIterations), + o => o._delayBetweenIterations, + (o, v) => o._delayBetweenIterations = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty SpeedRatioProperty = + AvaloniaProperty.RegisterDirect( + nameof(_speedRatio), + o => o._speedRatio, + (o, v) => o._speedRatio = v, + defaultBindingMode: BindingMode.TwoWay); + + private TimeSpan _duration; + private IterationCount _iterationCount = new IterationCount(1); + private PlaybackDirection _playbackDirection; + private FillMode _fillMode; + private Easing _easing = new LinearEasing(); + private TimeSpan _delay = TimeSpan.Zero; + private TimeSpan _delayBetweenIterations = TimeSpan.Zero; + private double _speedRatio = 1d; + /// /// Gets or sets the active time of this animation. /// - public TimeSpan Duration { get; set; } + public TimeSpan Duration + { + get { return _duration; } + set { SetAndRaise(DurationProperty, ref _duration, value); } + } /// /// Gets or sets the repeat count for this animation. /// - public RepeatCount RepeatCount { get; set; } + public IterationCount IterationCount + { + get { return _iterationCount; } + set { SetAndRaise(IterationCountProperty, ref _iterationCount, value); } + } /// /// Gets or sets the playback direction for this animation. /// - public PlaybackDirection PlaybackDirection { get; set; } + public PlaybackDirection PlaybackDirection + { + get { return _playbackDirection; } + set { SetAndRaise(PlaybackDirectionProperty, ref _playbackDirection, value); } + } /// /// Gets or sets the value fill mode for this animation. - /// - public FillMode FillMode { get; set; } + /// + public FillMode FillMode + { + get { return _fillMode; } + set { SetAndRaise(FillModeProperty, ref _fillMode, value); } + } /// /// Gets or sets the easing function to be used for this animation. /// - public Easing Easing { get; set; } = new LinearEasing(); - - /// - /// Gets or sets the speed multiple for this animation. - /// - public double SpeedRatio { get; set; } = 1d; + public Easing Easing + { + get { return _easing; } + set { SetAndRaise(EasingProperty, ref _easing, value); } + } /// - /// Gets or sets the delay time for this animation. + /// Gets or sets the initial delay time for this animation. /// - /// - /// Describes a delay to be added before the animation starts, and optionally between - /// repeats of the animation if is set. - /// - public TimeSpan Delay { get; set; } + public TimeSpan Delay + { + get { return _delay; } + set { SetAndRaise(DelayProperty, ref _delay, value); } + } /// - /// Gets or sets a value indicating whether will be applied between - /// iterations of the animation. + /// Gets or sets the delay time in between iterations. + /// + public TimeSpan DelayBetweenIterations + { + get { return _delayBetweenIterations; } + set { SetAndRaise(DelayBetweenIterationsProperty, ref _delayBetweenIterations, value); } + } + + /// + /// Gets or sets the speed multiple for this animation. /// - /// - /// If this property is not set, then will only be applied to the first - /// iteration of the animation. - /// - public bool DelayBetweenIterations { get; set; } + public double SpeedRatio + { + get { return _speedRatio; } + set { SetAndRaise(SpeedRatioProperty, ref _speedRatio, value); } + } + + /// + /// Obsolete: Do not use this property, use instead. + /// + /// + [Obsolete("This property has been superceded by IterationCount.")] + public string RepeatCount + { + get { return IterationCount.ToString(); } + set + { + var val = value.ToUpper(); + val = val.Replace("LOOP", "INFINITE"); + val = val.Replace("NONE", "1"); + IterationCount = IterationCount.Parse(val); + } + } + + /// + /// Gets the children of the . + /// + [Content] + public KeyFrames Children { get; } = new KeyFrames(); private readonly static List<(Func Condition, Type Animator)> Animators = new List<(Func, Type)> { - ( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator) ) + ( prop => typeof(bool).IsAssignableFrom(prop.PropertyType), typeof(BoolAnimator) ), + ( prop => typeof(byte).IsAssignableFrom(prop.PropertyType), typeof(ByteAnimator) ), + ( prop => typeof(Int16).IsAssignableFrom(prop.PropertyType), typeof(Int16Animator) ), + ( prop => typeof(Int32).IsAssignableFrom(prop.PropertyType), typeof(Int32Animator) ), + ( prop => typeof(Int64).IsAssignableFrom(prop.PropertyType), typeof(Int64Animator) ), + ( prop => typeof(UInt16).IsAssignableFrom(prop.PropertyType), typeof(UInt16Animator) ), + ( prop => typeof(UInt32).IsAssignableFrom(prop.PropertyType), typeof(UInt32Animator) ), + ( prop => typeof(UInt64).IsAssignableFrom(prop.PropertyType), typeof(UInt64Animator) ), + ( prop => typeof(float).IsAssignableFrom(prop.PropertyType), typeof(FloatAnimator) ), + ( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator) ), + ( prop => typeof(decimal).IsAssignableFrom(prop.PropertyType), typeof(DecimalAnimator) ), }; public static void RegisterAnimator(Func condition) @@ -95,9 +236,9 @@ namespace Avalonia.Animation var animatorKeyFrames = new List(); var subscriptions = new List(); - foreach (var keyframe in this) + foreach (var keyframe in Children) { - foreach (var setter in keyframe) + foreach (var setter in keyframe.Setters) { var handler = GetAnimatorType(setter.Property); @@ -179,7 +320,7 @@ namespace Avalonia.Animation { var run = new TaskCompletionSource(); - if (this.RepeatCount == RepeatCount.Loop) + if (this.IterationCount == IterationCount.Infinite) run.SetException(new InvalidOperationException("Looping animations must not use the Run method.")); IDisposable subscriptions = null; diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Animation/AnimationInstance`1.cs index 8184e68d42..bc7e106a2a 100644 --- a/src/Avalonia.Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Animation/AnimationInstance`1.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Reactive.Linq; +using Avalonia.Animation.Animators; using Avalonia.Animation.Utils; using Avalonia.Data; using Avalonia.Reactive; @@ -15,78 +16,77 @@ namespace Avalonia.Animation { private T _lastInterpValue; private T _firstKFValue; - private long _repeatCount; - private double _currentIteration; - private bool _isLooping; + private ulong? _iterationCount; + private ulong _currentIteration; private bool _gotFirstKFValue; - private bool _iterationDelay; private FillMode _fillMode; - private PlaybackDirection _animationDirection; - private Animator _parent; + private PlaybackDirection _playbackDirection; + private Animator _animator; + private Animation _animation; private Animatable _targetControl; private T _neutralValue; - private double _speedRatio; - private TimeSpan _delay; + private double _speedRatioConv; + private TimeSpan _initialDelay; + private TimeSpan _iterationDelay; private TimeSpan _duration; private Easings.Easing _easeFunc; private Action _onCompleteAction; private Func _interpolator; - private IDisposable _timerSubscription; + private IDisposable _timerSub; private readonly IClock _baseClock; private IClock _clock; public AnimationInstance(Animation animation, Animatable control, Animator animator, IClock baseClock, Action OnComplete, Func Interpolator) { - if (animation.SpeedRatio <= 0) - throw new InvalidOperationException("Speed ratio cannot be negative or zero."); + _animator = animator; + _animation = animation; + _targetControl = control; + _onCompleteAction = OnComplete; + _interpolator = Interpolator; + _baseClock = baseClock; + _neutralValue = (T)_targetControl.GetValue(_animator.Property); - if (animation.Duration.TotalSeconds <= 0) - throw new InvalidOperationException("Duration cannot be negative or zero."); + FetchProperties(); + } - _parent = animator; - _easeFunc = animation.Easing; - _targetControl = control; - _neutralValue = (T)_targetControl.GetValue(_parent.Property); + private void FetchProperties() + { + if (_animation.SpeedRatio < 0d) + throw new ArgumentOutOfRangeException("SpeedRatio value should not be negative."); - _speedRatio = animation.SpeedRatio; + if (_animation.Duration.TotalSeconds <= 0) + throw new InvalidOperationException("Duration value cannot be negative or zero."); - _delay = animation.Delay; - _duration = animation.Duration; - _iterationDelay = animation.DelayBetweenIterations; + _easeFunc = _animation.Easing; - switch (animation.RepeatCount.RepeatType) - { - case RepeatType.None: - _repeatCount = 1; - break; - case RepeatType.Loop: - _isLooping = true; - break; - case RepeatType.Repeat: - _repeatCount = (long)animation.RepeatCount.Value; - break; - } + _speedRatioConv = 1d / _animation.SpeedRatio; - _animationDirection = animation.PlaybackDirection; - _fillMode = animation.FillMode; - _onCompleteAction = OnComplete; - _interpolator = Interpolator; - _baseClock = baseClock; - } + _initialDelay = _animation.Delay; + _duration = _animation.Duration; + _iterationDelay = _animation.DelayBetweenIterations; + + if (_animation.IterationCount.RepeatType == IterationType.Many) + _iterationCount = _animation.IterationCount.Value; + else + _iterationCount = null; + + _playbackDirection = _animation.PlaybackDirection; + _fillMode = _animation.FillMode; + } protected override void Unsubscribed() { - //Animation may have been stopped before it has finished + // Animation may have been stopped before it has finished. ApplyFinalFill(); - _timerSubscription?.Dispose(); + _timerSub?.Dispose(); _clock.PlayState = PlayState.Stop; } protected override void Subscribed() { _clock = new Clock(_baseClock); - _timerSubscription = _clock.Subscribe(Step); + _timerSub = _clock.Subscribe(Step); } public void Step(TimeSpan frameTick) @@ -104,7 +104,7 @@ namespace Avalonia.Animation private void ApplyFinalFill() { if (_fillMode == FillMode.Forward || _fillMode == FillMode.Both) - _targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue); + _targetControl.SetValue(_animator.Property, _lastInterpValue, BindingPriority.LocalValue); } private void DoComplete() @@ -130,7 +130,7 @@ namespace Avalonia.Animation if (!_gotFirstKFValue) { - _firstKFValue = (T)_parent.First().Value; + _firstKFValue = (T)_animator.First().Value; _gotFirstKFValue = true; } } @@ -138,75 +138,77 @@ namespace Avalonia.Animation private void InternalStep(TimeSpan time) { DoPlayStates(); - var delayEndpoint = _delay; - var iterationEndpoint = delayEndpoint + _duration; - var iterationTime = time; - //determine if time is currently in the first iteration. - if (time >= TimeSpan.Zero & time <= iterationEndpoint) - { - _currentIteration = 1; - } - else if (time > iterationEndpoint) - { - //Subtract first iteration to properly get the subsequent iteration time - iterationTime -= iterationEndpoint; + FetchProperties(); - if (!_iterationDelay & delayEndpoint > TimeSpan.Zero) - { - delayEndpoint = TimeSpan.Zero; - iterationEndpoint = _duration; - } - - //Calculate the current iteration number - _currentIteration = Math.Min(_repeatCount,(int)Math.Floor((double)((double)iterationTime.Ticks / iterationEndpoint.Ticks)) + 2); - } - else - { - return; - } + // Scale timebases according to speedratio. + var indexTime = time.Ticks; + var iterDuration = _duration.Ticks * _speedRatioConv; + var iterDelay = _iterationDelay.Ticks * _speedRatioConv; + var initDelay = _initialDelay.Ticks * _speedRatioConv; - // Determine if the current iteration should have its normalized time inverted. - bool isCurIterReverse = _animationDirection == PlaybackDirection.Normal ? false : - _animationDirection == PlaybackDirection.Alternate ? (_currentIteration % 2 == 0) ? false : true : - _animationDirection == PlaybackDirection.AlternateReverse ? (_currentIteration % 2 == 0) ? true : false : - _animationDirection == PlaybackDirection.Reverse ? true : false; - - if (!_isLooping) - { - var totalTime = _iterationDelay ? _repeatCount * ( _duration.Ticks + _delay.Ticks) : _repeatCount * _duration.Ticks + _delay.Ticks; - if (time.Ticks >= totalTime) - { - var easedTime = _easeFunc.Ease(isCurIterReverse ? 0.0 : 1.0); - _lastInterpValue = _interpolator(easedTime, _neutralValue); - - DoComplete(); - return; - } - } - iterationTime = TimeSpan.FromTicks((long)(iterationTime.Ticks % iterationEndpoint.Ticks)); - - if (delayEndpoint > TimeSpan.Zero & iterationTime < delayEndpoint) + if (indexTime > 0 & indexTime <= initDelay) { DoDelay(); } else { - // Offset the delay time - iterationTime -= delayEndpoint; - iterationEndpoint -= delayEndpoint; + // Calculate timebases. + var iterationTime = iterDuration + iterDelay; + var opsTime = indexTime - initDelay; + var playbackTime = opsTime % iterationTime; - // Normalize time - var interpVal = (double)iterationTime.Ticks / iterationEndpoint.Ticks; + _currentIteration = (ulong)(opsTime / iterationTime); - if (isCurIterReverse) - interpVal = 1 - interpVal; + // Stop animation when the current iteration is beyond the iteration count. + if ((_currentIteration + 1) > _iterationCount) + DoComplete(); - // Ease and interpolate - var easedTime = _easeFunc.Ease(interpVal); - _lastInterpValue = _interpolator(easedTime, _neutralValue); + if (playbackTime <= iterDuration) + { + // Normalize time for interpolation. + var normalizedTime = playbackTime / iterDuration; + + // Check if normalized time needs to be reversed according to PlaybackDirection + + bool playbackReversed; + switch (_playbackDirection) + { + case PlaybackDirection.Normal: + playbackReversed = false; + break; + case PlaybackDirection.Reverse: + playbackReversed = true; + break; + case PlaybackDirection.Alternate: + playbackReversed = (_currentIteration % 2 == 0) ? false : true; + break; + case PlaybackDirection.AlternateReverse: + playbackReversed = (_currentIteration % 2 == 0) ? true : false; + break; + default: + throw new InvalidOperationException($"Animation direction value is unknown: {_playbackDirection}"); + } + + if (playbackReversed) + normalizedTime = 1 - normalizedTime; + + // Ease and interpolate + var easedTime = _easeFunc.Ease(normalizedTime); + _lastInterpValue = _interpolator(easedTime, _neutralValue); - PublishNext(_lastInterpValue); + PublishNext(_lastInterpValue); + } + else if (playbackTime > iterDuration & + playbackTime <= iterationTime & + iterDelay > 0) + { + // The last iteration's trailing delay should be skipped. + if ((_currentIteration + 1) < _iterationCount) + DoDelay(); + else + DoComplete(); + } } } } diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animators/Animator`1.cs similarity index 69% rename from src/Avalonia.Animation/Animator`1.cs rename to src/Avalonia.Animation/Animators/Animator`1.cs index d1a8960a10..e42489d6a6 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animators/Animator`1.cs @@ -10,10 +10,10 @@ using Avalonia.Collections; using Avalonia.Data; using Avalonia.Reactive; -namespace Avalonia.Animation +namespace Avalonia.Animation.Animators { /// - /// Base class for KeyFrames objects + /// Base class for objects /// public abstract class Animator : AvaloniaList, IAnimator { @@ -45,17 +45,10 @@ namespace Avalonia.Animation return match.Subscribe(subject); } - /// - /// Get the nearest pair of cue-time ordered keyframes - /// according to the given time parameter that is relative to the - /// total animation time and the normalized intra-keyframe pair time - /// (i.e., the normalized time between the selected keyframes, relative to the - /// time parameter). - /// - /// The time parameter, relative to the total animation time - protected (double IntraKFTime, KeyFramePair KFPair) GetKFPairAndIntraKFTime(double animationTime) + protected T InterpolationHandler(double animationTime, T neutralValue) { AnimatorKeyFrame firstKeyframe, lastKeyframe; + int kvCount = _convertedKeyframes.Count; if (kvCount > 2) { @@ -84,38 +77,31 @@ namespace Avalonia.Animation double t0 = firstKeyframe.Cue.CueValue; double t1 = lastKeyframe.Cue.CueValue; - var intraframeTime = (animationTime - t0) / (t1 - t0); - var firstFrameData = (firstKeyframe.GetTypedValue(), firstKeyframe.isNeutral); - var lastFrameData = (lastKeyframe.GetTypedValue(), lastKeyframe.isNeutral); - return (intraframeTime, new KeyFramePair(firstFrameData, lastFrameData)); + + double progress = (animationTime - t0) / (t1 - t0); + + T oldValue, newValue; + + if (firstKeyframe.isNeutral) + oldValue = neutralValue; + else + oldValue = (T)firstKeyframe.Value; + + if (lastKeyframe.isNeutral) + newValue = neutralValue; + else + newValue = (T)lastKeyframe.Value; + + return Interpolate(progress, oldValue, newValue); } private int FindClosestBeforeKeyFrame(double time) { - int FindClosestBeforeKeyFrame(int startIndex, int length) - { - if (length == 0 || length == 1) - { - return startIndex; - } + for (int i = 0; i < _convertedKeyframes.Count; i++) + if (_convertedKeyframes[i].Cue.CueValue > time) + return i - 1; - int middle = startIndex + (length / 2); - - if (_convertedKeyframes[middle].Cue.CueValue < time) - { - return FindClosestBeforeKeyFrame(middle, length - middle); - } - else if (_convertedKeyframes[middle].Cue.CueValue > time) - { - return FindClosestBeforeKeyFrame(startIndex, middle - startIndex); - } - else - { - return middle; - } - } - - return FindClosestBeforeKeyFrame(0, _convertedKeyframes.Count); + throw new Exception("Index time is out of keyframe time range."); } /// @@ -129,18 +115,15 @@ namespace Avalonia.Animation this, clock ?? control.Clock ?? Clock.GlobalClock, onComplete, - DoInterpolation); + InterpolationHandler); return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation); } /// - /// Interpolates a value given the desired time. + /// Interpolates in-between two key values given the desired progress time. /// - protected abstract T DoInterpolation(double time, T neutralValue); + public abstract T Interpolate(double progress, T oldValue, T newValue); - /// - /// Verifies, converts and sorts keyframe values according to this class's target type. - /// private void VerifyConvertKeyFrames() { foreach (AnimatorKeyFrame keyframe in this) @@ -188,4 +171,4 @@ namespace Avalonia.Animation } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/Animators/BoolAnimator.cs b/src/Avalonia.Animation/Animators/BoolAnimator.cs new file mode 100644 index 0000000000..63ff4933d6 --- /dev/null +++ b/src/Avalonia.Animation/Animators/BoolAnimator.cs @@ -0,0 +1,21 @@ +// Copyright (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 +{ + /// + /// Animator that handles properties. + /// + public class BoolAnimator : Animator + { + /// + public override bool Interpolate(double progress, bool oldValue, bool newValue) + { + if(progress >= 1d) + return newValue; + if(progress >= 0) + return oldValue; + return oldValue; + } + } +} diff --git a/src/Avalonia.Animation/Animators/ByteAnimator.cs b/src/Avalonia.Animation/Animators/ByteAnimator.cs new file mode 100644 index 0000000000..0fb8f7fdc1 --- /dev/null +++ b/src/Avalonia.Animation/Animators/ByteAnimator.cs @@ -0,0 +1,24 @@ +// Copyright (c) The 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.Animators +{ + /// + /// Animator that handles properties. + /// + public class ByteAnimator : Animator + { + const double maxVal = (double)byte.MaxValue; + + /// + public override byte Interpolate(double progress, byte oldValue, byte newValue) + { + var normOV = oldValue / maxVal; + var normNV = newValue / maxVal; + var deltaV = normNV - normOV; + return (byte)Math.Round(maxVal * ((deltaV * progress) + normOV)); + } + } +} diff --git a/src/Avalonia.Animation/Animators/DecimalAnimator.cs b/src/Avalonia.Animation/Animators/DecimalAnimator.cs new file mode 100644 index 0000000000..b5fae7990c --- /dev/null +++ b/src/Avalonia.Animation/Animators/DecimalAnimator.cs @@ -0,0 +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.Animation.Animators +{ + /// + /// Animator that handles properties. + /// + public class DecimalAnimator : Animator + { + /// + public override decimal Interpolate(double progress, decimal oldValue, decimal newValue) + { + return ((newValue - oldValue) * (decimal)progress) + oldValue; + } + } +} diff --git a/src/Avalonia.Animation/Animators/DoubleAnimator.cs b/src/Avalonia.Animation/Animators/DoubleAnimator.cs new file mode 100644 index 0000000000..68975be9d0 --- /dev/null +++ b/src/Avalonia.Animation/Animators/DoubleAnimator.cs @@ -0,0 +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.Animation.Animators +{ + /// + /// Animator that handles properties. + /// + public class DoubleAnimator : Animator + { + /// + public override double Interpolate(double progress, double oldValue, double newValue) + { + return ((newValue - oldValue) * progress) + oldValue; + } + } +} diff --git a/src/Avalonia.Animation/Animators/FloatAnimator.cs b/src/Avalonia.Animation/Animators/FloatAnimator.cs new file mode 100644 index 0000000000..0fd3bf8729 --- /dev/null +++ b/src/Avalonia.Animation/Animators/FloatAnimator.cs @@ -0,0 +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.Animation.Animators +{ + /// + /// Animator that handles properties. + /// + public class FloatAnimator : Animator + { + /// + public override float Interpolate(double progress, float oldValue, float newValue) + { + return (float)(((newValue - oldValue) * progress) + oldValue); + } + } +} diff --git a/src/Avalonia.Animation/Animators/Int16Animator.cs b/src/Avalonia.Animation/Animators/Int16Animator.cs new file mode 100644 index 0000000000..d7e7da5d38 --- /dev/null +++ b/src/Avalonia.Animation/Animators/Int16Animator.cs @@ -0,0 +1,24 @@ +// Copyright (c) The 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.Animators +{ + /// + /// Animator that handles properties. + /// + public class Int16Animator : Animator + { + const double maxVal = (double)Int16.MaxValue; + + /// + public override Int16 Interpolate(double progress, Int16 oldValue, Int16 newValue) + { + var normOV = oldValue / maxVal; + var normNV = newValue / maxVal; + var deltaV = normNV - normOV; + return (Int16)Math.Round(maxVal * ((deltaV * progress) + normOV)); + } + } +} diff --git a/src/Avalonia.Animation/Animators/Int32Animator.cs b/src/Avalonia.Animation/Animators/Int32Animator.cs new file mode 100644 index 0000000000..792b810652 --- /dev/null +++ b/src/Avalonia.Animation/Animators/Int32Animator.cs @@ -0,0 +1,24 @@ +// Copyright (c) The 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.Animators +{ + /// + /// Animator that handles properties. + /// + public class Int32Animator : Animator + { + const double maxVal = (double)Int32.MaxValue; + + /// + public override Int32 Interpolate(double progress, Int32 oldValue, Int32 newValue) + { + var normOV = oldValue / maxVal; + var normNV = newValue / maxVal; + var deltaV = normNV - normOV; + return (Int32)Math.Round(maxVal * ((deltaV * progress) + normOV)); + } + } +} diff --git a/src/Avalonia.Animation/Animators/Int64Animator.cs b/src/Avalonia.Animation/Animators/Int64Animator.cs new file mode 100644 index 0000000000..ca5817924e --- /dev/null +++ b/src/Avalonia.Animation/Animators/Int64Animator.cs @@ -0,0 +1,24 @@ +// Copyright (c) The 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.Animators +{ + /// + /// Animator that handles properties. + /// + public class Int64Animator : Animator + { + const double maxVal = (double)Int64.MaxValue; + + /// + public override Int64 Interpolate(double progress, Int64 oldValue, Int64 newValue) + { + var normOV = oldValue / maxVal; + var normNV = newValue / maxVal; + var deltaV = normNV - normOV; + return (Int64)Math.Round(maxVal * ((deltaV * progress) + normOV)); + } + } +} diff --git a/src/Avalonia.Animation/Animators/UInt16Animator.cs b/src/Avalonia.Animation/Animators/UInt16Animator.cs new file mode 100644 index 0000000000..4b5463dade --- /dev/null +++ b/src/Avalonia.Animation/Animators/UInt16Animator.cs @@ -0,0 +1,24 @@ +// Copyright (c) The 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.Animators +{ + /// + /// Animator that handles properties. + /// + public class UInt16Animator : Animator + { + const double maxVal = (double)UInt16.MaxValue; + + /// + public override UInt16 Interpolate(double progress, UInt16 oldValue, UInt16 newValue) + { + var normOV = oldValue / maxVal; + var normNV = newValue / maxVal; + var deltaV = normNV - normOV; + return (UInt16)Math.Round(maxVal * ((deltaV * progress) + normOV)); + } + } +} diff --git a/src/Avalonia.Animation/Animators/UInt32Animator.cs b/src/Avalonia.Animation/Animators/UInt32Animator.cs new file mode 100644 index 0000000000..c1f09e3518 --- /dev/null +++ b/src/Avalonia.Animation/Animators/UInt32Animator.cs @@ -0,0 +1,24 @@ +// Copyright (c) The 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.Animators +{ + /// + /// Animator that handles properties. + /// + public class UInt32Animator : Animator + { + const double maxVal = (double)UInt32.MaxValue; + + /// + public override UInt32 Interpolate(double progress, UInt32 oldValue, UInt32 newValue) + { + var normOV = oldValue / maxVal; + var normNV = newValue / maxVal; + var deltaV = normNV - normOV; + return (UInt32)Math.Round(maxVal * ((deltaV * progress) + normOV)); + } + } +} diff --git a/src/Avalonia.Animation/Animators/UInt64Animator.cs b/src/Avalonia.Animation/Animators/UInt64Animator.cs new file mode 100644 index 0000000000..0fd9fcb30a --- /dev/null +++ b/src/Avalonia.Animation/Animators/UInt64Animator.cs @@ -0,0 +1,24 @@ +// Copyright (c) The 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.Animators +{ + /// + /// Animator that handles properties. + /// + public class UInt64Animator : Animator + { + const double maxVal = (double)UInt64.MaxValue; + + /// + public override UInt64 Interpolate(double progress, UInt64 oldValue, UInt64 newValue) + { + var normOV = oldValue / maxVal; + var normNV = newValue / maxVal; + var deltaV = normNV - normOV; + return (UInt64)Math.Round(maxVal * ((deltaV * progress) + normOV)); + } + } +} diff --git a/src/Avalonia.Animation/Avalonia.Animation.csproj b/src/Avalonia.Animation/Avalonia.Animation.csproj index 3b6d1c1b33..73c619942e 100644 --- a/src/Avalonia.Animation/Avalonia.Animation.csproj +++ b/src/Avalonia.Animation/Avalonia.Animation.csproj @@ -1,7 +1,6 @@  netstandard2.0 - false diff --git a/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs b/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs index a535b30b58..b44f392ce3 100644 --- a/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs +++ b/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; +using Avalonia.Animation.Animators; using Avalonia.Animation.Utils; using Avalonia.Collections; using Avalonia.Data; @@ -33,8 +34,6 @@ namespace Avalonia.Animation this._onComplete = onComplete; this._clock = clock; } - - public void Dispose() { _lastInstance?.Dispose(); diff --git a/src/Avalonia.Animation/DoubleAnimator.cs b/src/Avalonia.Animation/DoubleAnimator.cs deleted file mode 100644 index 2e0ce64185..0000000000 --- a/src/Avalonia.Animation/DoubleAnimator.cs +++ /dev/null @@ -1,35 +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.Animation -{ - /// - /// Animator that handles properties. - /// - public class DoubleAnimator : Animator - { - - /// - protected override double DoInterpolation(double t, double neutralValue) - { - var pair = GetKFPairAndIntraKFTime(t); - double y0, y1; - - var firstKF = pair.KFPair.FirstKeyFrame; - var secondKF = pair.KFPair.SecondKeyFrame; - - if (firstKF.isNeutral) - y0 = neutralValue; - else - y0 = firstKF.TargetValue; - - if (secondKF.isNeutral) - y1 = neutralValue; - else - y1 = secondKF.TargetValue; - - // Do linear parametric interpolation - return y0 + (pair.IntraKFTime) * (y1 - y0); - } - } -} diff --git a/src/Avalonia.Animation/IEasing.cs b/src/Avalonia.Animation/Easing/IEasing.cs similarity index 100% rename from src/Avalonia.Animation/IEasing.cs rename to src/Avalonia.Animation/Easing/IEasing.cs diff --git a/src/Avalonia.Animation/IterationCount.cs b/src/Avalonia.Animation/IterationCount.cs new file mode 100644 index 0000000000..eafc483868 --- /dev/null +++ b/src/Avalonia.Animation/IterationCount.cs @@ -0,0 +1,176 @@ +// Copyright (c) The Avalonia 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; + +namespace Avalonia.Animation +{ + /// + /// Defines the valid modes for a . + /// + public enum IterationType + { + Many, + Infinite + } + + /// + /// Determines the number of iterations of an animation. + /// Also defines its repeat behavior. + /// + [TypeConverter(typeof(IterationCountTypeConverter))] + public struct IterationCount : IEquatable + { + private readonly IterationType _type; + private readonly ulong _value; + + /// + /// Initializes a new instance of the struct. + /// + /// The number of iterations of an animation. + public IterationCount(ulong value) + : this(value, IterationType.Many) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The size of the IterationCount. + /// The unit of the IterationCount. + public IterationCount(ulong value, IterationType type) + { + if (type > IterationType.Infinite) + { + throw new ArgumentException("Invalid value", "type"); + } + + _type = type; + _value = value; + } + + /// + /// Gets an instance of that indicates that an animation + /// should repeat forever. + /// + public static IterationCount Infinite => new IterationCount(0, IterationType.Infinite); + + /// + /// Gets the unit of the . + /// + public IterationType RepeatType => _type; + + /// + /// Gets a value that indicates whether the is set to loop. + /// + public bool IsInfinite => _type == IterationType.Infinite; + + /// + /// Gets the number of repeat iterations. + /// + public ulong Value => _value; + + /// + /// Compares two IterationCount structures for equality. + /// + /// The first IterationCount. + /// The second IterationCount. + /// True if the structures are equal, otherwise false. + public static bool operator ==(IterationCount a, IterationCount b) + { + return (a.IsInfinite && b.IsInfinite) + || (a._value == b._value && a._type == b._type); + } + + /// + /// Compares two IterationCount structures for inequality. + /// + /// The first IterationCount. + /// The first IterationCount. + /// True if the structures are unequal, otherwise false. + public static bool operator !=(IterationCount rc1, IterationCount rc2) + { + return !(rc1 == rc2); + } + + /// + /// Determines whether the is equal to the specified object. + /// + /// The object with which to test equality. + /// True if the objects are equal, otherwise false. + public override bool Equals(object o) + { + if (o == null) + { + return false; + } + + if (!(o is IterationCount)) + { + return false; + } + + return this == (IterationCount)o; + } + + /// + /// Compares two IterationCount structures for equality. + /// + /// The structure with which to test equality. + /// True if the structures are equal, otherwise false. + public bool Equals(IterationCount IterationCount) + { + return this == IterationCount; + } + + /// + /// Gets a hash code for the IterationCount. + /// + /// The hash code. + public override int GetHashCode() + { + return _value.GetHashCode() ^ _type.GetHashCode(); + } + + /// + /// Gets a string representation of the . + /// + /// The string representation. + public override string ToString() + { + if (IsInfinite) + { + return "Infinite"; + } + + string s = _value.ToString(); + return s; + } + + /// + /// Parses a string to return a . + /// + /// The string. + /// The . + public static IterationCount Parse(string s) + { + s = s.ToUpperInvariant().Trim(); + + if (s.EndsWith("INFINITE")) + { + return Infinite; + } + else + { + if (s.StartsWith("-")) + throw new InvalidCastException("IterationCount can't be a negative number."); + + var value = ulong.Parse(s, CultureInfo.InvariantCulture); + + return new IterationCount(value); + } + } + } +} diff --git a/src/Avalonia.Animation/RepeatCountTypeConverter.cs b/src/Avalonia.Animation/IterationCountTypeConverter.cs similarity index 83% rename from src/Avalonia.Animation/RepeatCountTypeConverter.cs rename to src/Avalonia.Animation/IterationCountTypeConverter.cs index 4eab50c1c7..a2bb9a20f1 100644 --- a/src/Avalonia.Animation/RepeatCountTypeConverter.cs +++ b/src/Avalonia.Animation/IterationCountTypeConverter.cs @@ -7,7 +7,7 @@ using System.Globalization; namespace Avalonia.Animation { - public class RepeatCountTypeConverter : TypeConverter + public class IterationCountTypeConverter : TypeConverter { public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { @@ -16,7 +16,7 @@ namespace Avalonia.Animation public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { - return RepeatCount.Parse((string)value); + return IterationCount.Parse((string)value); } } } diff --git a/src/Avalonia.Animation/KeyFrame.cs b/src/Avalonia.Animation/KeyFrame.cs index 44e39e042e..a6577505ee 100644 --- a/src/Avalonia.Animation/KeyFrame.cs +++ b/src/Avalonia.Animation/KeyFrame.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Avalonia.Collections; +using Avalonia.Metadata; namespace Avalonia.Animation { @@ -17,7 +18,7 @@ namespace Avalonia.Animation /// Stores data regarding a specific key /// point and value in an animation. /// - public class KeyFrame : AvaloniaList + public class KeyFrame : AvaloniaObject { private TimeSpan _ktimeSpan; private Cue _kCue; @@ -26,13 +27,11 @@ namespace Avalonia.Animation { } - public KeyFrame(IEnumerable items) : base(items) - { - } - - public KeyFrame(params IAnimationSetter[] items) : base(items) - { - } + /// + /// Gets the setters of . + /// + [Content] + public AvaloniaList Setters { get; } = new AvaloniaList(); internal KeyFrameTimingMode TimingMode { get; private set; } diff --git a/src/Avalonia.Animation/KeyFramePair`1.cs b/src/Avalonia.Animation/KeyFramePair`1.cs deleted file mode 100644 index 60a16d094f..0000000000 --- a/src/Avalonia.Animation/KeyFramePair`1.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. - -namespace Avalonia.Animation -{ - /// - /// Represents a pair of keyframe, usually the - /// Start and End keyframes of a object. - /// - public struct KeyFramePair - { - /// - /// Initializes this - /// - /// - /// - public KeyFramePair((T TargetValue, bool isNeutral) FirstKeyFrame, (T TargetValue, bool isNeutral) LastKeyFrame) : this() - { - this.FirstKeyFrame = FirstKeyFrame; - this.SecondKeyFrame = LastKeyFrame; - } - - /// - /// First object. - /// - public (T TargetValue, bool isNeutral) FirstKeyFrame { get; } - - /// - /// Second object. - /// - public (T TargetValue, bool isNeutral) SecondKeyFrame { get; } - } -} diff --git a/src/Avalonia.Animation/KeyFrames.cs b/src/Avalonia.Animation/KeyFrames.cs new file mode 100644 index 0000000000..9e3b1d9f51 --- /dev/null +++ b/src/Avalonia.Animation/KeyFrames.cs @@ -0,0 +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; +using System.Collections.Generic; +using Avalonia.Collections; + +namespace Avalonia.Animation +{ + /// + /// A collection of s. + /// + public class KeyFrames : AvaloniaList + { + /// + /// Initializes a new instance of the class. + /// + public KeyFrames() + { + ResetBehavior = ResetBehavior.Remove; + } + + /// + /// Initializes a new instance of the class. + /// + /// The initial items in the collection. + public KeyFrames(IEnumerable items) + : base(items) + { + ResetBehavior = ResetBehavior.Remove; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Animation/Properties/AssemblyInfo.cs b/src/Avalonia.Animation/Properties/AssemblyInfo.cs index 59d1357bb3..985a8e5bfe 100644 --- a/src/Avalonia.Animation/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Animation/Properties/AssemblyInfo.cs @@ -5,4 +5,5 @@ using Avalonia.Metadata; using System.Reflection; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Easings")] \ No newline at end of file +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Easings")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Animators")] \ No newline at end of file diff --git a/src/Avalonia.Animation/RepeatCount.cs b/src/Avalonia.Animation/RepeatCount.cs deleted file mode 100644 index 9794fe8f36..0000000000 --- a/src/Avalonia.Animation/RepeatCount.cs +++ /dev/null @@ -1,199 +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.Globalization; - -namespace Avalonia.Animation -{ - /// - /// Defines the valid modes for a . - /// - public enum RepeatType - { - None, - Repeat, - Loop - } - - /// - /// Determines the number of iterations of an animation. - /// Also defines its repeat behavior. - /// - [TypeConverter(typeof(RepeatCountTypeConverter))] - public struct RepeatCount : IEquatable - { - private readonly RepeatType _type; - private readonly ulong _value; - - /// - /// Initializes a new instance of the struct. - /// - /// The number of iterations of an animation. - public RepeatCount(ulong value) - : this(value, RepeatType.Repeat) - { - } - - /// - /// Initializes a new instance of the struct. - /// - /// The size of the RepeatCount. - /// The unit of the RepeatCount. - public RepeatCount(ulong value, RepeatType type) - { - if (type < RepeatType.None || type > RepeatType.Loop) - { - throw new ArgumentException("Invalid value", "type"); - } - - _type = type; - _value = value; - } - - /// - /// Gets an instance of that indicates that an animation - /// should repeat forever. - /// - public static RepeatCount Loop => new RepeatCount(0, RepeatType.Loop); - - /// - /// Gets an instance of that indicates that an animation - /// should not repeat. - /// - public static RepeatCount None => new RepeatCount(0, RepeatType.None); - - /// - /// Gets the unit of the . - /// - public RepeatType RepeatType => _type; - - /// - /// Gets a value that indicates whether the is set to loop. - /// - public bool IsLoop => _type == RepeatType.Loop; - - /// - /// Gets a value that indicates whether the is set to not repeat. - /// - public bool IsNone => _type == RepeatType.None; - - /// - /// Gets the number of repeat iterations. - /// - public ulong Value => _value; - - /// - /// Compares two RepeatCount structures for equality. - /// - /// The first RepeatCount. - /// The second RepeatCount. - /// True if the structures are equal, otherwise false. - public static bool operator ==(RepeatCount a, RepeatCount b) - { - return (a.IsNone && b.IsNone) && (a.IsLoop && b.IsLoop) - || (a._value == b._value && a._type == b._type); - } - - /// - /// Compares two RepeatCount structures for inequality. - /// - /// The first RepeatCount. - /// The first RepeatCount. - /// True if the structures are unequal, otherwise false. - public static bool operator !=(RepeatCount rc1, RepeatCount rc2) - { - return !(rc1 == rc2); - } - - /// - /// Determines whether the is equal to the specified object. - /// - /// The object with which to test equality. - /// True if the objects are equal, otherwise false. - public override bool Equals(object o) - { - if (o == null) - { - return false; - } - - if (!(o is RepeatCount)) - { - return false; - } - - return this == (RepeatCount)o; - } - - /// - /// Compares two RepeatCount structures for equality. - /// - /// The structure with which to test equality. - /// True if the structures are equal, otherwise false. - public bool Equals(RepeatCount RepeatCount) - { - return this == RepeatCount; - } - - /// - /// Gets a hash code for the RepeatCount. - /// - /// The hash code. - public override int GetHashCode() - { - return _value.GetHashCode() ^ _type.GetHashCode(); - } - - /// - /// Gets a string representation of the . - /// - /// The string representation. - public override string ToString() - { - if (IsLoop) - { - return "Auto"; - } - else if (IsNone) - { - return "None"; - } - - string s = _value.ToString(); - return s; - } - - /// - /// Parses a string to return a . - /// - /// The string. - /// The . - public static RepeatCount Parse(string s) - { - s = s.ToUpperInvariant().Trim(); - - if (s == "NONE") - { - return None; - } - else if (s.EndsWith("LOOP")) - { - return Loop; - } - else - { - if(s.StartsWith("-")) - throw new InvalidCastException("RepeatCount can't be a negative number."); - - var value = ulong.Parse(s, CultureInfo.InvariantCulture); - - if (value == 1) - return None; - - return new RepeatCount(value); - } - } - } -} diff --git a/src/Avalonia.Animation/DoubleTransition.cs b/src/Avalonia.Animation/Transitions/DoubleTransition.cs similarity index 78% rename from src/Avalonia.Animation/DoubleTransition.cs rename to src/Avalonia.Animation/Transitions/DoubleTransition.cs index 23445e1122..d7dd93c743 100644 --- a/src/Avalonia.Animation/DoubleTransition.cs +++ b/src/Avalonia.Animation/Transitions/DoubleTransition.cs @@ -14,9 +14,12 @@ namespace Avalonia.Animation /// public override IObservable DoTransition(IObservable progress, double oldValue, double newValue) { - var delta = newValue - oldValue; return progress - .Select(p => Easing.Ease(p) * delta + oldValue); + .Select(p => + { + var f = Easing.Ease(p); + return ((newValue - oldValue) * f) + oldValue; + }); } } } diff --git a/src/Avalonia.Animation/FloatTransition.cs b/src/Avalonia.Animation/Transitions/FloatTransition.cs similarity index 100% rename from src/Avalonia.Animation/FloatTransition.cs rename to src/Avalonia.Animation/Transitions/FloatTransition.cs diff --git a/src/Avalonia.Animation/IntegerTransition.cs b/src/Avalonia.Animation/Transitions/IntegerTransition.cs similarity index 100% rename from src/Avalonia.Animation/IntegerTransition.cs rename to src/Avalonia.Animation/Transitions/IntegerTransition.cs diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index a72d20a57c..cbcf7a7386 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -4,8 +4,10 @@ Avalonia.Base Avalonia True - false + + + diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index 9ee4787e47..f2f3ed9bfc 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -88,18 +88,21 @@ namespace Avalonia.Data.Core _subscriber(value); } - protected void ValueChanged(object value) + protected void ValueChanged(object value) => ValueChanged(value, true); + + private void ValueChanged(object value, bool notify) { var notification = value as BindingNotification; if (notification == null) { LastValue = new WeakReference(value); + if (Next != null) { - Next.Target = new WeakReference(value); + Next.Target = LastValue; } - else + else if (notify) { _subscriber(value); } @@ -110,7 +113,7 @@ namespace Avalonia.Data.Core if (Next != null) { - Next.Target = new WeakReference(notification.Value); + Next.Target = LastValue; } if (Next == null || notification.Error != null) @@ -136,6 +139,7 @@ namespace Avalonia.Data.Core } else { + ValueChanged(AvaloniaProperty.UnsetValue, notify:false); _listening = false; } } diff --git a/src/Avalonia.Base/Platform/IAssetLoader.cs b/src/Avalonia.Base/Platform/IAssetLoader.cs index dda2cbc2d5..691fa2fc12 100644 --- a/src/Avalonia.Base/Platform/IAssetLoader.cs +++ b/src/Avalonia.Base/Platform/IAssetLoader.cs @@ -66,6 +66,6 @@ namespace Avalonia.Platform /// /// The URI. /// All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset - IEnumerable<(string absolutePath, Assembly assembly)> GetAssets(Uri uri); + IEnumerable GetAssets(Uri uri, Uri baseUri); } } diff --git a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs new file mode 100644 index 0000000000..c6845485dc --- /dev/null +++ b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace Avalonia.Threading +{ + public class ThreadSafeObjectPool where T : class, new() + { + private Stack _stack = new Stack(); + private object _lock = new object(); + public static ThreadSafeObjectPool Default { get; } = new ThreadSafeObjectPool(); + + public T Get() + { + lock (_lock) + { + if(_stack.Count == 0) + return new T(); + return _stack.Pop(); + } + } + + public void Return(T obj) + { + lock (_stack) + { + _stack.Push(obj); + } + } + } +} diff --git a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs new file mode 100644 index 0000000000..22e5c952bf --- /dev/null +++ b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Json; + +// ReSharper disable AssignNullToNotNullAttribute + +namespace Avalonia.Utilities +{ + #if !BUILDTASK + public + #endif + static class AvaloniaResourcesIndexReaderWriter + { + private const int LastKnownVersion = 1; + public static List Read(Stream stream) + { + var ver = new BinaryReader(stream).ReadInt32(); + if (ver > LastKnownVersion) + throw new Exception("Resources index format version is not known"); + var index = (AvaloniaResourcesIndex) + new DataContractSerializer(typeof(AvaloniaResourcesIndex)).ReadObject(stream); + return index.Entries; + } + + public static void Write(Stream stream, List entries) + { + new BinaryWriter(stream).Write(LastKnownVersion); + new DataContractSerializer(typeof(AvaloniaResourcesIndex)).WriteObject(stream, + new AvaloniaResourcesIndex() + { + Entries = entries + }); + } + } + + [DataContract] +#if !BUILDTASK + public +#endif + class AvaloniaResourcesIndex + { + [DataMember] + public List Entries { get; set; } = new List(); + } + + [DataContract] +#if !BUILDTASK + public +#endif + class AvaloniaResourcesIndexEntry + { + [DataMember] + public string Path { get; set; } + + [DataMember] + public int Offset { get; set; } + + [DataMember] + public int Size { get; set; } + } +} diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj new file mode 100644 index 0000000000..ce767aaa87 --- /dev/null +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -0,0 +1,18 @@ + + + + netstandard2.0 + tools + $(DefineConstants);BUILDTASK + + + + + Shared/AvaloniaResourcesIndex.cs + + + Shared/AvaloniaResourceXamlInfo.cs + + + + diff --git a/src/Avalonia.Build.Tasks/BuildEngineErrorCode.cs b/src/Avalonia.Build.Tasks/BuildEngineErrorCode.cs new file mode 100644 index 0000000000..3b8a390881 --- /dev/null +++ b/src/Avalonia.Build.Tasks/BuildEngineErrorCode.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Build.Tasks +{ + public enum BuildEngineErrorCode + { + InvalidXAML = 1, + DuplicateXClass = 2, + LegacyResmScheme = 3, + } +} diff --git a/src/Avalonia.Build.Tasks/Extensions.cs b/src/Avalonia.Build.Tasks/Extensions.cs new file mode 100644 index 0000000000..faecee0f37 --- /dev/null +++ b/src/Avalonia.Build.Tasks/Extensions.cs @@ -0,0 +1,21 @@ +using Microsoft.Build.Framework; + +namespace Avalonia.Build.Tasks +{ + public static class Extensions + { + static string FormatErrorCode(BuildEngineErrorCode code) => $"AVLN:{(int)code:0000}"; + + 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, + "", "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")); + } + } +} diff --git a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs new file mode 100644 index 0000000000..98ebb3e7d1 --- /dev/null +++ b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Json; +using System.Text; +using Avalonia.Markup.Xaml.PortableXaml; +using Avalonia.Utilities; +using Microsoft.Build.Framework; +using SPath=System.IO.Path; +namespace Avalonia.Build.Tasks +{ + public class GenerateAvaloniaResourcesTask : ITask + { + [Required] + public ITaskItem[] Resources { get; set; } + [Required] + public string Root { get; set; } + [Required] + public string Output { get; set; } + [Required] + public ITaskItem[] EmbeddedResources { get; set; } + + class Source + { + public string Path { get; set; } + public int Size { get; set; } + private byte[] _data; + private string _sourcePath; + + public Source(string file, 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; + Size = (int)new FileInfo(_sourcePath).Length; + } + + public string SystemPath => _sourcePath ?? Path; + + public Source(string path, byte[] data) + { + Path = path; + _data = data; + Size = data.Length; + } + + public Stream Open() + { + if (_data != null) + return new MemoryStream(_data, false); + return File.OpenRead(_sourcePath); + } + + public string ReadAsString() + { + if (_data != null) + return Encoding.UTF8.GetString(_data); + return File.ReadAllText(_sourcePath); + } + } + + List BuildResourceSources() => Resources.Select(r => new Source(r.ItemSpec, Root)).ToList(); + + private void Pack(Stream output, List sources) + { + var offsets = new Dictionary(); + var coffset = 0; + foreach (var s in sources) + { + offsets[s] = coffset; + coffset += s.Size; + } + var index = sources.Select(s => new AvaloniaResourcesIndexEntry + { + Path = s.Path, + Size = s.Size, + Offset = offsets[s] + }).ToList(); + var ms = new MemoryStream(); + AvaloniaResourcesIndexReaderWriter.Write(ms, index); + new BinaryWriter(output).Write((int)ms.Length); + ms.Position = 0; + ms.CopyTo(output); + foreach (var s in sources) + { + using(var input = s.Open()) + input.CopyTo(output); + } + } + + private bool PreProcessXamlFiles(List sources) + { + var typeToXamlIndex = new Dictionary(); + + foreach (var s in sources.ToList()) + { + if (s.Path.ToLowerInvariant().EndsWith(".xaml") || s.Path.ToLowerInvariant().EndsWith(".paml")) + { + XamlFileInfo info; + try + { + info = XamlFileInfo.Parse(s.ReadAsString()); + } + catch(Exception e) + { + BuildEngine.LogError(BuildEngineErrorCode.InvalidXAML, s.SystemPath, "File doesn't contain valid XAML: " + e); + return false; + } + + if (info.XClass != null) + { + if (typeToXamlIndex.ContainsKey(info.XClass)) + { + + BuildEngine.LogError(BuildEngineErrorCode.DuplicateXClass, s.SystemPath, + $"Duplicate x:Class directive, {info.XClass} is already used in {typeToXamlIndex[info.XClass]}"); + return false; + } + typeToXamlIndex[info.XClass] = s.Path; + } + } + } + + var xamlInfo = new AvaloniaResourceXamlInfo + { + ClassToResourcePathIndex = typeToXamlIndex + }; + var ms = new MemoryStream(); + new DataContractSerializer(typeof(AvaloniaResourceXamlInfo)).WriteObject(ms, xamlInfo); + 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"))) + BuildEngine.LogWarning(BuildEngineErrorCode.LegacyResmScheme, r.ItemSpec, + "XAML file is packed using legacy EmbeddedResource/resm scheme, relative URIs won't work"); + var resources = BuildResourceSources(); + + if (!PreProcessXamlFiles(resources)) + return false; + var dir = Path.GetDirectoryName(Output); + Directory.CreateDirectory(dir); + using (var file = File.Create(Output)) + Pack(file, resources); + return true; + } + + public IBuildEngine BuildEngine { get; set; } + public ITaskHost HostObject { get; set; } + } +} diff --git a/src/Avalonia.Build.Tasks/XamlFileInfo.cs b/src/Avalonia.Build.Tasks/XamlFileInfo.cs new file mode 100644 index 0000000000..d3169f9454 --- /dev/null +++ b/src/Avalonia.Build.Tasks/XamlFileInfo.cs @@ -0,0 +1,20 @@ +using System.Xml.Linq; + +namespace Avalonia.Build.Tasks +{ + public class XamlFileInfo + { + public string XClass { get; set; } + + public static XamlFileInfo Parse(string data) + { + var xdoc = XDocument.Parse(data); + var xclass = xdoc.Root.Attribute(XName.Get("Class", "http://schemas.microsoft.com/winfx/2006/xaml")); + return new XamlFileInfo + { + XClass = xclass?.Value + }; + } + } + +} diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 376714b20b..c5dd072d8a 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -145,6 +145,15 @@ namespace Avalonia.Controls Instance.Run(mainWindow); } + public delegate void AppMainDelegate(Application app, string[] args); + + public void Start(AppMainDelegate main, string[] args) + { + Setup(); + BeforeStartCallback(Self); + main(Instance, args); + } + /// /// Sets up the platform-specific services for the application, but does not run it. /// diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index fddd4403e9..32331d29ab 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -1,7 +1,6 @@  netstandard2.0 - false diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 1f3fcbafb3..d485924885 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -253,10 +253,8 @@ namespace Avalonia.Controls IsPressed = false; e.Handled = true; - var hittest = this.GetVisualsAt(e.GetPosition(this)); - if (ClickMode == ClickMode.Release && - hittest.Any(c => c == this || (c as IStyledElement)?.TemplatedParent == this)) + this.GetVisualsAt(e.GetPosition(this)).Any(c => this == c || this.IsVisualAncestorOf(c))) { OnClick(); } diff --git a/src/Avalonia.Controls/DropDown.cs b/src/Avalonia.Controls/DropDown.cs index 8596d06d2c..93b33e0589 100644 --- a/src/Avalonia.Controls/DropDown.cs +++ b/src/Avalonia.Controls/DropDown.cs @@ -2,9 +2,12 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Linq; using Avalonia.Controls.Generators; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; +using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Media; @@ -12,12 +15,17 @@ using Avalonia.VisualTree; namespace Avalonia.Controls { - /// /// A drop-down list control. /// public class DropDown : SelectingItemsControl { + /// + /// The default value for the property. + /// + private static readonly FuncTemplate DefaultPanel = + new FuncTemplate(() => new VirtualizingStackPanel()); + /// /// Defines the property. /// @@ -39,6 +47,12 @@ namespace Avalonia.Controls public static readonly DirectProperty SelectionBoxItemProperty = AvaloniaProperty.RegisterDirect(nameof(SelectionBoxItem), o => o.SelectionBoxItem); + /// + /// Defines the property. + /// + public static readonly StyledProperty VirtualizationModeProperty = + ItemsPresenter.VirtualizationModeProperty.AddOwner(); + private bool _isDropDownOpen; private Popup _popup; private object _selectionBoxItem; @@ -48,6 +62,7 @@ namespace Avalonia.Controls /// static DropDown() { + ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); FocusableProperty.OverrideDefaultValue(true); SelectedItemProperty.Changed.AddClassHandler(x => x.SelectedItemChanged); KeyDownEvent.AddClassHandler(x => x.OnKeyDown, Interactivity.RoutingStrategies.Tunnel); @@ -80,6 +95,15 @@ namespace Avalonia.Controls set { SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value); } } + /// + /// Gets or sets the virtualization mode for the items. + /// + public ItemVirtualizationMode VirtualizationMode + { + get { return GetValue(VirtualizationModeProperty); } + set { SetValue(VirtualizationModeProperty, value); } + } + /// protected override IItemContainerGenerator CreateItemContainerGenerator() { @@ -138,6 +162,16 @@ namespace Avalonia.Controls e.Handled = true; } } + else if (IsDropDownOpen && SelectedIndex < 0 && ItemCount > 0 && + (e.Key == Key.Up || e.Key == Key.Down)) + { + var firstChild = Presenter?.Panel?.Children.FirstOrDefault(c => CanFocus(c)); + if (firstChild != null) + { + FocusManager.Instance?.Focus(firstChild, NavigationMethod.Directional); + e.Handled = true; + } + } } /// @@ -159,6 +193,7 @@ namespace Avalonia.Controls e.Handled = true; } } + base.OnPointerPressed(e); } @@ -168,28 +203,65 @@ namespace Avalonia.Controls if (_popup != null) { _popup.Opened -= PopupOpened; + _popup.Closed -= PopupClosed; } _popup = e.NameScope.Get("PART_Popup"); _popup.Opened += PopupOpened; + _popup.Closed += PopupClosed; + + base.OnTemplateApplied(e); } - private void PopupOpened(object sender, EventArgs e) + internal void ItemFocused(DropDownItem dropDownItem) { - var selectedIndex = SelectedIndex; + if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid) + { + dropDownItem.BringIntoView(); + } + } - if (selectedIndex != -1) + private void PopupClosed(object sender, EventArgs e) + { + if (CanFocus(this)) { - var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); - container?.Focus(); + Focus(); } } + private void PopupOpened(object sender, EventArgs e) + { + TryFocusSelectedItem(); + } + private void SelectedItemChanged(AvaloniaPropertyChangedEventArgs e) { UpdateSelectionBoxItem(e.NewValue); + TryFocusSelectedItem(); + } + + private void TryFocusSelectedItem() + { + var selectedIndex = SelectedIndex; + if (IsDropDownOpen && selectedIndex != -1) + { + var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); + + if(container == null && SelectedItems.Count > 0) + { + ScrollIntoView(SelectedItems[0]); + container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); + } + + if (container != null && CanFocus(container)) + { + container.Focus(); + } + } } + private bool CanFocus(IControl control) => control.Focusable && control.IsEnabledCore && control.IsVisible; + private void UpdateSelectionBoxItem(object item) { var contentControl = item as IContentControl; @@ -219,7 +291,8 @@ namespace Avalonia.Controls } else { - SelectionBoxItem = item; + var selector = MemberSelector; + SelectionBoxItem = selector != null ? selector.Select(item) : item; } } diff --git a/src/Avalonia.Controls/DropDownItem.cs b/src/Avalonia.Controls/DropDownItem.cs index fb465e93ec..1e22ededf6 100644 --- a/src/Avalonia.Controls/DropDownItem.cs +++ b/src/Avalonia.Controls/DropDownItem.cs @@ -2,43 +2,19 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reactive.Linq; namespace Avalonia.Controls { /// /// A selectable item in a . /// - public class DropDownItem : ContentControl, ISelectable + public class DropDownItem : ListBoxItem { - /// - /// Defines the property. - /// - public static readonly StyledProperty IsSelectedProperty = - AvaloniaProperty.Register(nameof(IsSelected)); - - /// - /// Initializes static members of the class. - /// - static DropDownItem() - { - FocusableProperty.OverrideDefaultValue(true); - } - public DropDownItem() { - this.GetObservable(DropDownItem.IsFocusedProperty).Subscribe(focused => - { - PseudoClasses.Set(":selected", focused); - }); - } - - /// - /// Gets or sets the selection state of the item. - /// - public bool IsSelected - { - get { return GetValue(IsSelectedProperty); } - set { SetValue(IsSelectedProperty, value); } + this.GetObservable(DropDownItem.IsFocusedProperty).Where(focused => focused) + .Subscribe(_ => (Parent as DropDown)?.ItemFocused(this)); } } } diff --git a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs new file mode 100644 index 0000000000..a6a64e570b --- /dev/null +++ b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs @@ -0,0 +1,59 @@ +// Copyright (c) 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; + +namespace Avalonia.Controls.Generators +{ + public class TabItemContainerGenerator : ItemContainerGenerator + { + public TabItemContainerGenerator(TabControl owner) + : base(owner, ContentControl.ContentProperty, ContentControl.ContentTemplateProperty) + { + Owner = owner; + } + + public new TabControl Owner { get; } + + protected override IControl CreateContainer(object item) + { + var tabItem = (TabItem)base.CreateContainer(item); + + tabItem.ParentTabControl = Owner; + + tabItem[~TabControl.TabStripPlacementProperty] = Owner[~TabControl.TabStripPlacementProperty]; + + if (tabItem.HeaderTemplate == null) + { + tabItem[~HeaderedContentControl.HeaderTemplateProperty] = Owner[~ItemsControl.ItemTemplateProperty]; + } + + if (tabItem.Header == null) + { + if (item is IHeadered headered) + { + tabItem.Header = headered.Header; + } + else + { + if (!(tabItem.DataContext is IControl)) + { + tabItem.Header = tabItem.DataContext; + } + } + } + + if (!(tabItem.Content is IControl)) + { + tabItem[~ContentControl.ContentTemplateProperty] = Owner[~TabControl.ContentTemplateProperty]; + } + + if (tabItem.Content == null) + { + tabItem[~ContentControl.ContentProperty] = tabItem[~StyledElement.DataContextProperty]; + } + + return tabItem; + } + } +} diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index 87e3853643..950d4f34da 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -5,28 +5,30 @@ // http://silverlight.codeplex.com/SourceControl/changeset/view/74775#Release/Silverlight4/Source/Controls.Layout.Toolkit/LayoutTransformer/LayoutTransformer.cs // -using Avalonia.Controls.Primitives; -using Avalonia.Media; -using Avalonia.VisualTree; using System; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Reactive.Linq; +using Avalonia.Media; namespace Avalonia.Controls { /// /// Control that implements support for transformations as if applied by LayoutTransform. /// - public class LayoutTransformControl : ContentControl + public class LayoutTransformControl : Decorator { public static readonly AvaloniaProperty LayoutTransformProperty = AvaloniaProperty.Register(nameof(LayoutTransform)); static LayoutTransformControl() { + ClipToBoundsProperty.OverrideDefaultValue(true); + LayoutTransformProperty.Changed .AddClassHandler(x => x.OnLayoutTransformChanged); + + ChildProperty.Changed + .AddClassHandler(x => x.OnChildChanged); } /// @@ -38,8 +40,7 @@ namespace Avalonia.Controls set { SetValue(LayoutTransformProperty, value); } } - public Control TransformRoot => _transformRoot ?? - (_transformRoot = this.GetVisualChildren().OfType().FirstOrDefault()); + public IControl TransformRoot => Child; /// /// Provides the behavior for the "Arrange" pass of layout. @@ -132,16 +133,8 @@ namespace Avalonia.Controls return transformedDesiredSize; } - /// - /// Builds the visual tree for the LayoutTransformerControl when a new - /// template is applied. - /// - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + private void OnChildChanged(AvaloniaPropertyChangedEventArgs e) { - base.OnTemplateApplied(e); - - _matrixTransform = new MatrixTransform(); - if (null != TransformRoot) { TransformRoot.RenderTransform = _matrixTransform; @@ -169,14 +162,14 @@ namespace Avalonia.Controls /// /// RenderTransform/MatrixTransform applied to TransformRoot. /// - private MatrixTransform _matrixTransform; + private MatrixTransform _matrixTransform = new MatrixTransform(); /// /// Transformation matrix corresponding to _matrixTransform. /// private Matrix _transformation; private IDisposable _transformChangedEvent = null; - private Control _transformRoot; + /// /// Returns true if Size a is smaller than Size b in either dimension. /// @@ -215,7 +208,8 @@ namespace Avalonia.Controls /// private void ApplyLayoutTransform() { - if (LayoutTransform == null) return; + if (LayoutTransform == null) + return; // Get the transform matrix and apply it _transformation = RoundMatrix(LayoutTransform.Value, DecimalsAfterRound); @@ -376,11 +370,8 @@ namespace Avalonia.Controls { var newTransform = e.NewValue as Transform; - if (_transformChangedEvent != null) - { - _transformChangedEvent.Dispose(); - _transformChangedEvent = null; - } + _transformChangedEvent?.Dispose(); + _transformChangedEvent = null; if (newTransform != null) { @@ -392,4 +383,4 @@ namespace Avalonia.Controls ApplyLayoutTransform(); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 5415f3974d..0f365fcb08 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -31,7 +31,6 @@ namespace Avalonia.Controls static Panel() { AffectsRender(BackgroundProperty); - ClipToBoundsProperty.OverrideDefaultValue(true); } /// diff --git a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs index d67ebfd489..7a46e0f776 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs @@ -1,6 +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 Avalonia.Controls.Templates; + namespace Avalonia.Controls.Primitives { /// @@ -12,7 +14,13 @@ namespace Avalonia.Controls.Primitives /// Defines the property. /// public static readonly StyledProperty HeaderProperty = - AvaloniaProperty.Register(nameof(Header)); + AvaloniaProperty.Register(nameof(Header)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HeaderTemplateProperty = + AvaloniaProperty.Register(nameof(HeaderTemplate)); /// /// Gets or sets the header content. @@ -21,6 +29,15 @@ namespace Avalonia.Controls.Primitives { get { return GetValue(HeaderProperty); } set { SetValue(HeaderProperty, value); } + } + + /// + /// Gets or sets the data template used to display the header content of the control. + /// + public IDataTemplate HeaderTemplate + { + get { return GetValue(HeaderTemplateProperty); } + set { SetValue(HeaderTemplateProperty, value); } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index ccbdc71b1d..c40ddc37ad 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -431,9 +431,12 @@ namespace Avalonia.Controls.Primitives { if (i.ContainerControl != null && i.Item != null) { - MarkContainerSelected( - i.ContainerControl, - SelectedItems.Contains(i.Item)); + var ms = MemberSelector; + bool selected = ms == null ? + SelectedItems.Contains(i.Item) : + SelectedItems.OfType().Any(v => Equals(ms.Select(v), i.Item)); + + MarkContainerSelected(i.ContainerControl, selected); } } } diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 3aae256858..8eaf166f57 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -1,10 +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 Avalonia.Animation; using Avalonia.Controls.Generators; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Layout; namespace Avalonia.Controls { @@ -14,28 +16,46 @@ namespace Avalonia.Controls public class TabControl : SelectingItemsControl { /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty PageTransitionProperty = - Avalonia.Controls.Carousel.PageTransitionProperty.AddOwner(); + public static readonly StyledProperty TabStripPlacementProperty = + AvaloniaProperty.Register(nameof(TabStripPlacement), defaultValue: Dock.Top); /// - /// Defines an that selects the content of a . + /// Defines the property. /// - public static readonly IMemberSelector ContentSelector = - new FuncMemberSelector(SelectContent); + public static readonly StyledProperty HorizontalContentAlignmentProperty = + ContentControl.HorizontalContentAlignmentProperty.AddOwner(); /// - /// Defines an that selects the header of a . + /// Defines the property. /// - public static readonly IMemberSelector HeaderSelector = - new FuncMemberSelector(SelectHeader); + public static readonly StyledProperty VerticalContentAlignmentProperty = + ContentControl.VerticalContentAlignmentProperty.AddOwner(); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty TabStripPlacementProperty = - AvaloniaProperty.Register(nameof(TabStripPlacement), defaultValue: Dock.Top); + public static readonly StyledProperty ContentTemplateProperty = + ContentControl.ContentTemplateProperty.AddOwner(); + + /// + /// The selected content property + /// + public static readonly StyledProperty SelectedContentProperty = + AvaloniaProperty.Register(nameof(SelectedContent)); + + /// + /// The selected content template property + /// + public static readonly StyledProperty SelectedContentTemplateProperty = + AvaloniaProperty.Register(nameof(SelectedContentTemplate)); + + /// + /// The default value for the property. + /// + private static readonly FuncTemplate DefaultPanel = + new FuncTemplate(() => new WrapPanel()); /// /// Initializes static members of the class. @@ -43,107 +63,107 @@ namespace Avalonia.Controls static TabControl() { SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); - FocusableProperty.OverrideDefaultValue(false); + ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); AffectsMeasure(TabStripPlacementProperty); } /// - /// Gets the pages portion of the 's template. + /// Gets or sets the horizontal alignment of the content within the control. /// - public IControl Pages + public HorizontalAlignment HorizontalContentAlignment { - get; - private set; + get { return GetValue(HorizontalContentAlignmentProperty); } + set { SetValue(HorizontalContentAlignmentProperty, value); } } /// - /// Gets the tab strip portion of the 's template. + /// Gets or sets the vertical alignment of the content within the control. /// - public IControl TabStrip + public VerticalAlignment VerticalContentAlignment { - get; - private set; + get { return GetValue(VerticalContentAlignmentProperty); } + set { SetValue(VerticalContentAlignmentProperty, value); } } /// - /// Gets or sets the transition to use when switching tabs. + /// Gets or sets the tabstrip placement of the TabControl. /// - public IPageTransition PageTransition + public Dock TabStripPlacement { - get { return GetValue(PageTransitionProperty); } - set { SetValue(PageTransitionProperty, value); } + get { return GetValue(TabStripPlacementProperty); } + set { SetValue(TabStripPlacementProperty, value); } } /// - /// Gets or sets the tabstrip placement of the tabcontrol. + /// Gets or sets the default data template used to display the content of the selected tab. /// - public Dock TabStripPlacement + public IDataTemplate ContentTemplate { - get { return GetValue(TabStripPlacementProperty); } - set { SetValue(TabStripPlacementProperty, value); } + get { return GetValue(ContentTemplateProperty); } + set { SetValue(ContentTemplateProperty, value); } } + /// + /// Gets or sets the content of the selected tab. + /// + /// + /// The content of the selected tab. + /// + public object SelectedContent + { + get { return GetValue(SelectedContentProperty); } + internal set { SetValue(SelectedContentProperty, value); } + } + + /// + /// Gets or sets the content template for the selected tab. + /// + /// + /// The content template of the selected tab. + /// + public IDataTemplate SelectedContentTemplate + { + get { return GetValue(SelectedContentTemplateProperty); } + internal set { SetValue(SelectedContentTemplateProperty, value); } + } + + internal ItemsPresenter ItemsPresenterPart { get; private set; } + + internal ContentPresenter ContentPart { get; private set; } + protected override IItemContainerGenerator CreateItemContainerGenerator() { - // TabControl doesn't actually create items - instead its TabStrip and Carousel - // children create the items. However we want it to be a SelectingItemsControl - // so that it has the Items/SelectedItem etc properties. In this case, we can - // return a null ItemContainerGenerator to disable the creation of item containers. - return null; + return new TabItemContainerGenerator(this); } protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { base.OnTemplateApplied(e); - TabStrip = e.NameScope.Find("PART_TabStrip"); - Pages = e.NameScope.Find("PART_Content"); + ItemsPresenterPart = e.NameScope.Get("PART_ItemsPresenter"); + + ContentPart = e.NameScope.Get("PART_Content"); } - /// - /// Selects the content of a tab item. - /// - /// The tab item. - /// The content. - private static object SelectContent(object o) + /// + protected override void OnGotFocus(GotFocusEventArgs e) { - var content = o as IContentControl; + base.OnGotFocus(e); - if (content != null) + if (e.NavigationMethod == NavigationMethod.Directional) { - return content.Content; + e.Handled = UpdateSelectionFromEventSource(e.Source); } - else - { - return o; - } } - /// - /// Selects the header of a tab item. - /// - /// The tab item. - /// The content. - private static object SelectHeader(object o) + /// + protected override void OnPointerPressed(PointerPressedEventArgs e) { - var headered = o as IHeadered; - var control = o as IControl; + base.OnPointerPressed(e); - if (headered != null) - { - return headered.Header ?? string.Empty; - } - else if (control != null) - { - // Non-headered control items should result in TabStripItems with empty content. - // If a TabStrip is created with non IHeadered controls as its items, don't try to - // display the control in the TabStripItem: the content portion will also try to - // display this control, resulting in dual-parentage breakage. - return string.Empty; - } - else + if (e.MouseButton == MouseButton.Left) { - return o; + e.Handled = UpdateSelectionFromEventSource(e.Source); } } } diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 4fb68c8b6f..47a2348d59 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -11,6 +11,12 @@ namespace Avalonia.Controls /// public class TabItem : HeaderedContentControl, ISelectable { + /// + /// Defines the property. + /// + public static readonly StyledProperty TabStripPlacementProperty = + TabControl.TabStripPlacementProperty.AddOwner(); + /// /// Defines the property. /// @@ -24,6 +30,19 @@ namespace Avalonia.Controls { SelectableMixin.Attach(IsSelectedProperty); FocusableProperty.OverrideDefaultValue(typeof(TabItem), true); + IsSelectedProperty.Changed.AddClassHandler(x => x.UpdateSelectedContent); + DataContextProperty.Changed.AddClassHandler(x => x.UpdateHeader); + } + + /// + /// Gets the tab strip placement. + /// + /// + /// The tab strip placement. + /// + public Dock TabStripPlacement + { + get { return GetValue(TabStripPlacementProperty); } } /// @@ -34,5 +53,53 @@ namespace Avalonia.Controls get { return GetValue(IsSelectedProperty); } set { SetValue(IsSelectedProperty, value); } } + + internal TabControl ParentTabControl { get; set; } + + private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj) + { + if (Header == null) + { + if (obj.NewValue is IHeadered headered) + { + if (Header != headered.Header) + { + Header = headered.Header; + } + } + else + { + if (!(obj.NewValue is IControl)) + { + Header = obj.NewValue; + } + } + } + else + { + if (Header == obj.OldValue) + { + Header = obj.NewValue; + } + } + } + + private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e) + { + if (!IsSelected) + { + return; + } + + if (ParentTabControl.SelectedContentTemplate != ContentTemplate) + { + ParentTabControl.SelectedContentTemplate = ContentTemplate; + } + + if (ParentTabControl.SelectedContent != Content) + { + ParentTabControl.SelectedContent = Content; + } + } } } diff --git a/src/Avalonia.Controls/Viewbox.cs b/src/Avalonia.Controls/Viewbox.cs new file mode 100644 index 0000000000..db753f4ab4 --- /dev/null +++ b/src/Avalonia.Controls/Viewbox.cs @@ -0,0 +1,124 @@ +using System; +using Avalonia.Media; + +namespace Avalonia.Controls +{ + /// + /// Viewbox is used to scale single child. + /// + /// + public class Viewbox : Decorator + { + /// + /// The stretch property + /// + public static AvaloniaProperty StretchProperty = + AvaloniaProperty.RegisterDirect(nameof(Stretch), + v => v.Stretch, (c, v) => c.Stretch = v, Stretch.Uniform); + + private Stretch _stretch = Stretch.Uniform; + + /// + /// Gets or sets the stretch mode, + /// which determines how child fits into the available space. + /// + /// + /// The stretch. + /// + public Stretch Stretch + { + get => _stretch; + set => SetAndRaise(StretchProperty, ref _stretch, value); + } + + static Viewbox() + { + ClipToBoundsProperty.OverrideDefaultValue(true); + AffectsMeasure(StretchProperty); + } + + protected override Size MeasureOverride(Size availableSize) + { + var child = Child; + + if (child != null) + { + child.Measure(Size.Infinity); + + var childSize = child.DesiredSize; + + var scale = GetScale(availableSize, childSize, Stretch); + + return childSize * scale; + } + + return new Size(); + } + + protected override Size ArrangeOverride(Size finalSize) + { + var child = Child; + + if (child != null) + { + var childSize = child.DesiredSize; + var scale = GetScale(finalSize, childSize, Stretch); + var scaleTransform = child.RenderTransform as ScaleTransform; + + if (scaleTransform == null) + { + child.RenderTransform = scaleTransform = new ScaleTransform(scale.X, scale.Y); + child.RenderTransformOrigin = RelativePoint.TopLeft; + } + + scaleTransform.ScaleX = scale.X; + scaleTransform.ScaleY = scale.Y; + + child.Arrange(new Rect(childSize)); + + return childSize * scale; + } + + return new Size(); + } + + private static Vector GetScale(Size availableSize, Size childSize, Stretch stretch) + { + double scaleX = 1.0; + double scaleY = 1.0; + + bool validWidth = !double.IsPositiveInfinity(availableSize.Width); + bool validHeight = !double.IsPositiveInfinity(availableSize.Height); + + if (stretch != Stretch.None && (validWidth || validHeight)) + { + scaleX = childSize.Width <= 0.0 ? 0.0 : availableSize.Width / childSize.Width; + scaleY = childSize.Height <= 0.0 ? 0.0 : availableSize.Height / childSize.Height; + + if (!validWidth) + { + scaleX = scaleY; + } + else if (!validHeight) + { + scaleY = scaleX; + } + else + { + switch (stretch) + { + case Stretch.Uniform: + scaleX = scaleY = Math.Min(scaleX, scaleY); + break; + + case Stretch.UniformToFill: + scaleX = scaleY = Math.Max(scaleX, scaleY); + break; + } + } + } + + return new Vector(scaleX, scaleY); + } + } +} diff --git a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj index e732a7eef0..2c5aaf1a70 100644 --- a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj +++ b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj @@ -1,7 +1,6 @@  netstandard2.0 - false + diff --git a/src/Avalonia.Themes.Default/DropDown.xaml b/src/Avalonia.Themes.Default/DropDown.xaml index 451f1c2f23..ad2be275d6 100644 --- a/src/Avalonia.Themes.Default/DropDown.xaml +++ b/src/Avalonia.Themes.Default/DropDown.xaml @@ -35,15 +35,21 @@ MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}" MaxHeight="{TemplateBinding MaxDropDownHeight}" PlacementTarget="{TemplateBinding}" + ObeyScreenEdges="True" StaysOpen="False"> - - - + + + + + @@ -54,4 +60,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/DropDownItem.xaml b/src/Avalonia.Themes.Default/DropDownItem.xaml index f52608c0a8..f542a34d71 100644 --- a/src/Avalonia.Themes.Default/DropDownItem.xaml +++ b/src/Avalonia.Themes.Default/DropDownItem.xaml @@ -18,7 +18,7 @@ - + diff --git a/src/Avalonia.Themes.Default/FocusAdorner.xaml b/src/Avalonia.Themes.Default/FocusAdorner.xaml index 573c43dc8d..2d5e369573 100644 --- a/src/Avalonia.Themes.Default/FocusAdorner.xaml +++ b/src/Avalonia.Themes.Default/FocusAdorner.xaml @@ -3,7 +3,8 @@ + StrokeDashArray="1,2" + Margin="1"/> - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/LayoutTransformControl.xaml b/src/Avalonia.Themes.Default/LayoutTransformControl.xaml deleted file mode 100644 index b26f053622..0000000000 --- a/src/Avalonia.Themes.Default/LayoutTransformControl.xaml +++ /dev/null @@ -1,13 +0,0 @@ - \ No newline at end of file diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index 07faf7a632..f2940227e1 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -49,7 +49,7 @@ ObeyScreenEdges="True"> + BorderThickness="{TemplateBinding BorderThickness}"> + BorderThickness="{TemplateBinding BorderThickness}"> - + - - - - - \ No newline at end of file + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/TabItem.xaml b/src/Avalonia.Themes.Default/TabItem.xaml new file mode 100644 index 0000000000..fcdb76524e --- /dev/null +++ b/src/Avalonia.Themes.Default/TabItem.xaml @@ -0,0 +1,45 @@ + + + + + + + + + + diff --git a/src/Avalonia.Visuals/Animation/Animators/ColorAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/ColorAnimator.cs new file mode 100644 index 0000000000..6d1f6c39bd --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Animators/ColorAnimator.cs @@ -0,0 +1,70 @@ +// Original color interpolation code was written by Romain Guy and Francois Blavoet +// and adopted from LottieSharp Project (https://github.com/ascora/LottieSharp). + +using System; +using System.Reactive.Disposables; +using Avalonia.Logging; +using Avalonia.Media; + +namespace Avalonia.Animation.Animators +{ + /// + /// Animator that interpolates through + /// gamma sRGB color space for better visual result. + /// + public class ColorAnimator : Animator + { + // Opto-electronic conversion function for the sRGB color space + // Takes a gamma-encoded sRGB value and converts it to a linear sRGB value + private static double OECF_sRGB(double linear) + { + // IEC 61966-2-1:1999 + return linear <= 0.0031308d ? linear * 12.92d : (double)(Math.Pow(linear, 1.0d / 2.4d) * 1.055d - 0.055d); + } + + // Electro-optical conversion function for the sRGB color space + // Takes a linear sRGB value and converts it to a gamma-encoded sRGB value + private static double EOCF_sRGB(double srgb) + { + // IEC 61966-2-1:1999 + return srgb <= 0.04045d ? srgb / 12.92d : (double)Math.Pow((srgb + 0.055d) / 1.055d, 2.4d); + } + + public override Color Interpolate(double progress, Color oldValue, Color newValue) + { + // normalize sRGB values. + var oldA = oldValue.A / 255d; + var oldR = oldValue.R / 255d; + var oldG = oldValue.G / 255d; + var oldB = oldValue.B / 255d; + + var newA = newValue.A / 255d; + var newR = newValue.R / 255d; + var newG = newValue.G / 255d; + var newB = newValue.B / 255d; + + // convert from sRGB to linear + oldR = EOCF_sRGB(oldR); + oldG = EOCF_sRGB(oldG); + oldB = EOCF_sRGB(oldB); + + newR = EOCF_sRGB(newR); + newG = EOCF_sRGB(newG); + newB = EOCF_sRGB(newB); + + // compute the interpolated color in linear space + var a = oldA + progress * (newA - oldA); + var r = oldR + progress * (newR - oldR); + var g = oldG + progress * (newG - oldG); + var b = oldB + progress * (newB - oldB); + + // convert back to sRGB in the [0..255] range + a = a * 255d; + r = OECF_sRGB(r) * 255d; + g = OECF_sRGB(g) * 255d; + b = OECF_sRGB(b) * 255d; + + return new Color((byte)Math.Round(a), (byte)Math.Round(r), (byte)Math.Round(g), (byte)Math.Round(b)); + } + } +} diff --git a/src/Avalonia.Visuals/Animation/Animators/CornerRadiusAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/CornerRadiusAnimator.cs new file mode 100644 index 0000000000..8129bd38de --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Animators/CornerRadiusAnimator.cs @@ -0,0 +1,27 @@ +using System; +using Avalonia.Logging; +using Avalonia.Media; + +namespace Avalonia.Animation.Animators +{ + /// + /// Animator that handles properties. + /// + public class CornerRadiusAnimator : Animator + { + public override CornerRadius Interpolate(double progress, CornerRadius oldValue, CornerRadius newValue) + { + var deltaTL = newValue.TopLeft - oldValue.TopLeft; + var deltaTR = newValue.TopRight - oldValue.TopRight; + var deltaBR = newValue.BottomRight - oldValue.BottomRight; + var deltaBL = newValue.BottomLeft - oldValue.BottomLeft; + + var nTL = progress * deltaTL + oldValue.TopLeft; + var nTR = progress * deltaTR + oldValue.TopRight; + var nBR = progress * deltaBR + oldValue.BottomRight; + var nBL = progress * deltaBL + oldValue.BottomLeft; + + return new CornerRadius(nTL, nTR, nBR, nBL); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Animation/Animators/PointAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/PointAnimator.cs new file mode 100644 index 0000000000..793691e5ac --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Animators/PointAnimator.cs @@ -0,0 +1,17 @@ +using System; +using Avalonia.Logging; +using Avalonia.Media; + +namespace Avalonia.Animation.Animators +{ + /// + /// Animator that handles properties. + /// + public class PointAnimator : Animator + { + public override Point Interpolate(double progress, Point oldValue, Point newValue) + { + return ((newValue - oldValue) * progress) + oldValue; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Animation/Animators/RectAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/RectAnimator.cs new file mode 100644 index 0000000000..b2d3ec2996 --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Animators/RectAnimator.cs @@ -0,0 +1,23 @@ +using System; +using Avalonia.Logging; +using Avalonia.Media; + +namespace Avalonia.Animation.Animators +{ + /// + /// Animator that handles properties. + /// + public class RectAnimator : Animator + { + public override Rect Interpolate(double progress, Rect oldValue, Rect newValue) + { + var deltaPos = newValue.Position - oldValue.Position; + var deltaSize = newValue.Size - oldValue.Size; + + var newPos = (deltaPos * progress) + oldValue.Position; + var newSize = (deltaSize * progress) + oldValue.Size; + + return new Rect(newPos, newSize); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Animation/Animators/SizeAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/SizeAnimator.cs new file mode 100644 index 0000000000..bb4f343e4a --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Animators/SizeAnimator.cs @@ -0,0 +1,17 @@ +using System; +using Avalonia.Logging; +using Avalonia.Media; + +namespace Avalonia.Animation.Animators +{ + /// + /// Animator that handles properties. + /// + public class SizeAnimator : Animator + { + public override Size Interpolate(double progress, Size oldValue, Size newValue) + { + return ((newValue - oldValue) * progress) + oldValue; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs new file mode 100644 index 0000000000..8776d3a7b7 --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs @@ -0,0 +1,74 @@ +using System; +using System.Reactive.Disposables; +using Avalonia.Logging; +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace Avalonia.Animation.Animators +{ + /// + /// Animator that handles . + /// + public class SolidColorBrushAnimator : Animator + { + ColorAnimator _colorAnimator; + + void InitializeColorAnimator() + { + _colorAnimator = new ColorAnimator(); + + foreach (AnimatorKeyFrame keyframe in this) + { + _colorAnimator.Add(keyframe); + } + + _colorAnimator.Property = SolidColorBrush.ColorProperty; + } + + public override IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable match, Action onComplete) + { + 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)) + { + keyframe.Value = ((ImmutableSolidColorBrush)keyframe.Value).Color; + } + } + + // Add SCB if the target prop is empty. + if (control.GetValue(Property) == null) + control.SetValue(Property, new SolidColorBrush(Colors.Transparent)); + + var targetVal = control.GetValue(Property); + + // Continue if target prop is not empty & is a SolidColorBrush derivative. + if (typeof(ISolidColorBrush).IsAssignableFrom(targetVal.GetType())) + { + 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); + } + + return Disposable.Empty; + } + + public override SolidColorBrush Interpolate(double p, SolidColorBrush o, SolidColorBrush n) => null; + } +} diff --git a/src/Avalonia.Visuals/Animation/Animators/ThicknessAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/ThicknessAnimator.cs new file mode 100644 index 0000000000..a6ae95ce3f --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Animators/ThicknessAnimator.cs @@ -0,0 +1,17 @@ +using System; +using Avalonia.Logging; +using Avalonia.Media; + +namespace Avalonia.Animation.Animators +{ + /// + /// Animator that handles properties. + /// + public class ThicknessAnimator : Animator + { + public override Thickness Interpolate(double progress, Thickness oldValue, Thickness newValue) + { + return ((newValue - oldValue) * progress) + oldValue; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Animation/TransformAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs similarity index 75% rename from src/Avalonia.Visuals/Animation/TransformAnimator.cs rename to src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs index 64b3ebd626..e7be272f13 100644 --- a/src/Avalonia.Visuals/Animation/TransformAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs @@ -2,14 +2,14 @@ using Avalonia.Logging; using Avalonia.Media; -namespace Avalonia.Animation +namespace Avalonia.Animation.Animators { /// /// Animator that handles properties. /// public class TransformAnimator : Animator { - DoubleAnimator childAnimator; + DoubleAnimator _doubleAnimator; /// public override IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable obsMatch, Action onComplete) @@ -27,7 +27,7 @@ namespace Avalonia.Animation // default RenderTransform order. normalTransform.Children.Add(new ScaleTransform()); - normalTransform.Children.Add(new SkewTransform()); + normalTransform.Children.Add(new SkewTransform()); normalTransform.Children.Add(new RotateTransform()); normalTransform.Children.Add(new TranslateTransform()); @@ -36,15 +36,22 @@ namespace Avalonia.Animation var renderTransformType = ctrl.RenderTransform.GetType(); - if (childAnimator == null) + if (_doubleAnimator == null) { - InitializeChildAnimator(); + _doubleAnimator = new DoubleAnimator(); + + foreach (AnimatorKeyFrame keyframe in this) + { + _doubleAnimator.Add(keyframe); + } + + _doubleAnimator.Property = Property; } // It's a transform object so let's target that. if (renderTransformType == Property.OwnerType) { - return childAnimator.Apply(animation, ctrl.RenderTransform, clock ?? control.Clock, obsMatch, onComplete); + return _doubleAnimator.Apply(animation, ctrl.RenderTransform, clock ?? control.Clock, obsMatch, onComplete); } // It's a TransformGroup and try finding the target there. else if (renderTransformType == typeof(TransformGroup)) @@ -53,7 +60,7 @@ namespace Avalonia.Animation { if (transform.GetType() == Property.OwnerType) { - return childAnimator.Apply(animation, transform, clock ?? control.Clock, obsMatch, onComplete); + return _doubleAnimator.Apply(animation, transform, clock ?? control.Clock, obsMatch, onComplete); } } } @@ -73,19 +80,7 @@ namespace Avalonia.Animation return null; } - void InitializeChildAnimator() - { - childAnimator = new DoubleAnimator(); - - foreach (AnimatorKeyFrame keyframe in this) - { - childAnimator.Add(keyframe); - } - - childAnimator.Property = Property; - } - - /// - protected override double DoInterpolation(double time, double neutralValue) => 0; + /// + public override double Interpolate(double p, double o, double n) => 0; } } diff --git a/src/Avalonia.Visuals/Animation/Animators/VectorAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/VectorAnimator.cs new file mode 100644 index 0000000000..e1ffb60971 --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Animators/VectorAnimator.cs @@ -0,0 +1,17 @@ +using System; +using Avalonia.Logging; +using Avalonia.Media; + +namespace Avalonia.Animation.Animators +{ + /// + /// Animator that handles properties. + /// + public class VectorAnimator : Animator + { + public override Vector Interpolate(double progress, Vector oldValue, Vector newValue) + { + return ((newValue - oldValue) * progress) + oldValue; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Animation/CrossFade.cs b/src/Avalonia.Visuals/Animation/CrossFade.cs index d5ddf1c7f5..6b8cb8b755 100644 --- a/src/Avalonia.Visuals/Animation/CrossFade.cs +++ b/src/Avalonia.Visuals/Animation/CrossFade.cs @@ -21,7 +21,7 @@ namespace Avalonia.Animation /// Initializes a new instance of the class. /// public CrossFade() - :this(TimeSpan.Zero) + : this(TimeSpan.Zero) { } @@ -33,30 +33,40 @@ namespace Avalonia.Animation { _fadeOutAnimation = new Animation { - new KeyFrame - ( - new Setter + Children = + { + new KeyFrame() { - Property = Visual.OpacityProperty, - Value = 0d + Setters = + { + new Setter + { + Property = Visual.OpacityProperty, + Value = 0d + } + }, + Cue = new Cue(1d) } - ) - { - Cue = new Cue(1d) + } }; _fadeInAnimation = new Animation { - new KeyFrame - ( - new Setter + Children = + { + new KeyFrame() { - Property = Visual.OpacityProperty, - Value = 0d + Setters = + { + new Setter + { + Property = Visual.OpacityProperty, + Value = 0d + } + }, + Cue = new Cue(0d) } - ) - { - Cue = new Cue(0d) + } }; _fadeOutAnimation.Duration = _fadeInAnimation.Duration = duration; diff --git a/src/Avalonia.Visuals/Animation/PageSlide.cs b/src/Avalonia.Visuals/Animation/PageSlide.cs index c5d43068c1..0aa85035ae 100644 --- a/src/Avalonia.Visuals/Animation/PageSlide.cs +++ b/src/Avalonia.Visuals/Animation/PageSlide.cs @@ -74,34 +74,36 @@ namespace Avalonia.Animation var distance = Orientation == SlideAxis.Horizontal ? parent.Bounds.Width : parent.Bounds.Height; var translateProperty = Orientation == SlideAxis.Horizontal ? TranslateTransform.XProperty : TranslateTransform.YProperty; - - // TODO: Implement relevant transition logic here (or discard this class) - // in favor of XAML based transition for pages if (from != null) { var animation = new Animation { - new KeyFrame - ( - new Setter - { - Property = translateProperty, - Value = 0d - } - ) + Children = { - Cue = new Cue(0d) - }, - new KeyFrame - ( - new Setter + new KeyFrame { - Property = translateProperty, - Value = forward ? -distance : distance - } - ) - { - Cue = new Cue(1d) + Setters = + { + new Setter + { + Property = translateProperty, + Value = 0d + } + }, + Cue = new Cue(0d) + }, + new KeyFrame + { + Setters = + { + new Setter + { + Property = translateProperty, + Value = forward ? -distance : distance + } + }, + Cue = new Cue(1d) + } } }; animation.Duration = Duration; @@ -113,29 +115,34 @@ namespace Avalonia.Animation to.IsVisible = true; var animation = new Animation { + Children = + { - new KeyFrame - ( - new Setter + new KeyFrame { - Property = translateProperty, - Value = forward ? distance : -distance - } - ) - { - Cue = new Cue(0d) - }, - new KeyFrame - ( - new Setter + Setters = + { + new Setter + { + Property = translateProperty, + Value = forward ? distance : -distance + } + }, + Cue = new Cue(0d) + }, + new KeyFrame { - Property = translateProperty, - Value = 0d + Setters = + { + new Setter + { + Property = translateProperty, + Value = 0d + } + }, + Cue = new Cue(1d) } - ) - { - Cue = new Cue(1d) - }, + } }; animation.Duration = Duration; tasks.Add(animation.RunAsync(to)); diff --git a/src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs new file mode 100644 index 0000000000..4b6703133d --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs @@ -0,0 +1,36 @@ +// Copyright (c) The Avalonia 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; + +namespace Avalonia.Animation +{ + /// + /// Transition class that handles with type. + /// + public class CornerRadiusTransition : Transition + { + /// + public override IObservable DoTransition(IObservable progress, CornerRadius oldValue, CornerRadius newValue) + { + return progress + .Select(p => + { + var f = Easing.Ease(p); + + var deltaTL = newValue.TopLeft - oldValue.TopLeft; + var deltaTR = newValue.TopRight - oldValue.TopRight; + var deltaBR = newValue.BottomRight - oldValue.BottomRight; + var deltaBL = newValue.BottomLeft - oldValue.BottomLeft; + + var nTL = f * deltaTL + oldValue.TopLeft; + var nTR = f * deltaTR + oldValue.TopRight; + var nBR = f * deltaBR + oldValue.BottomRight; + var nBL = f * deltaBL + oldValue.BottomLeft; + + return new CornerRadius(nTL, nTR, nBR, nBL); + }); + } + } +} diff --git a/src/Avalonia.Visuals/Animation/PointTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs similarity index 74% rename from src/Avalonia.Visuals/Animation/PointTransition.cs rename to src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs index a883c517dd..ea0000f960 100644 --- a/src/Avalonia.Visuals/Animation/PointTransition.cs +++ b/src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs @@ -14,16 +14,11 @@ namespace Avalonia.Animation /// public override IObservable DoTransition(IObservable progress, Point oldValue, Point newValue) { - var deltaX = newValue.X - oldValue.Y; - var deltaY = newValue.X - oldValue.Y; - return progress .Select(p => { var f = Easing.Ease(p); - var nX = f * deltaX + oldValue.X; - var nY = f * deltaY + oldValue.Y; - return new Point(nX, nY); + return ((newValue - oldValue) * f) + oldValue; }); } } diff --git a/src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs new file mode 100644 index 0000000000..5de59edc53 --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs @@ -0,0 +1,25 @@ +// Copyright (c) The Avalonia 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; + +namespace Avalonia.Animation +{ + /// + /// Transition class that handles with type. + /// + public class SizeTransition : Transition + { + /// + public override IObservable DoTransition(IObservable progress, Size oldValue, Size newValue) + { + return progress + .Select(p => + { + var f = Easing.Ease(p); + return ((newValue - oldValue) * f) + oldValue; + }); + } + } +} diff --git a/src/Avalonia.Visuals/Animation/ThicknessTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs similarity index 57% rename from src/Avalonia.Visuals/Animation/ThicknessTransition.cs rename to src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs index 6c2ca22b74..e6b5e8904b 100644 --- a/src/Avalonia.Visuals/Animation/ThicknessTransition.cs +++ b/src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs @@ -14,20 +14,11 @@ namespace Avalonia.Animation /// public override IObservable DoTransition(IObservable progress, Thickness oldValue, Thickness newValue) { - var deltaL = newValue.Left - oldValue.Left; - var deltaT = newValue.Top - oldValue.Top; - var deltaR = newValue.Right - oldValue.Right; - var deltaB = newValue.Bottom - oldValue.Bottom; - return progress - .Select(p => + .Select(p => { var f = Easing.Ease(p); - var nL = f * deltaL + oldValue.Left; - var nT = f * deltaT + oldValue.Right; - var nR = f * deltaR + oldValue.Top; - var nB = f * deltaB + oldValue.Bottom; - return new Thickness(nL, nT, nR, nB); + return ((newValue - oldValue) * f) + oldValue; }); } } diff --git a/src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs new file mode 100644 index 0000000000..5271c1378b --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs @@ -0,0 +1,25 @@ +// Copyright (c) The Avalonia 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; + +namespace Avalonia.Animation +{ + /// + /// Transition class that handles with type. + /// + public class VectorTransition : Transition + { + /// + public override IObservable DoTransition(IObservable progress, Vector oldValue, Vector newValue) + { + return progress + .Select(p => + { + var f = Easing.Ease(p); + return ((newValue - oldValue) * f) + oldValue; + }); + } + } +} diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj index 85836a785f..2cc7741bbb 100644 --- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj +++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj @@ -2,7 +2,6 @@ netstandard2.0 Avalonia - false diff --git a/src/Avalonia.Visuals/CornerRadius.cs b/src/Avalonia.Visuals/CornerRadius.cs index 2d41acb5bc..09163a2dac 100644 --- a/src/Avalonia.Visuals/CornerRadius.cs +++ b/src/Avalonia.Visuals/CornerRadius.cs @@ -3,12 +3,18 @@ using System; using System.Globalization; +using Avalonia.Animation.Animators; using Avalonia.Utilities; namespace Avalonia { public struct CornerRadius { + static CornerRadius() + { + Animation.Animation.RegisterAnimator(prop => typeof(CornerRadius).IsAssignableFrom(prop.PropertyType)); + } + public CornerRadius(double uniformRadius) { TopLeft = TopRight = BottomLeft = BottomRight = uniformRadius; diff --git a/src/Avalonia.Visuals/Media/Brush.cs b/src/Avalonia.Visuals/Media/Brush.cs index c2c041f073..f36cd88537 100644 --- a/src/Avalonia.Visuals/Media/Brush.cs +++ b/src/Avalonia.Visuals/Media/Brush.cs @@ -3,6 +3,7 @@ using System; using System.ComponentModel; +using Avalonia.Animation; namespace Avalonia.Media { @@ -10,7 +11,7 @@ namespace Avalonia.Media /// Describes how an area is painted. /// [TypeConverter(typeof(BrushConverter))] - public abstract class Brush : AvaloniaObject, IMutableBrush + public abstract class Brush : Animatable, IMutableBrush { /// /// Defines the property. diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index 8294934751..a37463a0f0 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -3,6 +3,8 @@ using System; using System.Globalization; +using Avalonia.Animation; +using Avalonia.Animation.Animators; namespace Avalonia.Media { @@ -11,6 +13,11 @@ namespace Avalonia.Media /// public readonly struct Color { + static Color() + { + Animation.Animation.RegisterAnimator(prop => typeof(Color).IsAssignableFrom(prop.PropertyType)); + } + /// /// Gets or sets the Alpha component of the color. /// diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index 6b19167381..e7d6df5a93 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -2,23 +2,26 @@ using System; using System.Collections.Generic; using Avalonia.Media.Imaging; using Avalonia.Platform; +using Avalonia.Threading; using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Media { public sealed class DrawingContext : IDisposable { + private readonly bool _ownsImpl; private int _currentLevel; - static readonly Stack> StateStackPool = new Stack>(); - static readonly Stack> TransformStackPool = new Stack>(); + private static ThreadSafeObjectPool> StateStackPool { get; } = + ThreadSafeObjectPool>.Default; - private Stack _states = StateStackPool.Count == 0 ? new Stack() : StateStackPool.Pop(); + private static ThreadSafeObjectPool> TransformStackPool { get; } = + ThreadSafeObjectPool>.Default; - private Stack _transformContainers = TransformStackPool.Count == 0 - ? new Stack() - : TransformStackPool.Pop(); + private Stack _states = StateStackPool.Get(); + + private Stack _transformContainers = TransformStackPool.Get(); readonly struct TransformContainer { @@ -35,6 +38,13 @@ namespace Avalonia.Media public DrawingContext(IDrawingContextImpl impl) { PlatformImpl = impl; + _ownsImpl = true; + } + + public DrawingContext(IDrawingContextImpl impl, bool ownsImpl) + { + _ownsImpl = ownsImpl; + PlatformImpl = impl; } public IDrawingContextImpl PlatformImpl { get; } @@ -299,11 +309,14 @@ namespace Avalonia.Media { while (_states.Count != 0) _states.Peek().Dispose(); - StateStackPool.Push(_states); + StateStackPool.Return(_states); _states = null; - TransformStackPool.Push(_transformContainers); + if (_transformContainers.Count != 0) + throw new InvalidOperationException("Transform container stack is non-empty"); + TransformStackPool.Return(_transformContainers); _transformContainers = null; - PlatformImpl.Dispose(); + if (_ownsImpl) + PlatformImpl.Dispose(); } private static bool PenIsVisible(Pen pen) diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs index 16dba573fd..0ba25e6c0d 100644 --- a/src/Avalonia.Visuals/Media/FontFamily.cs +++ b/src/Avalonia.Visuals/Media/FontFamily.cs @@ -40,9 +40,10 @@ namespace Avalonia.Media /// /// The name of the . /// The source of font resources. - public FontFamily(string name, Uri source) : this(name) + /// + public FontFamily(string name, Uri source, Uri baseUri = null) : this(name) { - Key = new FontFamilyKey(source); + Key = new FontFamilyKey(source, baseUri); } /// @@ -87,11 +88,12 @@ namespace Avalonia.Media /// Parses a string. /// /// The string. + /// /// /// /// Specified family is not supported. /// - public static FontFamily Parse(string s) + public static FontFamily Parse(string s, Uri baseUri = null) { if (string.IsNullOrEmpty(s)) { @@ -112,7 +114,13 @@ namespace Avalonia.Media case 2: { - return new FontFamily(segments[1], new Uri(segments[0], UriKind.RelativeOrAbsolute)); + var uri = segments[0].StartsWith("/") + ? new Uri(segments[0], UriKind.Relative) + : new Uri(segments[0], UriKind.RelativeOrAbsolute); + + return uri.IsAbsoluteUri + ? new FontFamily(segments[1], uri) + : new FontFamily(segments[1], uri, baseUri); } default: diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs index 90ccac0e46..76ee2b7aad 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Linq; namespace Avalonia.Media.Fonts { @@ -12,48 +11,26 @@ namespace Avalonia.Media.Fonts public class FontFamilyKey { /// - /// Creates a new instance of and extracts and from given + /// Creates a new instance of /// /// - public FontFamilyKey(Uri source) + /// + public FontFamilyKey(Uri source, Uri baseUri = null) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } + Source = source ?? throw new ArgumentNullException(nameof(source)); - if (source.AbsolutePath.Contains(".ttf")) - { - var filePathWithoutExtension = source.AbsolutePath.Replace(".ttf", string.Empty); - var fileNameWithoutExtension = filePathWithoutExtension.Split('.').Last(); - FileName = fileNameWithoutExtension + ".ttf"; - Location = new Uri(source.OriginalString.Replace("." + FileName, string.Empty), UriKind.RelativeOrAbsolute); - } - else - { - if (source.AbsolutePath.Contains(".otf")) - { - var filePathWithoutExtension = source.AbsolutePath.Replace(".otf", string.Empty); - var fileNameWithoutExtension = filePathWithoutExtension.Split('.').Last(); - FileName = fileNameWithoutExtension + ".otf"; - Location = new Uri(source.OriginalString.Replace("." + FileName, string.Empty), UriKind.RelativeOrAbsolute); - } - else - { - Location = source; - } - } + BaseUri = baseUri; } /// - /// Location of stored font asset that belongs to a + /// Source of stored font asset that belongs to a /// - public Uri Location { get; } + public Uri Source { get; } /// - /// Optional filename for a font asset that belongs to a + /// A base URI to use if is relative /// - public string FileName { get; } + public Uri BaseUri { get; } /// /// Returns a hash code for this instance. @@ -67,14 +44,14 @@ namespace Avalonia.Media.Fonts { var hash = (int)2166136261; - if (Location != null) + if (Source != null) { - hash = (hash * 16777619) ^ Location.GetHashCode(); + hash = (hash * 16777619) ^ Source.GetHashCode(); } - if (FileName != null) + if (BaseUri != null) { - hash = (hash * 16777619) ^ FileName.GetHashCode(); + hash = (hash * 16777619) ^ BaseUri.GetHashCode(); } return hash; @@ -95,12 +72,12 @@ namespace Avalonia.Media.Fonts return false; } - if (Location != other.Location) + if (Source != other.Source) { return false; } - if (FileName != other.FileName) + if (BaseUri != other.BaseUri) { return false; } @@ -116,16 +93,12 @@ namespace Avalonia.Media.Fonts /// public override string ToString() { - if (FileName == null) + if (!Source.IsAbsoluteUri && BaseUri != null) { - return Location.PathAndQuery; + return BaseUri.Authority + Source; } - var builder = new UriBuilder(Location); - - builder.Path += "." + FileName; - - return builder.ToString(); + return Source.ToString(); } } } diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs b/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs index b6b83e2097..063fe8f20d 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using Avalonia.Platform; namespace Avalonia.Media.Fonts @@ -18,55 +17,110 @@ namespace Avalonia.Media.Fonts s_assetLoader = AvaloniaLocator.Current.GetService(); } + /// + /// Loads all font assets that belong to the specified + /// + /// + /// public static IEnumerable LoadFontAssets(FontFamilyKey fontFamilyKey) { - return fontFamilyKey.FileName != null - ? GetFontAssetsByFileName(fontFamilyKey.Location, fontFamilyKey.FileName) - : GetFontAssetsByLocation(fontFamilyKey.Location); + var sourceWithoutArguments = fontFamilyKey.Source.OriginalString.Split('?').First(); + + if (sourceWithoutArguments.EndsWith(".ttf") + || sourceWithoutArguments.EndsWith(".otf")) + { + return GetFontAssetsByExpression(fontFamilyKey); + } + + return GetFontAssetsBySource(fontFamilyKey); } /// /// Searches for font assets at a given location and returns a quantity of found assets /// - /// + /// /// - private static IEnumerable GetFontAssetsByLocation(Uri location) + private static IEnumerable GetFontAssetsBySource(FontFamilyKey fontFamilyKey) { - var availableAssets = s_assetLoader.GetAssets(location); + var availableAssets = s_assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri); - var matchingAssets = availableAssets.Where(x => x.absolutePath.EndsWith(".ttf") || x.absolutePath.EndsWith(".otf")); + var matchingAssets = + availableAssets.Where(x => x.AbsolutePath.EndsWith(".ttf") || x.AbsolutePath.EndsWith(".otf")); - return matchingAssets.Select(x => GetAssetUri(x.absolutePath, x.assembly)); + return matchingAssets; } /// /// Searches for font assets at a given location and only accepts assets that fit to a given filename expression. /// File names can target multiple files with * wildcard. For example "FontFile*.ttf" /// - /// - /// + /// /// - private static IEnumerable GetFontAssetsByFileName(Uri location, string fileName) + private static IEnumerable GetFontAssetsByExpression(FontFamilyKey fontFamilyKey) { - var availableResources = s_assetLoader.GetAssets(location); + var fileName = GetFileName(fontFamilyKey, out var fileExtension, out var location); - var compareTo = location.AbsolutePath + "." + fileName.Split('*').First(); + var availableResources = s_assetLoader.GetAssets(location, fontFamilyKey.BaseUri); - var matchingResources = - availableResources.Where(x => x.absolutePath.Contains(compareTo) && (x.absolutePath.EndsWith(".ttf") || x.absolutePath.EndsWith(".otf"))); + string compareTo; - return matchingResources.Select(x => GetAssetUri(x.absolutePath, x.assembly)); + if (fontFamilyKey.Source.IsAbsoluteUri) + { + if (fontFamilyKey.Source.Scheme == "resm") + { + compareTo = location.AbsolutePath + "." + fileName.Split('*').First(); + } + else + { + compareTo = location.AbsolutePath + fileName.Split('*').First(); + } + } + else + { + compareTo = location.AbsolutePath + fileName.Split('*').First(); + } + + var matchingResources = availableResources.Where( + x => x.AbsolutePath.Contains(compareTo) + && x.AbsolutePath.EndsWith(fileExtension)); + + return matchingResources; } - /// - /// Returns a for a font asset that follows the resm scheme - /// - /// - /// - /// - private static Uri GetAssetUri(string absolutePath, Assembly assembly) + private static string GetFileName(FontFamilyKey fontFamilyKey, out string fileExtension, out Uri location) { - return new Uri("resm:" + absolutePath + "?assembly=" + assembly.GetName().Name, UriKind.RelativeOrAbsolute); + if (fontFamilyKey.Source.IsAbsoluteUri && fontFamilyKey.Source.Scheme == "resm") + { + fileExtension = "." + fontFamilyKey.Source.AbsolutePath.Split('.').LastOrDefault(); + + var fileName = fontFamilyKey.Source.LocalPath.Replace(fileExtension, string.Empty).Split('.').LastOrDefault(); + + location = new Uri(fontFamilyKey.Source.AbsoluteUri.Replace("." + fileName + fileExtension, string.Empty), UriKind.RelativeOrAbsolute); + + return fileName; + } + + var pathSegments = fontFamilyKey.Source.OriginalString.Split('/'); + + var fileNameWithExtension = pathSegments.Last(); + + var fileNameSegments = fileNameWithExtension.Split('.'); + + fileExtension = "." + fileNameSegments.Last(); + + if (fontFamilyKey.BaseUri != null) + { + var relativePath = fontFamilyKey.Source.OriginalString + .Replace(fileNameWithExtension, string.Empty); + + location = new Uri(fontFamilyKey.BaseUri, relativePath); + } + else + { + location = new Uri(fontFamilyKey.Source.AbsolutePath.Replace(fileNameWithExtension, string.Empty)); + } + + return fileNameSegments.First(); } } } diff --git a/src/Avalonia.Visuals/Media/PixelSize.cs b/src/Avalonia.Visuals/Media/PixelSize.cs index 3d0ae0b351..6785b51716 100644 --- a/src/Avalonia.Visuals/Media/PixelSize.cs +++ b/src/Avalonia.Visuals/Media/PixelSize.cs @@ -159,8 +159,8 @@ namespace Avalonia /// The dots per inch. /// The device-independent size. public static PixelSize FromSize(Size size, Vector dpi) => new PixelSize( - (int)(size.Width * (dpi.X / 96)), - (int)(size.Height * (dpi.Y / 96))); + (int)Math.Ceiling(size.Width * (dpi.X / 96)), + (int)Math.Ceiling(size.Height * (dpi.Y / 96))); /// /// Returns the string representation of the size. diff --git a/src/Avalonia.Visuals/Media/SolidColorBrush.cs b/src/Avalonia.Visuals/Media/SolidColorBrush.cs index 32b87df56b..bacb9e4ad7 100644 --- a/src/Avalonia.Visuals/Media/SolidColorBrush.cs +++ b/src/Avalonia.Visuals/Media/SolidColorBrush.cs @@ -1,6 +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 Avalonia.Animation; +using Avalonia.Animation.Animators; using Avalonia.Media.Immutable; namespace Avalonia.Media @@ -18,6 +20,7 @@ namespace Avalonia.Media static SolidColorBrush() { + Animation.Animation.RegisterAnimator(prop => typeof(IBrush).IsAssignableFrom(prop.PropertyType)); AffectsRender(ColorProperty); } diff --git a/src/Avalonia.Visuals/Media/Transform.cs b/src/Avalonia.Visuals/Media/Transform.cs index 677514763c..7a70657ce0 100644 --- a/src/Avalonia.Visuals/Media/Transform.cs +++ b/src/Avalonia.Visuals/Media/Transform.cs @@ -3,6 +3,7 @@ using System; using Avalonia.Animation; +using Avalonia.Animation.Animators; using Avalonia.VisualTree; namespace Avalonia.Media diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs index e74094fe50..0d3e354615 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Globalization; +using Avalonia.Animation.Animators; using Avalonia.Utilities; namespace Avalonia @@ -11,6 +12,11 @@ namespace Avalonia /// public readonly struct Point { + static Point() + { + Animation.Animation.RegisterAnimator(prop => typeof(Point).IsAssignableFrom(prop.PropertyType)); + } + /// /// The X position. /// @@ -133,7 +139,7 @@ namespace Avalonia /// Point to multiply /// Factor /// Points having its coordinates multiplied - public static Point operator *(Point p, double k) => new Point(p.X*k, p.Y*k); + public static Point operator *(Point p, double k) => new Point(p.X * k, p.Y * k); /// /// Multiplies a point by a factor coordinate-wise @@ -141,7 +147,7 @@ namespace Avalonia /// Point to multiply /// Factor /// Points having its coordinates multiplied - public static Point operator *(double k, Point p) => new Point(p.X*k, p.Y*k); + public static Point operator *(double k, Point p) => new Point(p.X * k, p.Y * k); /// /// Divides a point by a factor coordinate-wise @@ -149,8 +155,8 @@ namespace Avalonia /// Point to divide by /// Factor /// Points having its coordinates divided - public static Point operator /(Point p, double k) => new Point(p.X/k, p.Y/k); - + public static Point operator /(Point p, double k) => new Point(p.X / k, p.Y / k); + /// /// Applies a matrix to a point. /// diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs index 63d34b474b..530a47729f 100644 --- a/src/Avalonia.Visuals/Rect.cs +++ b/src/Avalonia.Visuals/Rect.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; +using Avalonia.Animation.Animators; using Avalonia.Utilities; namespace Avalonia @@ -12,6 +13,11 @@ namespace Avalonia /// public readonly struct Rect { + static Rect() + { + Animation.Animation.RegisterAnimator(prop => typeof(Rect).IsAssignableFrom(prop.PropertyType)); + } + /// /// An empty rectangle. /// diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index a268eff78a..b48efaa34e 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -37,6 +37,7 @@ namespace Avalonia.Rendering private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects(); private IRef _currentDraw; private readonly IDeferredRendererLock _lock; + private readonly object _sceneLock = new object(); /// /// Initializes a new instance of the class. @@ -84,6 +85,7 @@ namespace Avalonia.Rendering RenderTarget = renderTarget; _sceneBuilder = sceneBuilder ?? new SceneBuilder(); Layers = new RenderLayers(); + _lock = new ManagedDeferredRendererLock(); } /// @@ -118,8 +120,13 @@ namespace Avalonia.Rendering /// public void Dispose() { - var scene = Interlocked.Exchange(ref _scene, null); - scene?.Dispose(); + lock (_sceneLock) + { + var scene = _scene; + _scene = null; + scene?.Dispose(); + } + Stop(); Layers.Clear(); @@ -134,7 +141,8 @@ namespace Avalonia.Rendering // When unit testing the renderLoop may be null, so update the scene manually. UpdateScene(); } - + //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(); } @@ -172,7 +180,8 @@ namespace Avalonia.Rendering } } - bool IRenderLoopTask.NeedsUpdate => _dirty == null || _dirty.Count > 0; + bool NeedsUpdate => _dirty == null || _dirty.Count > 0; + bool IRenderLoopTask.NeedsUpdate => NeedsUpdate; void IRenderLoopTask.Update(TimeSpan time) => UpdateScene(); @@ -197,79 +206,105 @@ namespace Avalonia.Rendering internal void UnitTestUpdateScene() => UpdateScene(); - internal void UnitTestRender() => Render(_scene.Item, false); + internal void UnitTestRender() => Render(false); private void Render(bool forceComposite) { using (var l = _lock.TryLock()) - if (l != null) - using (var scene = _scene?.Clone()) - { - Render(scene?.Item, forceComposite); - } - } - - private void Render(Scene scene, bool forceComposite) - { - bool renderOverlay = DrawDirtyRects || DrawFps; - bool composite = false; - - if (RenderTarget == null) { - RenderTarget = ((IRenderRoot)_root).CreateRenderTarget(); - } + if (l == null) + return; - if (renderOverlay) - { - _dirtyRectsDisplay.Tick(); - } - - try - { - if (scene != null && scene.Size != Size.Empty) + IDrawingContextImpl context = null; + try { - IDrawingContextImpl context = null; - - if (scene.Generation != _lastSceneId) + try { - context = RenderTarget.CreateDrawingContext(this); - Layers.Update(scene, context); + IDrawingContextImpl GetContext() + { + if (context != null) + return context; + if (RenderTarget == null) + RenderTarget = ((IRenderRoot)_root).CreateRenderTarget(); + return context = RenderTarget.CreateDrawingContext(this); - RenderToLayers(scene); + } - if (DebugFramesPath != null) + var (scene, updated) = UpdateRenderLayersAndConsumeSceneIfNeeded(GetContext); + using (scene) { - SaveDebugFrames(scene.Generation); + var overlay = DrawDirtyRects || DrawFps; + if (DrawDirtyRects) + _dirtyRectsDisplay.Tick(); + if (overlay) + RenderOverlay(scene.Item, GetContext()); + if (updated || forceComposite || overlay) + RenderComposite(scene.Item, GetContext()); } + } + finally + { + context?.Dispose(); + } + } + catch (RenderTargetCorruptedException ex) + { + Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex); + RenderTarget?.Dispose(); + RenderTarget = null; + } + } + } - _lastSceneId = scene.Generation; + private (IRef scene, bool updated) UpdateRenderLayersAndConsumeSceneIfNeeded(Func contextFactory, + bool recursiveCall = false) + { + IRef sceneRef; + lock (_sceneLock) + sceneRef = _scene?.Clone(); + if (sceneRef == null) + return (null, false); + using (sceneRef) + { + var scene = sceneRef.Item; + if (scene.Generation != _lastSceneId) + { + var context = contextFactory(); + Layers.Update(scene, context); - composite = true; - } + RenderToLayers(scene); - if (renderOverlay) + if (DebugFramesPath != null) { - context = context ?? RenderTarget.CreateDrawingContext(this); - RenderOverlay(scene, context); - RenderComposite(scene, context); + SaveDebugFrames(scene.Generation); } - else if (composite || forceComposite) + + lock (_sceneLock) + _lastSceneId = scene.Generation; + + + // 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) { - context = context ?? RenderTarget.CreateDrawingContext(this); - RenderComposite(scene, context); + UpdateScene(); + var (rs, _) = UpdateRenderLayersAndConsumeSceneIfNeeded(contextFactory, true); + return (rs, true); } - - context?.Dispose(); + + // Indicate that we have updated the layers + return (sceneRef.Clone(), true); } + + // Just return scene, layers weren't updated + return (sceneRef.Clone(), false); } - catch (RenderTargetCorruptedException ex) - { - Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex); - RenderTarget?.Dispose(); - RenderTarget = null; - } + } + private void Render(IDrawingContextImpl context, VisualNode node, IVisual layer, Rect clipBounds) { if (layer == null || node.LayerRoot == layer) @@ -405,6 +440,11 @@ namespace Avalonia.Rendering private void UpdateScene() { Dispatcher.UIThread.VerifyAccess(); + lock (_sceneLock) + { + if (_scene?.Item.Generation > _lastSceneId) + return; + } if (_root.IsVisible) { var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root)); @@ -423,15 +463,23 @@ namespace Avalonia.Rendering } } - var oldScene = Interlocked.Exchange(ref _scene, sceneRef); - oldScene?.Dispose(); + lock (_sceneLock) + { + var oldScene = _scene; + _scene = sceneRef; + oldScene?.Dispose(); + } _dirty.Clear(); } else { - var oldScene = Interlocked.Exchange(ref _scene, null); - oldScene?.Dispose(); + lock (_sceneLock) + { + var oldScene = _scene; + _scene = null; + oldScene?.Dispose(); + } } } diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index 08f3803e9b..03ac004fd5 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -266,7 +266,14 @@ namespace Avalonia.Rendering if (clipToBounds) { - clipRect = clipRect.Intersect(new Rect(visual.Bounds.Size)); + if (visual.RenderTransform != null) + { + clipRect = new Rect(visual.Bounds.Size); + } + else + { + clipRect = clipRect.Intersect(new Rect(visual.Bounds.Size)); + } } using (context.PushPostTransform(m)) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 8000d413ea..159c3cd0fa 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -220,6 +220,7 @@ namespace Avalonia.Rendering.SceneGraph Transform = Transform, ClipBounds = ClipBounds, ClipToBounds = ClipToBounds, + LayoutBounds = LayoutBounds, GeometryClip = GeometryClip, _opacity = Opacity, OpacityMask = OpacityMask, diff --git a/src/Avalonia.Visuals/Rendering/Utilities/TileBrushCalculator.cs b/src/Avalonia.Visuals/Rendering/Utilities/TileBrushCalculator.cs index 52a8fb5ab7..27949fcf55 100644 --- a/src/Avalonia.Visuals/Rendering/Utilities/TileBrushCalculator.cs +++ b/src/Avalonia.Visuals/Rendering/Utilities/TileBrushCalculator.cs @@ -116,8 +116,8 @@ namespace Avalonia.Rendering.Utilities return true; if (SourceRect.Size.AspectRatio == _imageSize.AspectRatio) return false; - if ((int)SourceRect.Width != _imageSize.Width || - (int)SourceRect.Height != _imageSize.Height) + if (SourceRect.Width != _imageSize.Width || + SourceRect.Height != _imageSize.Height) return true; return false; } diff --git a/src/Avalonia.Visuals/Size.cs b/src/Avalonia.Visuals/Size.cs index 8057571bf1..782c5ea67b 100644 --- a/src/Avalonia.Visuals/Size.cs +++ b/src/Avalonia.Visuals/Size.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; +using Avalonia.Animation.Animators; using Avalonia.Utilities; namespace Avalonia @@ -12,6 +13,11 @@ namespace Avalonia /// public readonly struct Size { + static Size() + { + Animation.Animation.RegisterAnimator(prop => typeof(Size).IsAssignableFrom(prop.PropertyType)); + } + /// /// A size representing infinity. /// diff --git a/src/Avalonia.Visuals/Thickness.cs b/src/Avalonia.Visuals/Thickness.cs index 3e70ecafb1..830ee4666e 100644 --- a/src/Avalonia.Visuals/Thickness.cs +++ b/src/Avalonia.Visuals/Thickness.cs @@ -3,6 +3,8 @@ using System; using System.Globalization; +using Avalonia.Animation; +using Avalonia.Animation.Animators; using Avalonia.Utilities; namespace Avalonia @@ -12,6 +14,11 @@ namespace Avalonia /// public readonly struct Thickness { + static Thickness() + { + Animation.Animation.RegisterAnimator(prop => typeof(Thickness).IsAssignableFrom(prop.PropertyType)); + } + /// /// The thickness on the left. /// @@ -134,6 +141,36 @@ namespace Avalonia a.Bottom + b.Bottom); } + /// + /// Subtracts two Thicknesses. + /// + /// The first thickness. + /// The second thickness. + /// The equality. + public static Thickness operator -(Thickness a, Thickness b) + { + return new Thickness( + a.Left - b.Left, + a.Top - b.Top, + a.Right - b.Right, + a.Bottom - b.Bottom); + } + + /// + /// Multiplies a Thickness to a scalar. + /// + /// The thickness. + /// The scalar. + /// The equality. + public static Thickness operator *(Thickness a, double b) + { + return new Thickness( + a.Left * b, + a.Top * b, + a.Right * b, + a.Bottom * b); + } + /// /// Adds a Thickness to a Size. /// @@ -169,7 +206,7 @@ namespace Avalonia { using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Thickness")) { - if(tokenizer.TryReadDouble(out var a)) + if (tokenizer.TryReadDouble(out var a)) { if (tokenizer.TryReadDouble(out var b)) { diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index c2db17cd86..2f1690184d 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; +using Avalonia.Animation.Animators; using JetBrains.Annotations; namespace Avalonia @@ -12,6 +13,11 @@ namespace Avalonia /// public readonly struct Vector { + static Vector() + { + Animation.Animation.RegisterAnimator(prop => typeof(Vector).IsAssignableFrom(prop.PropertyType)); + } + /// /// The X vector. /// @@ -60,7 +66,7 @@ namespace Avalonia /// The dot product public static double operator *(Vector a, Vector b) { - return a.X*b.X + a.Y*b.Y; + return a.X * b.X + a.Y * b.Y; } /// @@ -88,7 +94,7 @@ namespace Avalonia /// /// Length of the vector /// - public double Length => Math.Sqrt(X*X + Y*Y); + public double Length => Math.Sqrt(X * X + Y * Y); /// /// Negates a vector. diff --git a/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs b/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs index 8444e509fd..965973d3cb 100644 --- a/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs +++ b/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs @@ -50,7 +50,7 @@ namespace Avalonia.Gtk3 { Resolver.Custom = options.CustomResolver; UseDeferredRendering = EnvOption("USE_DEFERRED_RENDERING", true, options.UseDeferredRendering); - var useGpu = EnvOption("USE_GPU", false, options.UseGpuAcceleration); + var useGpu = EnvOption("USE_GPU", true, options.UseGpuAcceleration); if (!s_gtkInitialized) { try diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 04400e3714..0b5e25fa9f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -4,11 +4,12 @@ PCL;NETSTANDARD;NETSTANDARD2_0;HAS_TYPE_CONVERTER;HAS_CUSTOM_ATTRIBUTE_PROVIDER False false - false CS1591 + + @@ -18,6 +19,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs index 13eb3f1ae3..fa9d364fc0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs @@ -11,6 +11,8 @@ using Avalonia.Controls.Templates; namespace Avalonia.Markup.Xaml { + using Avalonia.Media; + /// /// Maintains a repository of s for XAML parsing on top of those /// maintained by . @@ -37,7 +39,9 @@ namespace Avalonia.Markup.Xaml { typeof(Selector), typeof(SelectorTypeConverter) }, { typeof(TimeSpan), typeof(TimeSpanTypeConverter) }, { typeof(WindowIcon), typeof(IconTypeConverter) }, - { typeof(CultureInfo), typeof(CultureInfoConverter)} + { typeof(CultureInfo), typeof(CultureInfoConverter) }, + { typeof(Uri), typeof(AvaloniaUriTypeConverter) }, + { typeof(FontFamily), typeof(FontFamilyTypeConverter) } }; /// @@ -83,4 +87,4 @@ namespace Avalonia.Markup.Xaml /// The converter type. Maybe be a non-constructed generic type. public static void Register(Type type, Type converterType) => _converters[type] = converterType; } -} \ No newline at end of file +} diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs index c89be32057..a825deeae3 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs @@ -10,6 +10,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Reflection; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Json; using System.Text; namespace Avalonia.Markup.Xaml @@ -85,7 +87,7 @@ namespace Avalonia.Markup.Xaml "Could not create IAssetLoader : maybe Application.RegisterServices() wasn't called?"); } - foreach (var uri in GetUrisFor(type)) + foreach (var uri in GetUrisFor(assetLocator, type)) { if (assetLocator.Exists(uri)) { @@ -134,18 +136,14 @@ namespace Avalonia.Markup.Xaml var asset = assetLocator.OpenAndGetAssembly(uri, baseUri); using (var stream = asset.stream) { + var absoluteUri = uri.IsAbsoluteUri ? uri : new Uri(baseUri, uri); try { - return Load(stream, asset.assembly, rootInstance, uri); + return Load(stream, asset.assembly, rootInstance, absoluteUri); } catch (Exception e) { - var uriString = uri.ToString(); - if (!uri.IsAbsoluteUri) - { - uriString = new Uri(baseUri, uri).AbsoluteUri; - } - throw new XamlLoadException("Error loading xaml at " + uriString + ": " + e.Message, e); + throw new XamlLoadException("Error loading xaml at " + absoluteUri + ": " + e.Message, e); } } } @@ -221,15 +219,37 @@ namespace Avalonia.Markup.Xaml return LoadFromReader(reader, null); } + + private static readonly DataContractSerializer s_xamlInfoSerializer = + new DataContractSerializer(typeof(AvaloniaResourceXamlInfo)); /// /// Gets the URI for a type. /// + /// /// The type. /// The URI. - private static IEnumerable GetUrisFor(Type type) + private static IEnumerable GetUrisFor(IAssetLoader assetLocator, Type type) { var asm = type.GetTypeInfo().Assembly.GetName().Name; + var xamlInfoUri = new Uri($"avares://{asm}/!AvaloniaResourceXamlInfo"); var typeName = type.FullName; + if (typeName == null) + throw new ArgumentException("Type doesn't have a FullName"); + + if (assetLocator.Exists(xamlInfoUri)) + { + using (var xamlInfoStream = assetLocator.Open(xamlInfoUri)) + { + var xamlInfo = (AvaloniaResourceXamlInfo)s_xamlInfoSerializer.ReadObject(xamlInfoStream); + if (xamlInfo.ClassToResourcePathIndex.TryGetValue(typeName, out var rv) == true) + { + yield return new Uri($"avares://{asm}{rv}"); + yield break; + } + } + } + + yield return new Uri("resm:" + typeName + ".xaml?assembly=" + asm); yield return new Uri("resm:" + typeName + ".paml?assembly=" + asm); } diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaUriTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaUriTypeConverter.cs new file mode 100644 index 0000000000..6f6a0b8563 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaUriTypeConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +namespace Avalonia.Markup.Xaml.Converters +{ + public class AvaloniaUriTypeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + var s = value as string; + if (s == null) + return null; + //On Unix Uri tries to interpret paths starting with "/" as file Uris + if (s.StartsWith("/")) + return new Uri(s, UriKind.Relative); + return new Uri(s); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/BitmapTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/BitmapTypeConverter.cs index 4679c6c61a..ad6bfa9059 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/BitmapTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/BitmapTypeConverter.cs @@ -20,18 +20,16 @@ namespace Avalonia.Markup.Xaml.Converters public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { - var uri = new Uri((string)value, UriKind.RelativeOrAbsolute); - var scheme = uri.IsAbsoluteUri ? uri.Scheme : "file"; + var s = (string)value; + var uri = s.StartsWith("/") + ? new Uri(s, UriKind.Relative) + : new Uri(s, UriKind.RelativeOrAbsolute); - switch (scheme) - { - case "file": - return new Bitmap((string)value); + if(uri.IsAbsoluteUri && uri.IsFile) + return new Bitmap(uri.LocalPath); - default: - var assets = AvaloniaLocator.Current.GetService(); - return new Bitmap(assets.Open(uri, context.GetBaseUri())); - } + var assets = AvaloniaLocator.Current.GetService(); + return new Bitmap(assets.Open(uri, context.GetBaseUri())); } } } \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/FontFamilyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/FontFamilyTypeConverter.cs new file mode 100644 index 0000000000..9b0e7a2b6b --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/FontFamilyTypeConverter.cs @@ -0,0 +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.ComponentModel; +using System.Globalization; + +using Avalonia.Media; + +using Portable.Xaml.ComponentModel; + +namespace Avalonia.Markup.Xaml.Converters +{ + public class FontFamilyTypeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + var s = (string)value; + + return FontFamily.Parse(s, context.GetBaseUri()); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/IconTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/IconTypeConverter.cs index 1af22a5147..b984e948e4 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/IconTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/IconTypeConverter.cs @@ -36,20 +36,16 @@ namespace Avalonia.Markup.Xaml.Converters throw new NotSupportedException(); } - private WindowIcon CreateIconFromPath(ITypeDescriptorContext context, string path) + private WindowIcon CreateIconFromPath(ITypeDescriptorContext context, string s) { - var uri = new Uri(path, UriKind.RelativeOrAbsolute); - var scheme = uri.IsAbsoluteUri ? uri.Scheme : "file"; - - switch (scheme) - { - case "file": - return new WindowIcon(path); - - default: - var assets = AvaloniaLocator.Current.GetService(); - return new WindowIcon(assets.Open(uri, context.GetBaseUri())); - } + var uri = s.StartsWith("/") + ? new Uri(s, UriKind.Relative) + : new Uri(s, UriKind.RelativeOrAbsolute); + + if(uri.IsAbsoluteUri && uri.IsFile) + return new WindowIcon(uri.LocalPath); + var assets = AvaloniaLocator.Current.GetService(); + return new WindowIcon(assets.Open(uri, context.GetBaseUri())); } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/RelativeSourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/RelativeSourceExtension.cs index ab82b7a28b..2f7256fa22 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/RelativeSourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/RelativeSourceExtension.cs @@ -1,13 +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 Avalonia.Data; +using Portable.Xaml.Markup; namespace Avalonia.Markup.Xaml.MarkupExtensions { - using Portable.Xaml.Markup; - using System; - public class RelativeSourceExtension : MarkupExtension { public RelativeSourceExtension() @@ -24,10 +23,19 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions return new RelativeSource { Mode = Mode, + AncestorType = AncestorType, + AncestorLevel = AncestorLevel, + Tree = Tree, }; } [ConstructorArgument("mode")] - public RelativeSourceMode Mode { get; set; } + public RelativeSourceMode Mode { get; set; } = RelativeSourceMode.FindAncestor; + + public Type AncestorType { get; set; } + + public TreeType Tree { get; set; } + + public int AncestorLevel { get; set; } = 1; } } \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaResourceXamlInfo.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaResourceXamlInfo.cs new file mode 100644 index 0000000000..7e5df47104 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaResourceXamlInfo.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Avalonia.Markup.Xaml.PortableXaml +{ + [DataContract] + class AvaloniaResourceXamlInfo + { + [DataMember] + public Dictionary ClassToResourcePathIndex { get; set; } = new Dictionary(); + } +} diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index a5cb5a8c06..673eb51901 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -2,7 +2,6 @@ netstandard2.0 Avalonia - false diff --git a/src/Shared/PlatformSupport/AssetLoader.cs b/src/Shared/PlatformSupport/AssetLoader.cs index 3392baee0a..22d702967b 100644 --- a/src/Shared/PlatformSupport/AssetLoader.cs +++ b/src/Shared/PlatformSupport/AssetLoader.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Reflection; using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Shared.PlatformSupport { @@ -15,23 +16,24 @@ namespace Avalonia.Shared.PlatformSupport /// public class AssetLoader : IAssetLoader { + private const string AvaloniaResourceName = "!AvaloniaResources"; private static readonly Dictionary AssemblyNameCache = new Dictionary(); - private AssemblyDescriptor _defaultAssembly; + private AssemblyDescriptor _defaultResmAssembly; /// /// Initializes a new instance of the class. /// /// - /// The default assembly from which to load assets for which no assembly is specified. + /// The default assembly from which to load resm: assets for which no assembly is specified. /// public AssetLoader(Assembly assembly = null) { if (assembly == null) assembly = Assembly.GetEntryAssembly(); if (assembly != null) - _defaultAssembly = new AssemblyDescriptor(assembly); + _defaultResmAssembly = new AssemblyDescriptor(assembly); } /// @@ -40,7 +42,7 @@ namespace Avalonia.Shared.PlatformSupport /// The default assembly. public void SetDefaultAssembly(Assembly assembly) { - _defaultAssembly = new AssemblyDescriptor(assembly); + _defaultResmAssembly = new AssemblyDescriptor(assembly); } /// @@ -99,21 +101,59 @@ namespace Avalonia.Shared.PlatformSupport /// Gets all assets of a folder and subfolders that match specified uri. /// /// The URI. + /// Base URI that is used if is relative. /// All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset - public IEnumerable<(string absolutePath, Assembly assembly)> GetAssets(Uri uri) + public IEnumerable GetAssets(Uri uri, Uri baseUri) { - var assembly = GetAssembly(uri); + if (uri.IsAbsoluteUri && uri.Scheme == "resm") + { + var assembly = GetAssembly(uri); + + return assembly?.Resources.Where(x => x.Key.Contains(uri.AbsolutePath)) + .Select(x =>new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ?? + Enumerable.Empty(); + } - return assembly?.Resources.Where(x => x.Key.Contains(uri.AbsolutePath)) - .Select(x => (x.Key, x.Value.Assembly)) ?? - Enumerable.Empty<(string AbsolutePath, Assembly Assembly)>(); + uri = EnsureAbsolute(uri, baseUri); + if (uri.Scheme == "avares") + { + var (asm, path) = GetResAsmAndPath(uri); + if (asm == null) + { + throw new ArgumentException( + "No default assembly, entry assembly or explicit assembly specified; " + + "don't know where to look up for the resource, try specifying assembly explicitly."); + } + + if (asm?.AvaloniaResources == null) + return Enumerable.Empty(); + path = path.TrimEnd('/') + '/'; + return asm.AvaloniaResources.Where(r => r.Key.StartsWith(path)) + .Select(x => new Uri($"avares://{asm.Name}{x.Key}")); + } + + return Enumerable.Empty(); } - private IAssetDescriptor GetAsset(Uri uri, Uri baseUri) + private Uri EnsureAbsolute(Uri uri, Uri baseUri) { - if (!uri.IsAbsoluteUri || uri.Scheme == "resm") + if (uri.IsAbsoluteUri) + return uri; + if(baseUri == null) + throw new ArgumentException($"Relative uri {uri} without base url"); + if (!baseUri.IsAbsoluteUri) + throw new ArgumentException($"Base uri {baseUri} is relative"); + if (baseUri.Scheme == "resm") + throw new ArgumentException( + $"Relative uris for 'resm' scheme aren't supported; {baseUri} uses resm"); + return new Uri(baseUri, uri); + } + + private IAssetDescriptor GetAsset(Uri uri, Uri baseUri) + { + if (uri.IsAbsoluteUri && uri.Scheme == "resm") { - var asm = GetAssembly(uri) ?? GetAssembly(baseUri) ?? _defaultAssembly; + var asm = GetAssembly(uri) ?? GetAssembly(baseUri) ?? _defaultResmAssembly; if (asm == null) { @@ -128,19 +168,45 @@ namespace Avalonia.Shared.PlatformSupport asm.Resources.TryGetValue(resourceKey, out rv); return rv; } - throw new ArgumentException($"Invalid uri, see https://github.com/AvaloniaUI/Avalonia/issues/282#issuecomment-166982104", nameof(uri)); + + uri = EnsureAbsolute(uri, baseUri); + + if (uri.Scheme == "avares") + { + var (asm, path) = GetResAsmAndPath(uri); + if (asm.AvaloniaResources == null) + return null; + asm.AvaloniaResources.TryGetValue(path, out var desc); + return desc; + } + + throw new ArgumentException($"Unsupported url type: " + uri.Scheme, nameof(uri)); } + private (AssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri) + { + var asm = GetAssembly(uri.Authority); + return (asm, uri.AbsolutePath); + } + private AssemblyDescriptor GetAssembly(Uri uri) { if (uri != null) { - var qs = ParseQueryString(uri); - string assemblyName; + if (!uri.IsAbsoluteUri) + return null; + if (uri.Scheme == "avares") + return GetResAsmAndPath(uri).asm; - if (qs.TryGetValue("assembly", out assemblyName)) + if (uri.Scheme == "resm") { - return GetAssembly(assemblyName); + var qs = ParseQueryString(uri); + string assemblyName; + + if (qs.TryGetValue("assembly", out assemblyName)) + { + return GetAssembly(assemblyName); + } } } @@ -150,9 +216,7 @@ namespace Avalonia.Shared.PlatformSupport private AssemblyDescriptor GetAssembly(string name) { if (name == null) - { - return _defaultAssembly; - } + throw new ArgumentNullException(nameof(name)); AssemblyDescriptor rv; if (!AssemblyNameCache.TryGetValue(name, out rv)) @@ -167,9 +231,7 @@ namespace Avalonia.Shared.PlatformSupport { // iOS does not support loading assemblies dynamically! // -#if NETCOREAPP1_0 - AssemblyNameCache[name] = rv = new AssemblyDescriptor(Assembly.Load(new AssemblyName(name))); -#elif __IOS__ +#if __IOS__ throw new InvalidOperationException( $"Assembly {name} needs to be referenced and explicitly loaded before loading resources"); #else @@ -213,6 +275,80 @@ namespace Avalonia.Shared.PlatformSupport public Assembly Assembly => _asm; } + + private class AvaloniaResourceDescriptor : IAssetDescriptor + { + private readonly int _offset; + private readonly int _length; + public Assembly Assembly { get; } + + public AvaloniaResourceDescriptor(Assembly asm, int offset, int length) + { + _offset = offset; + _length = length; + Assembly = asm; + } + + public Stream GetStream() + { + return new SlicedStream(Assembly.GetManifestResourceStream(AvaloniaResourceName), _offset, _length); + } + } + + class SlicedStream : Stream + { + private readonly Stream _baseStream; + private readonly int _from; + + public SlicedStream(Stream baseStream, int from, int length) + { + Length = length; + _baseStream = baseStream; + _from = from; + _baseStream.Position = from; + } + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _baseStream.Read(buffer, offset, (int)Math.Min(count, Length - Position)); + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + Position = offset; + if (origin == SeekOrigin.End) + Position = _from + Length + offset; + if (origin == SeekOrigin.Current) + Position = Position + offset; + return Position; + } + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + public override bool CanRead => true; + public override bool CanSeek => _baseStream.CanRead; + public override bool CanWrite => false; + public override long Length { get; } + public override long Position + { + get => _baseStream.Position - _from; + set => _baseStream.Position = value + _from; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + _baseStream.Dispose(); + } + + public override void Close() => _baseStream.Close(); + } private class AssemblyDescriptor { @@ -225,12 +361,37 @@ namespace Avalonia.Shared.PlatformSupport Resources = assembly.GetManifestResourceNames() .ToDictionary(n => n, n => (IAssetDescriptor)new AssemblyResourceDescriptor(assembly, n)); Name = assembly.GetName().Name; + using (var resources = assembly.GetManifestResourceStream(AvaloniaResourceName)) + { + if (resources != null) + { + Resources.Remove(AvaloniaResourceName); + + var indexLength = new BinaryReader(resources).ReadInt32(); + var index = AvaloniaResourcesIndexReaderWriter.Read(new SlicedStream(resources, 4, indexLength)); + var baseOffset = indexLength + 4; + AvaloniaResources = index.ToDictionary(r => "/" + r.Path.TrimStart('/'), r => (IAssetDescriptor) + new AvaloniaResourceDescriptor(assembly, baseOffset + r.Offset, r.Size)); + } + } } } public Assembly Assembly { get; } public Dictionary Resources { get; } + public Dictionary AvaloniaResources { get; } public string Name { get; } } + + public static void RegisterResUriParsers() + { + if (!UriParser.IsKnownScheme("avares")) + UriParser.Register(new GenericUriParser( + GenericUriParserOptions.GenericAuthority | + GenericUriParserOptions.NoUserInfo | + GenericUriParserOptions.NoPort | + GenericUriParserOptions.NoQuery | + GenericUriParserOptions.NoFragment), "avares", -1); + } } } diff --git a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs index 186c55d9eb..8a5a725594 100644 --- a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs +++ b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs @@ -89,9 +89,11 @@ namespace Avalonia.Shared.PlatformSupport #if DEBUG if (Thread.CurrentThread.ManagedThreadId == GCThread?.ManagedThreadId) { - Console.Error.WriteLine("Native blob disposal from finalizer thread\nBacktrace: " - + Environment.StackTrace - + "\n\nBlob created by " + _backtrace); + lock(_lock) + if (!IsDisposed) + Console.Error.WriteLine("Native blob disposal from finalizer thread\nBacktrace: " + + Environment.StackTrace + + "\n\nBlob created by " + _backtrace); } #endif DoDispose(); diff --git a/src/Shared/PlatformSupport/StandardRuntimePlatformServices.cs b/src/Shared/PlatformSupport/StandardRuntimePlatformServices.cs index b34b8f7f5f..0dd7aea356 100644 --- a/src/Shared/PlatformSupport/StandardRuntimePlatformServices.cs +++ b/src/Shared/PlatformSupport/StandardRuntimePlatformServices.cs @@ -10,6 +10,7 @@ namespace Avalonia.Shared.PlatformSupport public static void Register(Assembly assembly = null) { var standardPlatform = new StandardRuntimePlatform(); + AssetLoader.RegisterResUriParsers(); AvaloniaLocator.CurrentMutable .Bind().ToConstant(standardPlatform) .Bind().ToConstant(new AssetLoader(assembly)) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 1bfd7fd90d..076d69ae4a 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -225,10 +225,7 @@ namespace Avalonia.Skia /// public IRenderTargetBitmapImpl CreateLayer(Size size) { - var normalizedDpi = new Vector(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y); - var pixelSize = size * normalizedDpi; - - return CreateRenderTarget((int) pixelSize.Width, (int) pixelSize.Height, _dpi); + return CreateRenderTarget(size); } /// @@ -387,26 +384,27 @@ namespace Avalonia.Skia /// Target size. /// Tile brush to use. /// Tile brush image. - /// The bitmap interpolation mode. private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Size targetSize, ITileBrush tileBrush, IDrawableBitmapImpl tileBrushImage) { - var calc = new TileBrushCalculator(tileBrush, - new Size(tileBrushImage.PixelSize.Width, tileBrushImage.PixelSize.Height), targetSize); - - var intermediate = CreateRenderTarget( - (int)calc.IntermediateSize.Width, - (int)calc.IntermediateSize.Height, _dpi); + var calc = new TileBrushCalculator(tileBrush, tileBrushImage.PixelSize.ToSize(_dpi), targetSize); + var intermediate = CreateRenderTarget(calc.IntermediateSize); paintWrapper.AddDisposable(intermediate); using (var context = intermediate.CreateDrawingContext(null)) { - var rect = new Rect(0, 0, tileBrushImage.PixelSize.Width, tileBrushImage.PixelSize.Height); + var sourceRect = new Rect(tileBrushImage.PixelSize.ToSize(96)); + var targetRect = new Rect(tileBrushImage.PixelSize.ToSize(_dpi)); context.Clear(Colors.Transparent); context.PushClip(calc.IntermediateClip); context.Transform = calc.IntermediateTransform; - context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect, tileBrush.BitmapInterpolationMode); + context.DrawImage( + RefCountable.CreateUnownedNotClonable(tileBrushImage), + 1, + sourceRect, + targetRect, + tileBrush.BitmapInterpolationMode); context.PopClip(); } @@ -433,7 +431,14 @@ namespace Avalonia.Skia var image = intermediate.SnapshotImage(); paintWrapper.AddDisposable(image); - using (var shader = image.ToShader(tileX, tileY, tileTransform)) + var paintTransform = default(SKMatrix); + + SKMatrix.Concat( + ref paintTransform, + tileTransform, + SKMatrix.MakeScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y))); + + using (var shader = image.ToShader(tileX, tileY, paintTransform)) { paintWrapper.Paint.Shader = shader; } @@ -457,7 +462,7 @@ namespace Avalonia.Skia if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) { - var intermediate = CreateRenderTarget((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi); + var intermediate = CreateRenderTarget(intermediateSize); using (var ctx = intermediate.CreateDrawingContext(visualBrushRenderer)) { @@ -609,18 +614,17 @@ namespace Avalonia.Skia /// /// Create new render target compatible with this drawing context. /// - /// Width. - /// Height. - /// Drawing dpi. + /// The size of the render target in DIPs. /// Pixel format. /// - private SurfaceRenderTarget CreateRenderTarget(int width, int height, Vector dpi, PixelFormat? format = null) + private SurfaceRenderTarget CreateRenderTarget(Size size, PixelFormat? format = null) { + var pixelSize = PixelSize.FromSize(size, _dpi); var createInfo = new SurfaceRenderTarget.CreateInfo { - Width = width, - Height = height, - Dpi = dpi, + Width = pixelSize.Width, + Height = pixelSize.Height, + Dpi = _dpi, Format = format, DisableTextLcdRendering = !_canTextUseLcdRendering, GrContext = _grContext diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index 5efbc0861e..02b6188fb6 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -42,7 +42,8 @@ namespace Avalonia.Skia { var framebuffer = _platformSurface.Lock(); var framebufferImageInfo = new SKImageInfo(framebuffer.Size.Width, framebuffer.Size.Height, - framebuffer.Format.ToSkColorType(), SKAlphaType.Premul); + framebuffer.Format.ToSkColorType(), + framebuffer.Format == PixelFormat.Rgb565 ? SKAlphaType.Opaque : SKAlphaType.Premul); CreateSurface(framebufferImageInfo, framebuffer); diff --git a/src/Skia/Avalonia.Skia/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/GlRenderTarget.cs index a6269473a6..81c70e2be7 100644 --- a/src/Skia/Avalonia.Skia/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/GlRenderTarget.cs @@ -31,24 +31,19 @@ namespace Avalonia.Skia var size = session.Size; var scaling = session.Scaling; - GRBackendRenderTargetDesc desc = new GRBackendRenderTargetDesc - { - Width = size.Width, - Height = size.Height, - SampleCount = disp.SampleCount, - StencilBits = disp.StencilSize, - Config = GRPixelConfig.Rgba8888, - Origin=GRSurfaceOrigin.BottomLeft, - RenderTargetHandle = new IntPtr(fb) - }; - - gl.Viewport(0, 0, desc.Width, desc.Height); + gl.Viewport(0, 0, size.Width, size.Height); gl.ClearStencil(0); gl.ClearColor(0, 0, 0, 0); gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + _grContext.ResetContext(); - var surface = SKSurface.Create(_grContext, desc); - + GRBackendRenderTarget 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, + GRPixelConfig.Rgba8888.ToColorType()); + var nfo = new DrawingContextImpl.CreateInfo { GrContext = _grContext, @@ -62,6 +57,7 @@ namespace Avalonia.Skia { surface.Canvas.Flush(); surface.Dispose(); + renderTarget.Dispose(); session.Dispose(); })); } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 687fe7feca..17c51dbb6e 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -1,72 +1,115 @@ +// Copyright (c) The 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 SkiaSharp; namespace Avalonia.Skia { internal class SKTypefaceCollection { - private readonly ConcurrentDictionary _cachedTypefaces = - new ConcurrentDictionary(); + private readonly ConcurrentDictionary> _fontFamilies = + new ConcurrentDictionary>(); public void AddTypeFace(SKTypeface typeface) { - var key = new FontKey(typeface.FamilyName, (SKFontStyleWeight)typeface.FontWeight, typeface.FontSlant); + var key = new FontKey((SKFontStyleWeight)typeface.FontWeight, typeface.FontSlant); - _cachedTypefaces.TryAdd(key, typeface); + if (!_fontFamilies.TryGetValue(typeface.FamilyName, out var fontFamily)) + { + fontFamily = new ConcurrentDictionary(); + + _fontFamilies.TryAdd(typeface.FamilyName, fontFamily); + } + + fontFamily.TryAdd(key, typeface); } public SKTypeface GetTypeFace(Typeface typeface) { - SKFontStyleSlant skStyle = SKFontStyleSlant.Upright; + var styleSlant = SKFontStyleSlant.Upright; switch (typeface.Style) { case FontStyle.Italic: - skStyle = SKFontStyleSlant.Italic; + styleSlant = SKFontStyleSlant.Italic; break; case FontStyle.Oblique: - skStyle = SKFontStyleSlant.Oblique; + styleSlant = SKFontStyleSlant.Oblique; break; } - var key = new FontKey(typeface.FontFamily.Name, (SKFontStyleWeight)typeface.Weight, skStyle); + if (!_fontFamilies.TryGetValue(typeface.FontFamily.Name, out var fontFamily)) + { + return TypefaceCache.Default; + } + + 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(); + + if (!keys.Any()) + { + keys = fontFamily.Keys.Where( + x => x.Weight == key.Weight && (x.Slant >= key.Slant || x.Slant < key.Slant)).ToArray(); + + if (!keys.Any()) + { + keys = fontFamily.Keys.Where( + x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && + (x.Slant >= key.Slant || x.Slant < key.Slant)).ToArray(); + } + } + + key = keys.FirstOrDefault(); + + fontFamily.TryGetValue(key, out var typeface); - return _cachedTypefaces.TryGetValue(key, out var skTypeface) ? skTypeface : TypefaceCache.Default; + return typeface; } private struct FontKey { - public readonly string Name; public readonly SKFontStyleSlant Slant; public readonly SKFontStyleWeight Weight; - public FontKey(string name, SKFontStyleWeight weight, SKFontStyleSlant slant) + public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant) { - Name = name; Slant = slant; Weight = weight; } public override int GetHashCode() { - int hash = 17; - hash = hash * 31 + Name.GetHashCode(); - hash = hash * 31 + (int)Slant; - hash = hash * 31 + (int)Weight; + var 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; + return other is FontKey key && this.Equals(key); } private bool Equals(FontKey other) { - return Name == other.Name && Slant == other.Slant && + return Slant == other.Slant && Weight == other.Weight; } } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index b9a8c9b98b..ab8ee85a54 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -1,3 +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.Collections.Concurrent; using Avalonia.Media; using Avalonia.Media.Fonts; diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index 6955cc666f..dcd4b36d56 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -137,7 +137,7 @@ namespace Avalonia.Skia { var colorType = PixelFormatHelper.ResolveColorType(format); - return new SKImageInfo(width, height, colorType, SKAlphaType.Premul); + return new SKImageInfo(Math.Max(width, 1), Math.Max(height, 1), colorType, SKAlphaType.Premul); } /// diff --git a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs index 36ccd7930a..eb3a11b2a1 100644 --- a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs @@ -41,7 +41,7 @@ namespace Avalonia.Skia var nfo = new SKImageInfo(size.Width, size.Height, colorType, SKAlphaType.Premul); var blob = runtimePlatform.AllocBlob(nfo.BytesSize); - _bitmap.InstallPixels(nfo, blob.Address, nfo.RowBytes, null, s_releaseDelegate, blob); + _bitmap.InstallPixels(nfo, blob.Address, nfo.RowBytes, s_releaseDelegate, blob); } else { diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index efe26ba01a..aaca1a3b00 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -434,10 +434,16 @@ namespace Avalonia.Direct2D1.Media if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) { + // We need to ensure the size we're requesting is an integer pixel size, otherwise + // D2D alters the DPI of the render target, which messes stuff up. PixelSize.FromSize + // will do the rounding for us. + var dpi = new Vector(_deviceContext.DotsPerInch.Width, _deviceContext.DotsPerInch.Height); + var pixelSize = PixelSize.FromSize(intermediateSize, dpi); + using (var intermediate = new BitmapRenderTarget( _deviceContext, CompatibleRenderTargetOptions.None, - intermediateSize.ToSharpDX())) + pixelSize.ToSize(dpi).ToSharpDX())) { using (var ctx = new RenderTarget(intermediate).CreateDrawingContext(_visualBrushRenderer)) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs index 55877cefcb..19d320b783 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs @@ -20,7 +20,8 @@ namespace Avalonia.Direct2D1.Media BitmapImpl bitmap, Size targetSize) { - var calc = new TileBrushCalculator(brush, bitmap.PixelSize.ToSize(96), targetSize); + var dpi = new Vector(target.DotsPerInch.Width, target.DotsPerInch.Height); + var calc = new TileBrushCalculator(brush, bitmap.PixelSize.ToSize(dpi), targetSize); if (!calc.NeedsIntermediate) { @@ -99,7 +100,8 @@ namespace Avalonia.Direct2D1.Media using (var context = new RenderTarget(result).CreateDrawingContext(null)) { - var rect = new Rect(0, 0, bitmap.PixelSize.Width, bitmap.PixelSize.Height); + var dpi = new Vector(target.DotsPerInch.Width, target.DotsPerInch.Height); + var rect = new Rect(bitmap.PixelSize.ToSize(dpi)); context.Clear(Colors.Transparent); context.PushClip(calc.IntermediateClip); diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs index 72367ac50f..fbe7f840d7 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs @@ -1,8 +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.IO; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Utilities; using SharpDX; using SharpDX.Direct2D1; using D2DBitmap = SharpDX.Direct2D1.Bitmap; @@ -49,5 +51,22 @@ namespace Avalonia.Direct2D1.Media.Imaging { return new OptionalDispose(_renderTarget.Bitmap, false); } + + public override void Save(Stream stream) + { + using (var wic = new WicRenderTargetBitmapImpl(PixelSize, Dpi)) + { + using (var dc = wic.CreateDrawingContext(null)) + { + dc.DrawImage( + RefCountable.CreateUnownedNotClonable(this), + 1, + new Rect(PixelSize.ToSize(Dpi.X)), + new Rect(PixelSize.ToSize(Dpi.X))); + } + + wic.Save(stream); + } + } } } diff --git a/src/Windows/Avalonia.Direct2D1/Utils/DebugUtils.cs b/src/Windows/Avalonia.Direct2D1/Utils/DebugUtils.cs new file mode 100644 index 0000000000..9ce8f15cf4 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Utils/DebugUtils.cs @@ -0,0 +1,13 @@ +using Avalonia.Direct2D1.Media.Imaging; + +namespace Avalonia.Direct2D1.Utils +{ + internal static class DebugUtils + { + public static void Save(SharpDX.Direct2D1.BitmapRenderTarget bitmap, string filename) + { + var rtb = new D2DRenderTargetBitmapImpl(bitmap); + rtb.Save(filename); + } + } +} diff --git a/src/Windows/Avalonia.Win32/FramebufferManager.cs b/src/Windows/Avalonia.Win32/FramebufferManager.cs index c910703181..87c5a1bb02 100644 --- a/src/Windows/Avalonia.Win32/FramebufferManager.cs +++ b/src/Windows/Avalonia.Win32/FramebufferManager.cs @@ -5,7 +5,7 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32 { - class FramebufferManager : IFramebufferPlatformSurface, IDisposable + class FramebufferManager : IFramebufferPlatformSurface { private readonly IntPtr _hwnd; private WindowFramebuffer _fb; @@ -29,10 +29,5 @@ namespace Avalonia.Win32 } return _fb; } - - public void Dispose() - { - _fb?.Deallocate(); - } } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 9e8ff20e44..7d6e8fc8ce 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -909,8 +909,17 @@ namespace Avalonia.Win32.Interop public enum ClassLongIndex : int { - GCL_HCURSOR = -12, - GCL_HICON = -14 + GCLP_MENUNAME = -8, + GCLP_HBRBACKGROUND = -10, + GCLP_HCURSOR = -12, + GCLP_HICON = -14, + GCLP_HMODULE = -16, + GCL_CBWNDEXTRA = -18, + GCL_CBCLSEXTRA = -20, + GCLP_WNDPROC = -24, + GCL_STYLE = -26, + GCLP_HICONSM = -34, + GCW_ATOM = -32 } [DllImport("user32.dll", EntryPoint = "SetClassLongPtr")] @@ -929,6 +938,20 @@ namespace Avalonia.Win32.Interop return SetClassLong64(hWnd, nIndex, dwNewLong); } + public static IntPtr GetClassLongPtr(IntPtr hWnd, int nIndex) + { + if (IntPtr.Size > 4) + return GetClassLongPtr64(hWnd, nIndex); + else + return new IntPtr(GetClassLongPtr32(hWnd, nIndex)); + } + + [DllImport("user32.dll", EntryPoint = "GetClassLong")] + public static extern uint GetClassLongPtr32(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll", EntryPoint = "GetClassLongPtr")] + public static extern IntPtr GetClassLongPtr64(IntPtr hWnd, int nIndex); + [DllImport("user32.dll", EntryPoint = "SetCursor")] internal static extern IntPtr SetCursor(IntPtr hCursor); @@ -1172,6 +1195,8 @@ namespace Avalonia.Win32.Interop public int right; public int bottom; + public int Width => right - left; + public int Height => bottom - top; public RECT(Rect rect) { left = (int)rect.X; @@ -1179,6 +1204,34 @@ namespace Avalonia.Win32.Interop right = (int)(rect.X + rect.Width); bottom = (int)(rect.Y + rect.Height); } + + public void Offset(POINT pt) + { + left += pt.X; + right += pt.X; + top += pt.Y; + bottom += pt.Y; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct WINDOWPOS + { + public IntPtr hwnd; + public IntPtr hwndInsertAfter; + public int x; + public int y; + public int cx; + public int cy; + public uint flags; + } + + [StructLayout(LayoutKind.Sequential)] + public struct NCCALCSIZE_PARAMS + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + public RECT[] rgrc; + public WINDOWPOS lppos; } public struct TRACKMOUSEEVENT diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index 7849bd6a9d..39f1a95466 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -24,7 +24,7 @@ namespace Avalonia.Win32 UnmanagedMethods.WindowStyles.WS_EX_TOOLWINDOW | UnmanagedMethods.WindowStyles.WS_EX_TOPMOST; - return UnmanagedMethods.CreateWindowEx( + var result = UnmanagedMethods.CreateWindowEx( (int)exStyle, atom, null, @@ -37,6 +37,14 @@ namespace Avalonia.Win32 IntPtr.Zero, IntPtr.Zero, 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)); + + return result; } protected override IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 4c08e985cd..6174bc360f 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -13,6 +13,7 @@ using Avalonia.Input.Raw; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Threading; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; @@ -34,6 +35,7 @@ namespace Avalonia.Win32 private bool _decorated = true; private bool _resizable = true; private bool _topmost = false; + private bool _taskbarIcon = true; private double _scaling = 1; private WindowState _showWindowState; private WindowState _lastWindowState; @@ -93,17 +95,25 @@ namespace Avalonia.Win32 { get { - var style = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE); - var exStyle = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_EXSTYLE); - var padding = new UnmanagedMethods.RECT(); - - if (UnmanagedMethods.AdjustWindowRectEx(ref padding, style, false, exStyle)) + if (_decorated) { - return new Thickness(-padding.left, -padding.top, padding.right, padding.bottom); + var style = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE); + var exStyle = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_EXSTYLE); + + var padding = new RECT(); + + if (UnmanagedMethods.AdjustWindowRectEx(ref padding, style, false, exStyle)) + { + return new Thickness(-padding.left, -padding.top, padding.right, padding.bottom); + } + else + { + throw new Win32Exception(); + } } else { - throw new Win32Exception(); + return new Thickness(); } } } @@ -146,8 +156,7 @@ namespace Avalonia.Win32 if (value != ClientSize) { value *= Scaling; - value += BorderThickness; - + UnmanagedMethods.SetWindowPos( _hwnd, IntPtr.Zero, @@ -234,8 +243,6 @@ namespace Avalonia.Win32 public void Dispose() { - _framebuffer?.Dispose(); - _framebuffer = null; if (_hwnd != IntPtr.Zero) { UnmanagedMethods.DestroyWindow(_hwnd); @@ -266,63 +273,7 @@ namespace Avalonia.Win32 return; } - var style = (UnmanagedMethods.WindowStyles)UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE); - - style |= UnmanagedMethods.WindowStyles.WS_OVERLAPPEDWINDOW; - - if (!value) - { - style ^= UnmanagedMethods.WindowStyles.WS_OVERLAPPEDWINDOW; - } - - UnmanagedMethods.RECT windowRect; - - UnmanagedMethods.GetWindowRect(_hwnd, out windowRect); - - Rect newRect; - var oldThickness = BorderThickness; - - UnmanagedMethods.SetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE, (uint)style); - - if (value) - { - var thickness = BorderThickness; - - newRect = new Rect( - windowRect.left - thickness.Left, - windowRect.top - thickness.Top, - (windowRect.right - windowRect.left) + (thickness.Left + thickness.Right), - (windowRect.bottom - windowRect.top) + (thickness.Top + thickness.Bottom)); - } - else - { - newRect = new Rect( - windowRect.left + oldThickness.Left, - windowRect.top + oldThickness.Top, - (windowRect.right - windowRect.left) - (oldThickness.Left + oldThickness.Right), - (windowRect.bottom - windowRect.top) - (oldThickness.Top + oldThickness.Bottom)); - } - - UnmanagedMethods.SetWindowPos(_hwnd, IntPtr.Zero, (int)newRect.X, (int)newRect.Y, (int)newRect.Width, - (int)newRect.Height, - UnmanagedMethods.SetWindowPosFlags.SWP_NOZORDER | UnmanagedMethods.SetWindowPosFlags.SWP_NOACTIVATE); - - _decorated = value; - - if(_decorated) - { - if (_resizable) - { - // If we switch decorations back on we need to restore WS_SizeFrame. - _resizable = false; - CanResize(true); - } - else - { - _resizable = true; - CanResize(false); - } - } + UpdateWMStyles(()=> _decorated = value); } public void Invalidate(Rect rect) @@ -330,10 +281,10 @@ namespace Avalonia.Win32 var f = Scaling; var r = new UnmanagedMethods.RECT { - left = (int)(rect.X * f), - top = (int)(rect.Y * f), - right = (int)(rect.Right * f), - bottom = (int)(rect.Bottom * f), + 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), }; UnmanagedMethods.InvalidateRect(_hwnd, ref r, false); @@ -433,7 +384,7 @@ namespace Avalonia.Win32 public void SetCursor(IPlatformHandle cursor) { var hCursor = cursor?.Handle ?? DefaultCursor; - UnmanagedMethods.SetClassLong(_hwnd, UnmanagedMethods.ClassLongIndex.GCL_HCURSOR, hCursor); + UnmanagedMethods.SetClassLong(_hwnd, UnmanagedMethods.ClassLongIndex.GCLP_HCURSOR, hCursor); if (_owner.IsPointerOver) UnmanagedMethods.SetCursor(hCursor); @@ -487,6 +438,13 @@ namespace Avalonia.Win32 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) @@ -647,6 +605,20 @@ namespace Avalonia.Win32 new Point(0, 0), GetMouseModifiers(wParam)); 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: UnmanagedMethods.PAINTSTRUCT ps; if (UnmanagedMethods.BeginPaint(_hwnd, out ps) != IntPtr.Zero) @@ -689,7 +661,7 @@ namespace Avalonia.Win32 MINMAXINFO mmi = Marshal.PtrToStructure(lParam); - if (_minSize.Width > 0) + if (_minSize.Width > 0) mmi.ptMinTrackSize.X = (int)((_minSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right); if (_minSize.Height > 0) @@ -860,7 +832,7 @@ namespace Avalonia.Win32 { MONITORINFO monitorInfo = MONITORINFO.Create(); - if (GetMonitorInfo(monitor,ref monitorInfo)) + if (GetMonitorInfo(monitor, ref monitorInfo)) { RECT rcMonitorArea = monitorInfo.rcMonitor; @@ -889,13 +861,22 @@ namespace Avalonia.Win32 return (int)(ptr.ToInt64() & 0xffffffff); } + public void ShowTaskbarIcon(bool value) { + if (_taskbarIcon == value) + { + return; + } + + _taskbarIcon = value; + var style = (UnmanagedMethods.WindowStyles)UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_EXSTYLE); style &= ~(UnmanagedMethods.WindowStyles.WS_VISIBLE); style |= UnmanagedMethods.WindowStyles.WS_EX_TOOLWINDOW; + if (value) style |= UnmanagedMethods.WindowStyles.WS_EX_APPWINDOW; else @@ -911,26 +892,68 @@ namespace Avalonia.Win32 } } - public void CanResize(bool value) + private void UpdateWMStyles(Action change) { - if (value == _resizable) + var oldDecorated = _decorated; + + var oldThickness = BorderThickness; + + change(); + + var style = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE); + + const WindowStyles controlledFlags = WindowStyles.WS_OVERLAPPEDWINDOW; + + style = style | controlledFlags ^ controlledFlags; + + style |= WindowStyles.WS_OVERLAPPEDWINDOW; + + if (!_decorated) { - return; + style ^= (WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU); } - if (_decorated) + if (!_resizable) { - var style = (UnmanagedMethods.WindowStyles)UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE); + style ^= (WindowStyles.WS_SIZEFRAME); + } - if (value) - style |= UnmanagedMethods.WindowStyles.WS_SIZEFRAME; - else - style &= ~(UnmanagedMethods.WindowStyles.WS_SIZEFRAME); + 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) + { + 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; + } - UnmanagedMethods.SetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE, (uint)style); + 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); + } + + public void CanResize(bool value) + { + if (value == _resizable) + { + return; } - _resizable = value; + UpdateWMStyles(()=> _resizable = value); } public void SetTopmost(bool value) diff --git a/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs b/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs new file mode 100644 index 0000000000..d89b8469df --- /dev/null +++ b/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs @@ -0,0 +1,77 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Styling; +using Avalonia.UnitTests; +using Avalonia.Data; +using Xunit; + +namespace Avalonia.Animation.UnitTests +{ + public class AnimationIterationTests + { + [Fact] + public void Check_Initial_Inter_and_Trailing_Delay_Values() + { + var keyframe1 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 200d), + }, + Cue = new Cue(1d) + }; + + var keyframe2 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 100d), + }, + Cue = new Cue(0d) + }; + + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(3), + Delay = TimeSpan.FromSeconds(3), + DelayBetweenIterations = TimeSpan.FromSeconds(3), + IterationCount = new IterationCount(2), + 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); + + // Initial Delay. + clock.Step(TimeSpan.FromSeconds(1)); + Assert.Equal(border.Width, 0d); + + clock.Step(TimeSpan.FromSeconds(6)); + + // First Inter-Iteration delay. + clock.Step(TimeSpan.FromSeconds(8)); + Assert.Equal(border.Width, 200d); + + // Trailing Delay should be non-existent. + clock.Step(TimeSpan.FromSeconds(14)); + Assert.True(animationRun.Status == TaskStatus.RanToCompletion); + Assert.Equal(border.Width, 100d); + } + } +} diff --git a/tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj b/tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj new file mode 100644 index 0000000000..1b2ba3c7de --- /dev/null +++ b/tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj @@ -0,0 +1,25 @@ + + + netcoreapp2.0 + Library + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Avalonia.Animation.UnitTests/Properties/AssemblyInfo.cs b/tests/Avalonia.Animation.UnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..b00c205f10 --- /dev/null +++ b/tests/Avalonia.Animation.UnitTests/Properties/AssemblyInfo.cs @@ -0,0 +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.Reflection; +using Xunit; + +[assembly: AssemblyTitle("Avalonia.Animation.UnitTests")] + +// Don't run tests in parallel. +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/tests/Avalonia.Animation.UnitTests/TestClock.cs b/tests/Avalonia.Animation.UnitTests/TestClock.cs new file mode 100644 index 0000000000..a1c4ff9277 --- /dev/null +++ b/tests/Avalonia.Animation.UnitTests/TestClock.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.Animation.UnitTests +{ + internal class TestClock : IClock, IDisposable + { + private IObserver _observer; + + public PlayState PlayState { get; set; } = PlayState.Run; + + public void Dispose() + { + _observer?.OnCompleted(); + } + + public void Step(TimeSpan time) + { + _observer?.OnNext(time); + } + + public IDisposable Subscribe(IObserver observer) + { + _observer = observer; + return this; + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs b/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs index d5f9818f89..13c946b549 100644 --- a/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs @@ -1,6 +1,4 @@ -using Avalonia.Controls.Presenters; using Avalonia.Controls.Shapes; -using Avalonia.Controls.Templates; using Avalonia.Media; using Xunit; @@ -311,20 +309,10 @@ namespace Avalonia.Controls.UnitTests { var lt = new LayoutTransformControl() { - LayoutTransform = transform, - Template = new FuncControlTemplate( - p => new ContentPresenter() { Content = p.Content }) + LayoutTransform = transform }; - lt.Content = new Rectangle() { Width = width, Height = height }; - - lt.ApplyTemplate(); - - //we need to force create visual child - //so the measure after is correct - (lt.Presenter as ContentPresenter).UpdateChild(); - - Assert.NotNull(lt.Presenter?.Child); + lt.Child = new Rectangle() { Width = width, Height = height }; lt.Measure(Size.Infinity); lt.Arrange(new Rect(lt.DesiredSize)); @@ -332,4 +320,4 @@ namespace Avalonia.Controls.UnitTests return lt; } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 322c14c6bd..a5c3881d37 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -22,7 +22,7 @@ namespace Avalonia.Controls.UnitTests TabItem selected; var target = new TabControl { - Template = new FuncControlTemplate(CreateTabControlTemplate), + Template = TabControlTemplate(), Items = new[] { (selected = new TabItem @@ -61,7 +61,7 @@ namespace Avalonia.Controls.UnitTests var target = new TabControl { - Template = new FuncControlTemplate(CreateTabControlTemplate), + Template = TabControlTemplate(), Items = items, }; @@ -94,7 +94,7 @@ namespace Avalonia.Controls.UnitTests var target = new TabControl { - Template = new FuncControlTemplate(CreateTabControlTemplate), + Template = TabControlTemplate(), Items = collection, }; @@ -147,7 +147,7 @@ namespace Avalonia.Controls.UnitTests }, Child = new TabControl { - Template = new FuncControlTemplate(CreateTabControlTemplate), + Template = TabControlTemplate(), Items = collection, } }; @@ -172,7 +172,7 @@ namespace Avalonia.Controls.UnitTests var target = new TabControl { - Template = new FuncControlTemplate(CreateTabControlTemplate), + Template = TabControlTemplate(), DataContext = "Base", DataTemplates = { @@ -182,41 +182,39 @@ namespace Avalonia.Controls.UnitTests }; ApplyTemplate(target); - var carousel = (Carousel)target.Pages; - var container = (ContentPresenter)carousel.Presenter.Panel.Children.Single(); - container.UpdateChild(); - var dataContext = ((TextBlock)container.Child).DataContext; + target.ContentPart.UpdateChild(); + var dataContext = ((TextBlock)target.ContentPart.Child).DataContext; Assert.Equal(items[0], dataContext); target.SelectedIndex = 1; - container = (ContentPresenter)carousel.Presenter.Panel.Children.Single(); - container.UpdateChild(); - dataContext = ((Button)container.Child).DataContext; + target.ContentPart.UpdateChild(); + dataContext = ((Button)target.ContentPart.Child).DataContext; Assert.Equal(items[1], dataContext); target.SelectedIndex = 2; - dataContext = ((TextBlock)carousel.Presenter.Panel.Children.Single()).DataContext; + target.ContentPart.UpdateChild(); + dataContext = ((TextBlock)target.ContentPart.Child).DataContext; Assert.Equal("Base", dataContext); target.SelectedIndex = 3; - container = (ContentPresenter)carousel.Presenter.Panel.Children[0]; - container.UpdateChild(); - dataContext = ((TextBlock)container.Child).DataContext; + target.ContentPart.UpdateChild(); + dataContext = ((TextBlock)target.ContentPart.Child).DataContext; Assert.Equal("Qux", dataContext); target.SelectedIndex = 4; - dataContext = ((TextBlock)carousel.Presenter.Panel.Children.Single()).DataContext; + target.ContentPart.UpdateChild(); + dataContext = target.ContentPart.DataContext; Assert.Equal("Base", dataContext); } /// - /// Non-headered control items should result in TabStripItems with empty content. + /// Non-headered control items should result in TabItems with empty header. /// /// - /// If a TabStrip is created with non IHeadered controls as its items, don't try to - /// display the control in the TabStripItem: if the TabStrip is part of a TabControl - /// then *that* will also try to display the control, resulting in dual-parentage + /// If a TabControl is created with non IHeadered controls as its items, don't try to + /// display the control in the header: if the control is part of the header then + /// *that* control would also end up in the content region, resulting in dual-parentage /// breakage. /// [Fact] @@ -230,18 +228,20 @@ namespace Avalonia.Controls.UnitTests var target = new TabControl { - Template = new FuncControlTemplate(CreateTabControlTemplate), + Template = TabControlTemplate(), Items = items, }; ApplyTemplate(target); - var result = target.TabStrip.GetLogicalChildren() - .OfType() - .Select(x => x.Content) + var logicalChildren = target.ItemsPresenterPart.Panel.GetLogicalChildren(); + + var result = logicalChildren + .OfType() + .Select(x => x.Header) .ToList(); - Assert.Equal(new object[] { string.Empty, string.Empty }, result); + Assert.Equal(new object[] { null, null }, result); } [Fact] @@ -249,7 +249,7 @@ namespace Avalonia.Controls.UnitTests { TabControl target = new TabControl { - Template = new FuncControlTemplate(CreateTabControlTemplate), + Template = TabControlTemplate(), Items = new[] { new TabItem { Header = "Foo" }, @@ -262,70 +262,61 @@ namespace Avalonia.Controls.UnitTests target.SelectedIndex = 2; - var carousel = (Carousel)target.Pages; - var page = (TabItem)carousel.SelectedItem; + var page = (TabItem)target.SelectedItem; Assert.Null(page.Content); } - private Control CreateTabControlTemplate(TabControl parent) + private IControlTemplate TabControlTemplate() { - return new StackPanel - { - Children = - { - new TabStrip - { - Name = "PART_TabStrip", - Template = new FuncControlTemplate(CreateTabStripTemplate), - MemberSelector = TabControl.HeaderSelector, - [!TabStrip.ItemsProperty] = parent[!TabControl.ItemsProperty], - [!!TabStrip.SelectedIndexProperty] = parent[!!TabControl.SelectedIndexProperty] - }, - new Carousel - { - Name = "PART_Content", - Template = new FuncControlTemplate(CreateCarouselTemplate), - MemberSelector = TabControl.ContentSelector, - [!Carousel.ItemsProperty] = parent[!TabControl.ItemsProperty], - [!Carousel.SelectedItemProperty] = parent[!TabControl.SelectedItemProperty], - } - } - }; - } + return new FuncControlTemplate(parent => - private Control CreateTabStripTemplate(TabStrip parent) - { - return new ItemsPresenter - { - Name = "PART_ItemsPresenter", - [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], - [!CarouselPresenter.MemberSelectorProperty] = parent[!ItemsControl.MemberSelectorProperty], - }; + new StackPanel + { + Children = { + new ItemsPresenter + { + Name = "PART_ItemsPresenter", + [!TabStrip.ItemsProperty] = parent[!TabControl.ItemsProperty], + [!TabStrip.ItemTemplateProperty] = parent[!TabControl.ItemTemplateProperty], + }, + new ContentPresenter + { + Name = "PART_Content", + [!ContentPresenter.ContentProperty] = parent[!TabControl.SelectedContentProperty], + [!ContentPresenter.ContentTemplateProperty] = parent[!TabControl.SelectedContentTemplateProperty], + } + } + }); } - private Control CreateCarouselTemplate(Carousel control) + private IControlTemplate TabItemTemplate() { - return new CarouselPresenter - { - Name = "PART_ItemsPresenter", - [!CarouselPresenter.ItemsProperty] = control[!ItemsControl.ItemsProperty], - [!CarouselPresenter.ItemsPanelProperty] = control[!ItemsControl.ItemsPanelProperty], - [!CarouselPresenter.MemberSelectorProperty] = control[!ItemsControl.MemberSelectorProperty], - [!CarouselPresenter.SelectedIndexProperty] = control[!SelectingItemsControl.SelectedIndexProperty], - [~CarouselPresenter.PageTransitionProperty] = control[~Carousel.PageTransitionProperty], - }; + return new FuncControlTemplate(parent => + new ContentPresenter + { + Name = "PART_ContentPresenter", + [!ContentPresenter.ContentProperty] = parent[!TabItem.HeaderProperty], + [!ContentPresenter.ContentTemplateProperty] = parent[!TabItem.HeaderTemplateProperty] + }); } private void ApplyTemplate(TabControl target) { target.ApplyTemplate(); - var carousel = (Carousel)target.Pages; - carousel.ApplyTemplate(); - carousel.Presenter.ApplyTemplate(); - var tabStrip = (TabStrip)target.TabStrip; - tabStrip.ApplyTemplate(); - tabStrip.Presenter.ApplyTemplate(); + + target.Presenter.ApplyTemplate(); + + foreach (var tabItem in target.GetLogicalChildren().OfType()) + { + tabItem.Template = TabItemTemplate(); + + tabItem.ApplyTemplate(); + + ((ContentPresenter)tabItem.Presenter).UpdateChild(); + } + + target.ContentPart.ApplyTemplate(); } private class Item diff --git a/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs b/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs new file mode 100644 index 0000000000..ad0f318d2f --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs @@ -0,0 +1,105 @@ +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class ViewboxTests + { + [Fact] + public void Viewbox_Stretch_Uniform_Child() + { + var target = new Viewbox() { Child = new Rectangle() { Width = 100, Height = 50 } }; + + target.Measure(new Size(200, 200)); + target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); + + Assert.Equal(new Size(200, 100), target.DesiredSize); + var scaleTransform = target.Child.RenderTransform as ScaleTransform; + + Assert.NotNull(scaleTransform); + Assert.Equal(2.0, scaleTransform.ScaleX); + Assert.Equal(2.0, scaleTransform.ScaleY); + } + + [Fact] + public void Viewbox_Stretch_None_Child() + { + var target = new Viewbox() { Stretch = Stretch.None, Child = new Rectangle() { Width = 100, Height = 50 } }; + + target.Measure(new Size(200, 200)); + target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); + + Assert.Equal(new Size(100, 50), target.DesiredSize); + var scaleTransform = target.Child.RenderTransform as ScaleTransform; + + Assert.NotNull(scaleTransform); + Assert.Equal(1.0, scaleTransform.ScaleX); + Assert.Equal(1.0, scaleTransform.ScaleY); + } + + [Fact] + public void Viewbox_Stretch_Fill_Child() + { + var target = new Viewbox() { Stretch = Stretch.Fill, Child = new Rectangle() { Width = 100, Height = 50 } }; + + target.Measure(new Size(200, 200)); + target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); + + Assert.Equal(new Size(200, 200), target.DesiredSize); + var scaleTransform = target.Child.RenderTransform as ScaleTransform; + + Assert.NotNull(scaleTransform); + Assert.Equal(2.0, scaleTransform.ScaleX); + Assert.Equal(4.0, scaleTransform.ScaleY); + } + + [Fact] + public void Viewbox_Stretch_UniformToFill_Child() + { + var target = new Viewbox() { Stretch = Stretch.UniformToFill, Child = new Rectangle() { Width = 100, Height = 50 } }; + + target.Measure(new Size(200, 200)); + target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); + + Assert.Equal(new Size(200, 200), target.DesiredSize); + var scaleTransform = target.Child.RenderTransform as ScaleTransform; + + Assert.NotNull(scaleTransform); + Assert.Equal(4.0, scaleTransform.ScaleX); + Assert.Equal(4.0, scaleTransform.ScaleY); + } + + [Fact] + public void Viewbox_Stretch_Uniform_Child_With_Unrestricted_Width() + { + var target = new Viewbox() { Child = new Rectangle() { Width = 100, Height = 50 } }; + + target.Measure(new Size(double.PositiveInfinity, 200)); + target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); + + Assert.Equal(new Size(400, 200), target.DesiredSize); + var scaleTransform = target.Child.RenderTransform as ScaleTransform; + + Assert.NotNull(scaleTransform); + Assert.Equal(4.0, scaleTransform.ScaleX); + Assert.Equal(4.0, scaleTransform.ScaleY); + } + + [Fact] + public void Viewbox_Stretch_Uniform_Child_With_Unrestricted_Height() + { + var target = new Viewbox() { Child = new Rectangle() { Width = 100, Height = 50 } }; + + target.Measure(new Size(200, double.PositiveInfinity)); + target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); + + Assert.Equal(new Size(200, 100), target.DesiredSize); + var scaleTransform = target.Child.RenderTransform as ScaleTransform; + + Assert.NotNull(scaleTransform); + Assert.Equal(2.0, scaleTransform.ScaleX); + Assert.Equal(2.0, scaleTransform.ScaleY); + } + } +} diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs index 78d0efcbf9..ffdb146eec 100644 --- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs +++ b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs @@ -99,6 +99,8 @@ namespace Avalonia.Layout.UnitTests } }; + window.Resources["ScrollBarThickness"] = 10.0; + window.Show(); window.LayoutManager.ExecuteInitialLayoutPass(window); diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_RelativeSource.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_RelativeSource.cs index f2ddec6f3c..4c572acab1 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_RelativeSource.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_RelativeSource.cs @@ -1,9 +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.Collections.Generic; +using System.Reactive.Subjects; using Avalonia.Controls; using Avalonia.Data; -using Avalonia.Markup.Data; +using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; using Avalonia.UnitTests; using Xunit; @@ -162,5 +166,99 @@ namespace Avalonia.Markup.UnitTests.Data decorator2.Child = target; Assert.Equal("decorator2", target.Text); } + + [Fact] + public void Should_Update_When_Detached_And_Attached_To_Visual_Tree_With_BindingPath() + { + TextBlock target; + Decorator decorator1; + Decorator decorator2; + + var viewModel = new { Value = "Foo" }; + + var root1 = new TestRoot + { + Child = decorator1 = new Decorator + { + Name = "decorator1", + Child = target = new TextBlock(), + }, + DataContext = viewModel + }; + + var root2 = new TestRoot + { + Child = decorator2 = new Decorator + { + Name = "decorator2", + }, + DataContext = viewModel + }; + + var binding = new Binding + { + Path = "DataContext.Value", + RelativeSource = new RelativeSource + { + AncestorType = typeof(Decorator), + } + }; + + target.Bind(TextBox.TextProperty, binding); + Assert.Equal("Foo", target.Text); + + decorator1.Child = null; + Assert.Null(target.Text); + + decorator2.Child = target; + Assert.Equal("Foo", target.Text); + } + + [Fact] + public void Should_Update_When_Detached_And_Attached_To_Visual_Tree_With_ComplexBindingPath() + { + TextBlock target; + Decorator decorator1; + Decorator decorator2; + + var vm = new { Foo = new { Value = "Foo" } }; + + var root1 = new TestRoot + { + Child = decorator1 = new Decorator + { + Name = "decorator1", + Child = target = new TextBlock(), + }, + DataContext = vm + }; + + var root2 = new TestRoot + { + Child = decorator2 = new Decorator + { + Name = "decorator2", + }, + DataContext = vm + }; + + var binding = new Binding + { + Path = "DataContext.Foo.Value", + RelativeSource = new RelativeSource + { + AncestorType = typeof(Decorator), + } + }; + + target.Bind(TextBox.TextProperty, binding); + Assert.Equal("Foo", target.Text); + + decorator1.Child = null; + Assert.Null(target.Text); + + decorator2.Child = target; + Assert.Equal("Foo", target.Text); + } } } diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs index a97c998264..4f7264f2f2 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs @@ -1,10 +1,10 @@ -using Avalonia.Data; -using Avalonia.Markup.Parsers; -using System; +using System; using System.Collections.Generic; using System.Reactive.Linq; -using System.Text; +using System.Reactive.Subjects; using System.Threading.Tasks; +using Avalonia.Data; +using Avalonia.Markup.Parsers; using Xunit; namespace Avalonia.Markup.UnitTests.Parsers @@ -38,5 +38,55 @@ namespace Avalonia.Markup.UnitTests.Parsers GC.KeepAlive(data); } + + [Fact] + public void Should_Update_Value_After_Root_Changes() + { + var root = new { DataContext = new { Value = "Foo" } }; + var subject = new Subject(); + var obs = ExpressionObserverBuilder.Build(subject, "DataContext.Value"); + + var values = new List(); + obs.Subscribe(v => values.Add(v)); + + subject.OnNext(root); + subject.OnNext(null); + subject.OnNext(root); + + Assert.Equal("Foo", values[0]); + + Assert.IsType(values[1]); + var bn = values[1] as BindingNotification; + Assert.Equal(AvaloniaProperty.UnsetValue, bn.Value); + Assert.Equal(BindingErrorType.Error, bn.ErrorType); + + Assert.Equal(3, values.Count); + Assert.Equal("Foo", values[2]); + } + + [Fact] + public void Should_Update_Value_After_Root_Changes_With_ComplexPath() + { + var root = new { DataContext = new { Foo = new { Value = "Foo" } } }; + var subject = new Subject(); + var obs = ExpressionObserverBuilder.Build(subject, "DataContext.Foo.Value"); + + var values = new List(); + obs.Subscribe(v => values.Add(v)); + + subject.OnNext(root); + subject.OnNext(null); + subject.OnNext(root); + + Assert.Equal("Foo", values[0]); + + Assert.IsType(values[1]); + var bn = values[1] as BindingNotification; + Assert.Equal(AvaloniaProperty.UnsetValue, bn.Value); + Assert.Equal(BindingErrorType.Error, bn.ErrorType); + + Assert.Equal(3, values.Count); + Assert.Equal("Foo", values[2]); + } } } diff --git a/tests/Avalonia.RenderTests/Controls/BorderTests.cs b/tests/Avalonia.RenderTests/Controls/BorderTests.cs index c82d616094..4f4004e159 100644 --- a/tests/Avalonia.RenderTests/Controls/BorderTests.cs +++ b/tests/Avalonia.RenderTests/Controls/BorderTests.cs @@ -188,7 +188,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls } - [Fact] + [Win32Fact("Has text")] public async Task Border_Centers_Content_Horizontally() { Decorator target = new Decorator @@ -215,7 +215,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls CompareImages(); } - [Fact] + [Win32Fact("Has text")] public async Task Border_Centers_Content_Vertically() { Decorator target = new Decorator @@ -296,7 +296,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls CompareImages(); } - [Fact] + [Win32Fact("Has text")] public async Task Border_Left_Aligns_Content() { Decorator target = new Decorator @@ -323,7 +323,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls CompareImages(); } - [Fact] + [Win32Fact("Has text")] public async Task Border_Right_Aligns_Content() { Decorator target = new Decorator @@ -350,7 +350,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls CompareImages(); } - [Fact] + [Win32Fact("Has text")] public async Task Border_Top_Aligns_Content() { Decorator target = new Decorator @@ -377,7 +377,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls CompareImages(); } - [Fact] + [Win32Fact("Has text")] public async Task Border_Bottom_Aligns_Content() { Decorator target = new Decorator diff --git a/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs b/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs index 8a8ed4cb4a..14f6f78a8d 100644 --- a/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs +++ b/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs @@ -20,7 +20,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls { } - [Fact] + [Win32Fact("Has text")] public async Task Wrapping_NoWrap() { Decorator target = new Decorator diff --git a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs index 6f90dfd19e..7c53f4516a 100644 --- a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs @@ -449,5 +449,281 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } + + [Fact] + public async Task VisualBrush_Grip_96_Dpi() + { + var target = new Border + { + Width = 100, + Height = 10, + Background = new VisualBrush + { + SourceRect = new RelativeRect(0, 0, 4, 5, RelativeUnit.Absolute), + DestinationRect = new RelativeRect(0, 0, 4, 5, RelativeUnit.Absolute), + TileMode = TileMode.Tile, + Stretch = Stretch.UniformToFill, + Visual = new Canvas + { + Width = 4, + Height = 5, + Background = Brushes.WhiteSmoke, + Children = + { + new Rectangle + { + Width = 1, + Height = 1, + Fill = Brushes.Red, + [Canvas.LeftProperty] = 2, + }, + new Rectangle + { + Width = 1, + Height = 1, + Fill = Brushes.Red, + [Canvas.TopProperty] = 2, + }, + new Rectangle + { + Width = 1, + Height = 1, + Fill = Brushes.Red, + [Canvas.LeftProperty] = 2, + [Canvas.TopProperty] = 4, + } + } + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + + [Fact] + public async Task VisualBrush_Grip_144_Dpi() + { + var target = new Border + { + Width = 100, + Height = 7.5, + Background = new VisualBrush + { + SourceRect = new RelativeRect(0, 0, 4, 5, RelativeUnit.Absolute), + DestinationRect = new RelativeRect(0, 0, 4, 5, RelativeUnit.Absolute), + TileMode = TileMode.Tile, + Stretch = Stretch.UniformToFill, + Visual = new Canvas + { + Width = 4, + Height = 5, + Background = Brushes.WhiteSmoke, + Children = + { + new Rectangle + { + Width = 1, + Height = 1, + Fill = Brushes.Red, + [Canvas.LeftProperty] = 2, + }, + new Rectangle + { + Width = 1, + Height = 1, + Fill = Brushes.Red, + [Canvas.TopProperty] = 2, + }, + new Rectangle + { + Width = 1, + Height = 1, + Fill = Brushes.Red, + [Canvas.LeftProperty] = 2, + [Canvas.TopProperty] = 4, + } + } + } + } + }; + + await RenderToFile(target, dpi: 144); + CompareImages(); + } + + [Fact] + public async Task VisualBrush_Grip_192_Dpi() + { + var target = new Border + { + Width = 100, + Height = 10, + Background = new VisualBrush + { + SourceRect = new RelativeRect(0, 0, 4, 5, RelativeUnit.Absolute), + DestinationRect = new RelativeRect(0, 0, 4, 5, RelativeUnit.Absolute), + TileMode = TileMode.Tile, + Stretch = Stretch.UniformToFill, + Visual = new Canvas + { + Width = 4, + Height = 5, + Background = Brushes.WhiteSmoke, + Children = + { + new Rectangle + { + Width = 1, + Height = 1, + Fill = Brushes.Red, + [Canvas.LeftProperty] = 2, + }, + new Rectangle + { + Width = 1, + Height = 1, + Fill = Brushes.Red, + [Canvas.TopProperty] = 2, + }, + new Rectangle + { + Width = 1, + Height = 1, + Fill = Brushes.Red, + [Canvas.LeftProperty] = 2, + [Canvas.TopProperty] = 4, + } + } + } + } + }; + + await RenderToFile(target, dpi: 192); + CompareImages(); + } + + [Fact] + public async Task VisualBrush_Checkerboard_96_Dpi() + { + var target = new Border + { + Width = 200, + Height = 200, + Background = new VisualBrush + { + DestinationRect = new RelativeRect(0, 0, 16, 16, RelativeUnit.Absolute), + TileMode = TileMode.Tile, + Visual = new Canvas + { + Width = 16, + Height= 16, + Background = Brushes.Red, + Children = + { + new Rectangle + { + Width = 8, + Height = 8, + Fill = Brushes.Green, + }, + new Rectangle + { + Width = 8, + Height = 8, + Fill = Brushes.Green, + [Canvas.LeftProperty] = 8, + [Canvas.TopProperty] = 8, + }, + } + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + + [Fact] + public async Task VisualBrush_Checkerboard_144_Dpi() + { + var target = new Border + { + Width = 200, + Height = 200, + Background = new VisualBrush + { + DestinationRect = new RelativeRect(0, 0, 16, 16, RelativeUnit.Absolute), + TileMode = TileMode.Tile, + Visual = new Canvas + { + Width = 16, + Height = 16, + Background = Brushes.Red, + Children = + { + new Rectangle + { + Width = 8, + Height = 8, + Fill = Brushes.Green, + }, + new Rectangle + { + Width = 8, + Height = 8, + Fill = Brushes.Green, + [Canvas.LeftProperty] = 8, + [Canvas.TopProperty] = 8, + }, + } + } + } + }; + + await RenderToFile(target, dpi: 144); + CompareImages(); + } + + [Fact] + public async Task VisualBrush_Checkerboard_192_Dpi() + { + var target = new Border + { + Width = 200, + Height = 200, + Background = new VisualBrush + { + DestinationRect = new RelativeRect(0, 0, 16, 16, RelativeUnit.Absolute), + TileMode = TileMode.Tile, + Visual = new Canvas + { + Width = 16, + Height = 16, + Background = Brushes.Red, + Children = + { + new Rectangle + { + Width = 8, + Height = 8, + Fill = Brushes.Green, + }, + new Rectangle + { + Width = 8, + Height = 8, + Fill = Brushes.Green, + [Canvas.LeftProperty] = 8, + [Canvas.TopProperty] = 8, + }, + } + } + } + }; + + await RenderToFile(target, dpi: 192); + CompareImages(); + } } } diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs index 2892615e48..2efd28c2d5 100644 --- a/tests/Avalonia.RenderTests/TestBase.cs +++ b/tests/Avalonia.RenderTests/TestBase.cs @@ -46,6 +46,7 @@ namespace Avalonia.Direct2D1.RenderTests public TestBase(string outputPath) { + outputPath = outputPath.Replace('\\', Path.DirectorySeparatorChar); var testPath = GetTestsDirectory(); var testFiles = Path.Combine(testPath, "TestFiles"); #if AVALONIA_SKIA @@ -63,7 +64,7 @@ namespace Avalonia.Direct2D1.RenderTests get; } - protected async Task RenderToFile(Control target, [CallerMemberName] string testName = "") + protected async Task RenderToFile(Control target, [CallerMemberName] string testName = "", double dpi = 96) { if (!Directory.Exists(OutputPath)) { @@ -75,9 +76,9 @@ namespace Avalonia.Direct2D1.RenderTests var factory = AvaloniaLocator.Current.GetService(); var pixelSize = new PixelSize((int)target.Width, (int)target.Height); var size = new Size(target.Width, target.Height); - var dpi = new Vector(96, 96); + var dpiVector = new Vector(dpi, dpi); - using (RenderTargetBitmap bitmap = new RenderTargetBitmap(pixelSize, dpi)) + using (RenderTargetBitmap bitmap = new RenderTargetBitmap(pixelSize, dpiVector)) { target.Measure(size); target.Arrange(new Rect(size)); @@ -85,7 +86,7 @@ namespace Avalonia.Direct2D1.RenderTests bitmap.Save(immediatePath); } - using (var rtb = factory.CreateRenderTargetBitmap(pixelSize, dpi)) + using (var rtb = factory.CreateRenderTargetBitmap(pixelSize, dpiVector)) using (var renderer = new DeferredRenderer(target, rtb)) { target.Measure(size); diff --git a/tests/Avalonia.RenderTests/TestSkip.cs b/tests/Avalonia.RenderTests/TestSkip.cs new file mode 100644 index 0000000000..75407332e3 --- /dev/null +++ b/tests/Avalonia.RenderTests/TestSkip.cs @@ -0,0 +1,21 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using Xunit; + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests +#endif +{ + public class Win32Fact : FactAttribute + { + public Win32Fact(string message) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + Skip = message; + } + } +} + diff --git a/tests/Avalonia.UnitTests/MockAssetLoader.cs b/tests/Avalonia.UnitTests/MockAssetLoader.cs index 11d66128a7..988fe85152 100644 --- a/tests/Avalonia.UnitTests/MockAssetLoader.cs +++ b/tests/Avalonia.UnitTests/MockAssetLoader.cs @@ -32,10 +32,9 @@ namespace Avalonia.UnitTests return (Open(uri, baseUri), (Assembly)null); } - public IEnumerable<(string absolutePath, Assembly assembly)> GetAssets(Uri uri) + public IEnumerable GetAssets(Uri uri, Uri baseUri) { - return _assets.Keys.Where(x => x.AbsolutePath.Contains(uri.AbsolutePath)) - .Select(x => (x.AbsolutePath, Assembly.GetEntryAssembly())); + return _assets.Keys.Where(x => x.AbsolutePath.Contains(uri.AbsolutePath)); } public void SetDefaultAssembly(Assembly asm) diff --git a/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FontFamilyKeyTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FontFamilyKeyTests.cs index 96464b5784..b57ba781ed 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FontFamilyKeyTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FontFamilyKeyTests.cs @@ -22,9 +22,7 @@ namespace Avalonia.Visuals.UnitTests.Media.Fonts var fontFamilyKey = new FontFamilyKey(source); - Assert.Equal(new Uri("resm:Avalonia.Visuals.UnitTests"), fontFamilyKey.Location); - - Assert.Null(fontFamilyKey.FileName); + Assert.Equal(new Uri("resm:Avalonia.Visuals.UnitTests"), fontFamilyKey.Source); } [Fact] @@ -34,9 +32,7 @@ namespace Avalonia.Visuals.UnitTests.Media.Fonts var fontFamilyKey = new FontFamilyKey(source); - Assert.Equal(new Uri("resm:Avalonia.Visuals.UnitTests"), fontFamilyKey.Location); - - Assert.Equal("MyFont.ttf", fontFamilyKey.FileName); + Assert.Equal(new Uri("resm:Avalonia.Visuals.UnitTests.MyFont.ttf"), fontFamilyKey.Source); } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs index a53809a029..ab5f5f37f7 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs @@ -405,7 +405,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering root.Renderer = new DeferredRenderer(root, null); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); - + + root.Renderer.Paint(Rect.Empty); var result = root.Renderer.HitTest(new Point(50, 150), root, null).First(); Assert.Equal(item1, result); @@ -421,6 +422,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering container.InvalidateArrange(); container.Arrange(new Rect(container.DesiredSize)); + root.Renderer.Paint(Rect.Empty); result = root.Renderer.HitTest(new Point(50, 150), root, null).First(); Assert.Equal(item2, result); diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs index 82294246b1..73e4a14539 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs @@ -1,8 +1,11 @@ using System.Collections.Generic; using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.UnitTests; using Avalonia.VisualTree; using Moq; using Xunit; @@ -94,5 +97,86 @@ namespace Avalonia.Visuals.UnitTests.Rendering //then new position Assert.Equal(new Rect(100, 100, 100, 100), invalidationCalls[2]); } + + [Fact] + public void Should_Render_Child_In_Parent_With_RenderTransform() + { + var targetMock = new Mock() { CallBase = true }; + var target = targetMock.Object; + target.Width = 100; + target.Height = 50; + var child = new Panel() + { + RenderTransform = new RotateTransform() { Angle = 90 }, + Children = + { + new Panel() + { + Children = + { + target + } + } + } + }; + + var visualTarget = targetMock.As(); + int rendered = 0; + visualTarget.Setup(v => v.Render(It.IsAny())).Callback(() => rendered++); + + var root = new TestRoot(child); + root.Renderer = new ImmediateRenderer(root); + + root.LayoutManager.ExecuteInitialLayoutPass(root); + + root.Measure(new Size(50, 100)); + root.Arrange(new Rect(new Size(50, 100))); + + root.Renderer.Paint(root.Bounds); + + Assert.Equal(1, rendered); + } + + [Fact] + public void Should_Render_Child_In_Parent_With_RenderTransform2() + { + var targetMock = new Mock() { CallBase = true }; + var target = targetMock.Object; + + target.Width = 100; + target.Height = 50; + target.HorizontalAlignment = HorizontalAlignment.Center; + target.VerticalAlignment = VerticalAlignment.Center; + + var child = new Panel() + { + RenderTransform = new RotateTransform() { Angle = 90 }, + Children = + { + new Panel() + { + Children = + { + target + } + } + } + }; + + var visualTarget = targetMock.As(); + int rendered = 0; + visualTarget.Setup(v => v.Render(It.IsAny())).Callback(() => rendered++); + + var root = new TestRoot(child); + root.Renderer = new ImmediateRenderer(root); + + root.LayoutManager.ExecuteInitialLayoutPass(root); + + root.Measure(new Size(300, 100)); + root.Arrange(new Rect(new Size(300, 100))); + root.Renderer.Paint(root.Bounds); + + Assert.Equal(1, rendered); + } } } diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_144_Dpi.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_144_Dpi.expected.png new file mode 100644 index 0000000000..7e674b849a Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_144_Dpi.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_192_Dpi.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_192_Dpi.expected.png new file mode 100644 index 0000000000..f80ac7f90d Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_192_Dpi.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_96_Dpi.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_96_Dpi.expected.png new file mode 100644 index 0000000000..0f88a0d468 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_96_Dpi.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_144_Dpi.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_144_Dpi.expected.png new file mode 100644 index 0000000000..1900ade5e2 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_144_Dpi.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_192_Dpi.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_192_Dpi.expected.png new file mode 100644 index 0000000000..a209dc94cc Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_192_Dpi.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_96_Dpi.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_96_Dpi.expected.png new file mode 100644 index 0000000000..6ac1bdb088 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_96_Dpi.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_144_Dpi.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_144_Dpi.expected.png new file mode 100644 index 0000000000..7e674b849a Binary files /dev/null and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_144_Dpi.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_192_Dpi.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_192_Dpi.expected.png new file mode 100644 index 0000000000..f80ac7f90d Binary files /dev/null and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_192_Dpi.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_96_Dpi.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_96_Dpi.expected.png new file mode 100644 index 0000000000..0f88a0d468 Binary files /dev/null and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_96_Dpi.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_144_Dpi.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_144_Dpi.expected.png new file mode 100644 index 0000000000..d530c72519 Binary files /dev/null and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_144_Dpi.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_192_Dpi.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_192_Dpi.expected.png new file mode 100644 index 0000000000..a209dc94cc Binary files /dev/null and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_192_Dpi.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_96_Dpi.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_96_Dpi.expected.png new file mode 100644 index 0000000000..6ac1bdb088 Binary files /dev/null and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_96_Dpi.expected.png differ