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/.ncrunch/Avalonia.Native.v3.ncrunchproject b/.ncrunch/Avalonia.Native.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/Avalonia.Native.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file 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/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 06f808b726..0f7f2e80a8 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -1,31 +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 index 5b10e7d790..01ddc0ddca 100644 --- a/samples/ControlCatalog/Pages/TabControlPage.xaml +++ b/samples/ControlCatalog/Pages/TabControlPage.xaml @@ -1,4 +1,7 @@ - + - - - - - + This is the first page in the TabControl. - + - - - - - + This is the second page in the TabControl. - + - - - - - + You should not see this. @@ -82,10 +61,7 @@ + Text="{Binding Header}"> diff --git a/samples/ControlCatalog/Pages/TabControlPage.xaml.cs b/samples/ControlCatalog/Pages/TabControlPage.xaml.cs index 808d90a49c..a38a3ab4cb 100644 --- a/samples/ControlCatalog/Pages/TabControlPage.xaml.cs +++ b/samples/ControlCatalog/Pages/TabControlPage.xaml.cs @@ -26,13 +26,13 @@ namespace ControlCatalog.Pages { Header = "Arch", Text = "This is the first templated tab page.", - Image = LoadBitmap("resm:ControlCatalog.Assets.delicate-arch-896885_640.jpg?assembly=ControlCatalog"), + Image = LoadBitmap("avares://ControlCatalog/Assets/delicate-arch-896885_640.jpg"), }, new TabItemViewModel { Header = "Leaf", Text = "This is the second templated tab page.", - Image = LoadBitmap("resm:ControlCatalog.Assets.maple-leaf-888807_640.jpg?assembly=ControlCatalog"), + Image = LoadBitmap("avares://ControlCatalog/Assets/maple-leaf-888807_640.jpg"), }, new TabItemViewModel { 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 3ec8e43b07..f97a3064e7 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -1,5 +1,6 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:Class="ControlCatalog.SideBar"> + @@ -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/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 index 088f9e30ea..a6a64e570b 100644 --- a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs @@ -21,6 +21,8 @@ namespace Avalonia.Controls.Generators tabItem.ParentTabControl = Owner; + tabItem[~TabControl.TabStripPlacementProperty] = Owner[~TabControl.TabStripPlacementProperty]; + if (tabItem.HeaderTemplate == null) { tabItem[~HeaderedContentControl.HeaderTemplateProperty] = Owner[~ItemsControl.ItemTemplateProperty]; diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 9d4cbb9260..d74078c712 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -249,8 +249,6 @@ namespace Avalonia.Controls if (containerControl != null) { ((ISetLogicalParent)containerControl).SetParent(this); - containerControl.SetValue(TemplatedParentProperty, null); - containerControl.UpdateChild(); if (containerControl.Child != null) 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/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/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 296134ca48..ba4c5027d0 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -260,7 +260,7 @@ namespace Avalonia.Controls.Primitives var child = template.Build(this); var nameScope = new NameScope(); NameScope.SetNameScope((Control)child, nameScope); - child.SetValue(TemplatedParentProperty, this); + ApplyTemplatedParent(child); RegisterNames(child, nameScope); ((ISetLogicalParent)child).SetParent(this); VisualChildren.Add(child); @@ -326,6 +326,23 @@ namespace Avalonia.Controls.Primitives InvalidateMeasure(); } + /// + /// Sets the TemplatedParent property for the created template children. + /// + /// The control. + private void ApplyTemplatedParent(IControl control) + { + control.SetValue(TemplatedParentProperty, this); + + foreach (var child in control.LogicalChildren) + { + if (child is IControl c) + { + ApplyTemplatedParent(c); + } + } + } + /// /// Registers each control with its name scope. /// diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 80a3846ab2..47a2348d59 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -23,8 +23,6 @@ namespace Avalonia.Controls public static readonly StyledProperty IsSelectedProperty = ListBoxItem.IsSelectedProperty.AddOwner(); - private TabControl _parentTabControl; - /// /// Initializes static members of the class. /// @@ -56,11 +54,7 @@ namespace Avalonia.Controls set { SetValue(IsSelectedProperty, value); } } - internal TabControl ParentTabControl - { - get => _parentTabControl; - set => _parentTabControl = value; - } + internal TabControl ParentTabControl { get; set; } private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj) { diff --git a/src/Avalonia.Controls/Templates/TemplateExtensions.cs b/src/Avalonia.Controls/Templates/TemplateExtensions.cs index 09da737836..18c8bfdeda 100644 --- a/src/Avalonia.Controls/Templates/TemplateExtensions.cs +++ b/src/Avalonia.Controls/Templates/TemplateExtensions.cs @@ -24,12 +24,14 @@ namespace Avalonia.Controls.Templates { foreach (IControl child in control.GetVisualChildren()) { - if (child.TemplatedParent == templatedParent) + var childTemplatedParent = child.TemplatedParent; + + if (childTemplatedParent == templatedParent) { yield return child; } - if (child.TemplatedParent != null) + if (childTemplatedParent != null) { foreach (var descendant in GetTemplateChildren(child, templatedParent)) { 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